Internal tooling · plan for review

A guided studio for configuring onboarding forms per country — without touching JSON by hand.

Digital Onboarding Studio lets non-engineers safely author the base forms, country/channel patches, and journeys that the Digital Onboarding Service consumes — with a live customer-accurate preview, built-in validation, and every change emitted as a downloadable bundle that ships as a reviewable PR. It encodes the FormEngine's own rules so the tool can't produce config the backend would reject.

Angular + TypeScript Downloadable change bundle PR-review governance Live WYSIWYG preview Form config only (for now)

§1 Executive summary

Today, configuring onboarding forms for a country means hand-editing embedded JSON files that follow strict filename conventions, hand-writing RFC 6902 patch operations, and getting the naming/versioning exactly right — then rebuilding. It's powerful but error-prone and gated behind engineering. Digital Onboarding Studio turns that into a guided, visual, validated workflow.

What it does

Browse the form catalogue, edit base forms, author country & channel patches via visual diffs, build journeys, define validation, and preview the exact customer result — then download the change bundle.

Who uses it

Product/onboarding config authors (primary), reviewers/approvers (engineers or leads), and QA — with engineering owning the merge.

Why now

Expanding to new countries currently bottlenecks on engineers. The base+overlay model is already perfect for a tool — we just need a safe editor over it.

Design bet

The tool generates the filenames, JSON-Pointers, and patch ops. Authors think in fields, forms and rules — never in file paths or JSON pointers.

North star: a product author can stand up a new country's retail onboarding — forms, country tweaks, journey, validation — in an afternoon, see exactly what the customer will see, and hand engineering a clean, reviewable change set that the service seeds without changes.

§2 Scope & non-goals

In scope (this initiative)

  • Base forms — the _schema.json + _ui.json pairs under SeedData/Forms/.
  • Patches — country and channel overlays under SeedData/Patches/{CC}/ (RFC 6902), for both Schema and UiConfig targets.
  • Journey configs — per-country SeedData/JourneyConfig/{CC}.json: steps, branches, conditions, exit rules, workflow config.
  • Validation authoring — field constraints and the x-crossFieldRules expression rules.
  • Live preview and bundle export for PR-based publishing.

Explicit non-goals (for now)

  • Anything outside FormEngine config — no MDM, Documents, Onboarding runtime, or webhook management.
  • Authoring C# — new named branch predicates (e.g. isFatcaCountry) remain an engineering task; the tool only selects from registered predicates.
  • Editing live customer journeys/instances or any customer data. This tool configures; it does not operate.
  • Translations content management — the tool works with i18n keys; it may surface missing keys but does not own the translation store (see Open decisions).
Reference: the mechanics this tool automates are documented in the Form Configuration Guide; overall service context is in the Developer Guide. This plan assumes that model.

§3 Guiding principles (inherited from the backend)

The tool must be a faithful editor for the FormEngine's model — every rule the backend enforces, the tool enforces earlier and more visibly.

Backend principleHow the tool honours it
Base + overlay (patches change only what differs)Authors edit a base once; per-country changes are always expressed as patches, never as forked copies. The tool diffs base→desired and emits the minimal patch.
Configuration over codeEverything the tool produces is JSON seed data; no code generation, no engineering for standard changes.
Filename is the contractThe tool owns naming ({key}_base_v{n}_…, {key}_{cc}_v{n}_{channel}_{target}_patch.json). Authors never type a filename.
RFC 6902 patchesPatch ops are generated from a visual before/after diff; authors don't hand-write pointers or ops.
Draft-07 schema + x- extensionsSchema editing is field-oriented; x-labelKey/x-enumLabels/x-errorMessages are first-class fields, not raw JSON.
Two-layer validationField constraints + x-crossFieldRules are edited and previewed with the same evaluation the backend uses (§6).
Channel precedence (base then channel, then version)Preview replicates resolution order exactly; the tool shows which overlays applied.
Versioning & idempotent seedingThe tool guides "edit in place (non-prod) vs new version (prod)" and prevents accidental edits to already-published versions.
i18n keys, never literalsAll human text is a key; the tool can flag keys not present in the translation source.

§4 Decisions already taken

chosen

Persistence: downloadable bundle

The tool reads SeedData JSON and emits a zip of the changed files at their repository paths. A human copies them in and raises the PR. CI rebuild + startup seeding publishes. Zero backend write API, and no Git credential anywhere in the system.

chosen

Governance: ADO permissions + in-app

Who may change configuration is decided by Azure DevOps repo permissions and branch policies — already governed, already audited. The tool adds an in-app read-only review/diff so reviewers can vet a change set (visual diff + preview) alongside the PR.

chosen

Preview: backend-powered

Essential live preview via Option A — a read-only preview/resolve endpoint in FormEngine that reuses the real resolver/validator. We have the backend repo; use it (§6).

chosen

SPA: Angular 22+ · Tailwind

Angular v22 or higher, TypeScript throughout, styled with Tailwind CSS.

chosen

Design system

Use the components & tokens from the Global Tokens / Visual Language Figma design system. Tokens map into the Tailwind theme (§11).

chosen

Repos: split

Frontend lives in a separate repo (form-config-studio); the .NET BFF is a branch of this repo (feature/form-config-studio).

chosen

BFF: .NET, in this repo

The Config BFF is .NET, reusing the backend's FormEngine models, developed on a dedicated branch of this repo scoped to the config app (§5, §11).

chosen

i18n: keys + mapper

Author with i18n keys, plus a translation mapper. Store = a single JSON file, { "key": { "en": "…", "fr": "…" } } (§8, J9).

chosen

Targets: chosen at PR time

The bundle is target-agnostic — the same files apply to any branch. Which branch a change lands on is decided when the author opens the PR, and governed by ADO branch policy rather than by the tool.

chosen

Auth: open for now

v1 runs unauthenticated/open. The BFF is read-only and holds no credential, so an open endpoint exposes configuration already committed to the repo. Auth & roles are deferred — the design keeps a seam for OIDC/roles later (§13).

Revised: the tool no longer opens PRs itself. The original plan had the BFF create a branch, commit and open a PR via the Git host REST API. That requires a deployed service to hold an Azure DevOps credential, and an ADO PAT cannot be scoped to a single repository — its reach is every repo its owning identity can read or write, so a leak exposes far more than onboarding config. Concentrating one identity's org-wide access behind a network endpoint is a poor trade for saving a git push. The tool now emits a bundle and a human raises the PR; access control stays with ADO, where it already exists and is already audited. If we later need a path for authors without repo access, it will be an API that writes configuration directly — not a Git credential.

§5 Architecture

Three moving parts: the Angular SPA, a thin read-only Config BFF (backend-for-frontend) that serves seed data and preview, and the existing FormEngine (touched only by a small read-only preview endpoint). Nothing in the system can write to the repository.

Angular SPAeditors, visual diff, journey builder, preview pane, bundle export
→
Config BFF (.NET, this repo)reads SeedData, resolves previews — no credentials, no write path
→
Authordownloads the bundle, applies it to a clone, raises the PR in ADO
Config BFFreferences FormEngine components in-process ↓
→
FormResolver + JsonPatchEngine + ValidationEnginereal production code — powers preview/resolve, persists nothing

The Config BFF#

Decision: the BFF is a .NET service that reuses the backend's FormEngine models and components, developed on a dedicated branch of this repo scoped to the config app. Because it lives in the same solution, it can reference the real JsonPatchEngine, FormResolver, ValidationEngine, and the ResourceNameParser directly — so patch application, resolution, validation, and filename parsing are the same code as production, not reimplementations. It's also the natural host for the read-only preview/resolve endpoint (§6). Responsibilities:

  • Seed data read — list the catalogue and read base forms / patches / journey files. The seed files are EmbeddedResources in OmniDos.FormEngine.Infra, so a deployed BFF serves them from its own assembly with no repository checkout on disk.
  • Patch generation — given a base document and the author's edited document, compute the minimal RFC 6902 op array (the artifact that becomes a _patch.json).
  • Guardrails — enforce the naming contract, no-op detection, pointer existence, and version rules server-side (defense in depth beyond the SPA).
Why a BFF at all, if it can't write? Preview parity is the reason it exists (§6): resolution and validation must be the same code as production, which means running it in .NET next to FormEngine. Serving the catalogue from the same process is then free. Writing was never the justification — and removing it leaves a service that holds nothing worth stealing.

Environments#

The bundle is target-agnostic: the same files apply to any branch, so the author chooses the target when raising the PR (mirroring the existing pipeline's main/releases/*/pilot/releases/*). What the studio reads is per-environment — a BFF deployed alongside a service instance serves that instance's seed data, so the studio shows what that environment actually has. Non-prod targets may allow ClearBeforeSeeding edits-in-place; prod targets steer authors toward new versions (§ Journey J7).

§6 Preview & validation parity (the crux)

"Essential preview" only earns trust if what the author sees is exactly what the customer will get. That means the tool's resolution + validation must match the backend bit-for-bit.

Decision: Option A. Since the BFF is .NET in this repo (§5), preview is served by calling the real FormEngine components in-process — no re-implementation and, because the BFF reuses them directly, not even a change to the deployed onboarding service. Option B (a TS engine) is dropped for v1; it can return later purely as an offline UX accelerator, gated by parity tests.

Option A (chosen) — reuse the real components#

The BFF exposes a stateless, compute-only POST preview/resolve that accepts draft inputs (base schema+ui, the ordered patches, country, channel) and returns the resolved schema+ui plus validation results by invoking the real FormResolver, JsonPatchEngine and ValidationEngine — persisting nothing. Parity is guaranteed by construction, and nothing is stored, so it respects the Git/PR model.

One refactor needed: FormResolver today loads the base version and patches from repositories and caches results. For draft preview we need to resolve from supplied base+patches with caching bypassed. Plan: extract a pure "apply ordered patches → schema/ui" core (already essentially ApplyPatches + OrderByPrecedence) that both the repository-backed resolver and the draft-preview path call. Low-risk, and it makes the resolver more testable too.
The existing get/form and post/validate-form actions already resolve/validate seeded config. The new endpoint is the same logic over unsaved draft input — the minimal, safe backend addition that de-risks the whole preview.

Option B (deferred) — replicate resolution in TypeScript#

Not built for v1. If instant offline feedback ever justifies it, mirror the pipeline client-side (RFC 6902 apply, draft-07 validation, a TS port of the expression language) — but only behind a shared golden test-vector suite (inputs → expected output) run against both engines in CI, with Option A remaining the source of truth. For v1 we simply call the BFF, which is fast enough given it's in-process.

§7 Domain model inside the tool

The tool's internal model mirrors the backend entities so the mapping to files is mechanical.

Tool conceptBackend artifactAuthor edits…
Form (catalogue item)FormDefinition + FormVersionfields, constraints, UI layout, widgets — as a base
Country overlayFormPatch (Schema + UiConfig)a before/after that becomes a patch
Channel overlaychannel-tagged FormPatchlayout/widget tweaks for one channel
JourneyJourneyConfig/{CC}.jsonsteps, order, branches, conditions, exit rules, workflow config
Validation ruleschema constraints + x-crossFieldRulesfield rules & cross-field expressions
Country workspacea {CC}/ patch folder + {CC}.jsonthe full per-country bundle

§8 Feature areas

read

Catalogue & country browser

List base forms and countries; see which forms a country patches, on which channels, and applied-patch counts. Entry point for everything.

Base form editor

Field-oriented schema editor (type, constraints, enum + labels, error keys) side-by-side with a raw-JSON escape hatch (Monaco). UI editor for layout (drag rows/columns) and widget mapping.

Patch author (visual diff)

Open a base for a country/channel, edit the resolved copy, and the tool computes the minimal RFC 6902 patch. Shows the generated ops read-only for transparency.

Journey builder

Visual step list + branch canvas: reorder steps, attach forms, define branch conditions (predicate/operator pickers), exit rules, and workflow config.

Cross-field rule builder

Guided builder for x-crossFieldRules expressions with field pickers, function palette, and an inline "does this pass?" tester against sample data.

Live preview

Country + channel selector renders the resolved form as the customer sees it; toggles show applied patches and i18n keys vs resolved sample text.

Validation panel

Live field + cross-field validation and a "lint" pass: naming contract, no-op patches, dangling pointers, missing i18n keys, version rules.

new

Translation mapper

Every human string is an i18n key. The mapper collects all keys used by a form/journey, shows coverage per language, lets authors fill/edit key→text, and flags missing keys before publish.

In-app review / diff

Reviewers open a change set read-only inside the tool — the same visual diff + live preview the author saw — alongside the Git PR.

Review & download

Review a human-readable change summary + file diff, then download the change bundle — the files at their repository paths, plus a ready-to-paste PR description.

Translation mapper — design note#

Forms and journeys reference i18n keys throughout — x-labelKey, x-placeholderKey, x-enumLabels, x-errorMessages, cross-field errorKey, step title, x-pageTitle. The mapper closes the loop between "I authored a key" and "the customer sees translated text."

  • Key extraction — statically walk the resolved schema/uiSchema + journey to enumerate every key a country/channel actually uses.
  • Coverage view — per language, show which keys have values and which are missing; block publish (or warn) on gaps.
  • Authoring — edit key→text per language inline; the same PR flow ships the translation changes.
  • Preview integration — the live preview can toggle between showing raw keys and resolved text using the mapper's values.
Open: where translations live. The backend doesn't own translations. The mapper needs a source of truth — options: (a) versioned per-language JSON key→value files in this repo (shipped via the same PR), (b) the frontend app's translation resources, or (c) an external translation-management system the mapper syncs with. Recommended v1 default: (a) — keeps everything in one PR and one review. Confirm in §14.

§9 UX principles & guardrails

Make the easy thing the safe thing

  • Never expose the plumbing. No filenames, no JSON-Pointers, no raw op arrays required. They're generated and shown read-only for the curious.
  • Diff-first. Country/channel changes are always shown as "base → your change," so authors reason about deltas, exactly like patches.
  • Guided, not blank-page. New country = clone an existing one and adjust. New form = start from a template. Reduce every task to editing something that already works.
  • Validate continuously. Inline errors as you type; a change can't be published while the lint panel has blocking issues.
  • Preview is always one glance away. Split view: editor left, live customer preview right, channel switcher on top.
  • Escape hatches for experts. Raw JSON / raw patch view is available, round-trips safely, and is reconciled with the visual model.
Idempotency guardrail. Because seeding skips already-seeded (FormKey, Version), the tool must detect when an author edits a base that's already published to the target and prompt: "bump to a new version" (prod) vs "edit in place / re-seed" (non-prod with ClearBeforeSeeding). This prevents the classic "my edit didn't show up" trap.

§10 User journeys

The core deliverable of this plan. Each journey lists the actor, trigger, preconditions, the step-by-step flow, the artifacts produced (which files), and the guardrails that fire. Primary actor is the Config Author unless noted.

J1

Stand up a new country (clone & adapt)

Actor
Config Author
Trigger
"We're launching retail onboarding in Tanzania (TZ)."
Preconditions
Base forms exist; a comparable country (KE) is configured; author knows what differs for TZ.
  1. Author clicks New country → picks code TZ and a clone source (KE).
  2. The tool creates a TZ workspace: a copy of KE's journey as a starting draft, and an empty patch set (no overlays yet — TZ inherits base forms).
  3. Author walks the journey in the Journey builder, removing/reordering steps and adjusting branches/workflow config for TZ (see J5).
  4. For each form that must differ (e.g. ID types on basic_info), author opens it in the Patch author and edits the resolved TZ copy; the tool records patches (see J2/J3).
  5. Author adds/edits validation where TZ differs (see J4).
  6. Live preview is switched to TZ + each channel to confirm the end-to-end result.
  7. Lint panel must be clean; author hits Publish → J8.

Artifacts: JourneyConfig/TZ.json + Patches/TZ/*.json (only where TZ differs). Guardrails: country-code uniqueness, journey references only existing form keys, naming auto-generated, no-op patches dropped.

J2

Tweak a field for an existing country

Actor
Config Author
Trigger
"Kenya needs the 'Maisha' ID type added to Basic Info."
Preconditions
KE journey + base basic_info exist.
  1. From the catalogue, author opens basic_info → context KE / all channels. The tool shows the resolved form (base + existing KE patches).
  2. Author edits the documentType field: adds the MAISHA enum value and its x-enumLabels i18n key — all via form controls.
  3. The tool computes the minimal patch (a replace on /properties/documentType/enum and /x-enumLabels) and shows it read-only.
  4. Preview updates instantly; validation confirms the enum/labels are consistent; lint confirms the i18n key exists (or flags it).
  5. Publish → J8.

Artifacts: creates/updates Patches/KE/basic_info_ke_v1_schema_patch.json. Guardrails: enum values ↔ enum-label keys kept in sync; patch stays minimal; base form untouched.

J3

Add a channel-specific layout override

Actor
Config Author
Trigger
"On web, Basic Info should be two columns; leave mobile as-is."
Preconditions
KE basic_info resolved for web.
  1. Author opens basic_info → context KE / Web, switches the editor to the UI/layout tab.
  2. Author drags fields into a two-column layout. The tool detects this is a channel change and scopes the patch to web.
  3. The generated patch targets UiConfig for the web channel only; preview on Web shows two columns, Android/iOS unchanged.
  4. Publish → J8.

Artifacts: Patches/KE/basic_info_ke_v1_web_ui_patch.json. Guardrails: channel token added to filename automatically; precedence explained in-UI (base overlay then channel overlay).

J4

Add or edit a cross-field validation rule

Actor
Config Author
Trigger
"At least one of phone or email must be provided."
Preconditions
The target form is open for a country/channel.
  1. In the form's Validation tab, author clicks Add cross-field rule.
  2. Author builds the expression with field pickers + function palette: !isNullOrEmpty(data.phoneNumber) || !isNullOrEmpty(data.email), and picks/creates an errorKey.
  3. The inline tester lets the author enter sample data and see pass/fail using the same evaluation as the backend (§6).
  4. The rule is written into x-crossFieldRules (base form) or as a patch op if it's country/channel-specific.
  5. Publish → J8.

Artifacts: base schema x-crossFieldRules entry, or a patch op (add /x-crossFieldRules/-). Guardrails: expression parsed & validated against the grammar before save; unknown functions/fields flagged.

J5

Build or modify a journey

Actor
Config Author
Trigger
"TZ skips Next-of-Kin and adds a CRS branch."
Preconditions
Country workspace exists; forms referenced exist.
  1. Author opens the Journey builder for the country's journey (e.g. retail_onboarding, version).
  2. Author reorders/removes steps (each step = a form key + type: Form/Info/Review/Confirm/Consent) via drag-and-drop.
  3. Author adds a branch: sets triggerAfterStep, resumeBeforeStep, a condition (pick a registered predicate like isCrsCountry, or build an operator comparison on a source-step field), and the branch's steps.
  4. Author adds exit rules on consent steps (e.g. terminate journey if consent declined).
  5. Author sets workflow config (initiateAfterStepId, completeOnStepId, otpEnabled).
  6. The builder validates references (every step's form exists; branch trigger/resume steps exist) and previews the flow. Publish → J8.

Artifacts: JourneyConfig/{CC}.json. Guardrails: only registered predicates selectable; dangling step references blocked; branch trigger/resume must be real steps.

Note the step-id vs form distinction: the builder treats a step's id as its navigation identity and its formKey+formVersion as the form it renders — surfaced as separate fields so authors don't conflate them.
J6

Add a brand-new base form

Actor
Config Author (with engineering review)
Trigger
"We need a new 'proof_of_income' form that doesn't exist yet."
Preconditions
Widget types needed are supported by the frontend renderer.
  1. Author clicks New base form → names the key (proof_of_income), starts at version 1, optionally from a template.
  2. Author defines fields (schema) and layout/widgets (UI) in the base editor.
  3. Author references the new form from a journey step (J5) and optionally patches it per country (J2/J3).
  4. Lint flags any widget the renderer doesn't know and any missing i18n keys. Publish → J8.

Artifacts: Forms/proof_of_income_base_v1_schema.json + …_ui.json. Guardrails: widget catalogue check; key format check; version starts at 1.

J7

Version a form safely

Actor
Config Author
Trigger
"Change a live form's shape without breaking in-flight journeys."
Preconditions
Target form/version already published to the target branch.
  1. Author edits a published base; the tool detects it's live on the target and offers Create version v+1.
  2. The tool clones the base to the next version, re-targets country patches to the new version, and offers to bump the journey step's formVersion.
  3. Preview confirms old vs new; publish → J8. Old cached/in-flight resolutions keep using the prior version.

Artifacts: new …_base_v2_*, re-versioned patches, updated journey step. Guardrails: blocks silent edits to already-seeded versions; explains idempotent-seeding behaviour.

J8

Review & download (and roll back)

Actors
Config Author → Reviewer/Engineer
Trigger
A change set is complete and lint-clean.
Preconditions
Repo access for whoever raises the PR (the author, or someone they hand the bundle to).
  1. Author opens Review & download: sees a plain-language summary ("KE · basic_info · added MAISHA ID type; web 2-column layout") plus the raw file diff.
  2. Download bundle emits a zip: repo/ (the changed files at their repository paths), PR.md (a filled-in PR description), manifest.json (data source + timestamp) and README.txt.
  3. The author copies repo/ over a clone, commits to a branch, pushes, and opens the PR, pasting PR.md as the description.
  4. A Reviewer reviews the diff in the Git host (or an in-app read-only diff), approves, and merges. CI rebuild + startup seeding publishes.
  5. Rollback = revert the PR in git — a normal repository operation, with no tool involvement.

Artifacts: a zip containing only the changed SeedData files, plus the PR description and provenance stamp. Guardrails: can't download with blocking lint errors; PR.md auto-documents scope for the reviewer; manifest.json makes a bundle built on stale seed data identifiable.

J9

Map translations for authored keys

Actor
Config Author (or a Translator role later)
Trigger
New/changed config introduced i18n keys that need text.
Preconditions
A form/journey change set exists; target languages known.
  1. After editing (J2–J6), the author opens the Translation mapper for the change set.
  2. The tool extracts every i18n key the resolved config uses and shows a coverage grid (keys × languages), highlighting missing values.
  3. Author fills in text for each missing key per language; preview can render resolved text instead of keys to sanity-check.
  4. Lint blocks (or warns) on missing translations per the configured policy.
  5. Translation changes are bundled into the same PR as the config change (J8).

Artifacts: per-language key→value entries in the chosen translation store (v1 default: versioned JSON files in this repo). Guardrails: key coverage check; consistent key naming; no orphan translations.

Supporting / secondary journeys

  • Preview across channels — switch country/channel and toggle "show i18n keys vs sample text" and "highlight applied patches."
  • Compare countries — side-by-side of how two countries configure the same form (useful when cloning).
  • Find usages — "which journeys/countries use this base form/version?" before versioning or editing.
  • Reviewer read-only mode — open a PR's change set inside the tool with the same visual diff + preview the author saw.

§11 Tech stack & key libraries

ConcernChoiceNotes
SPAAngular v22+ + TypeScriptScaffolded with @angular/cli@latest (must be ≥ 22). Standalone components, Angular signals (or a store if state grows) for editor/preview state.
StylingTailwind CSSUtility-first; the design-system tokens are wired into the Tailwind theme (see below). Component styles compose tokens, not ad-hoc values.
Design systemFigma "Global Tokens / Visual Language"Tokens (color, typography, spacing, radius, elevation) + components are the visual source of truth. Extracted to a tokens layer that feeds Tailwind (see "Design system & styling" below).
Raw JSON editingMonaco editorSchema-aware editing + the expert escape hatch, round-tripped with the visual model.
Schema-driven form render (preview)A JSON-Schema form rendererMust honour the x- extensions + layout/widgets. Likely a thin custom renderer matching the real frontend's widget set rather than an off-the-shelf one, for fidelity.
RFC 6902 patchesfast-json-patch (or equivalent)Generate (diff) + apply (preview) patch ops in the BFF/SPA.
Schema validationajv (draft-07)Client-side field validation; parity checked against backend.
Journey flow canvasA graph/flow libraryFor the branch/step visualization.
Config BFF.NET, in this repo (dedicated branch)Reuses FormEngine models & components directly (patch engine, resolver, validators, name parser) and hosts the in-process preview/resolve. Same solution as the backend; scoped to the config app.
Git integrationNoneDeliberately absent. The studio emits a zip; the author applies it to a clone and raises the PR with their own credentials. No Git client, no token, no write path.
TranslationsSingle JSON fileShape: { "key": { "en": "…", "fr": "…" } } — each i18n key maps to a per-language string object. Versioned in the repo, shipped via the same PR. Mapper reads coverage + writes values.
AuthNone in v1 (open)Unauthenticated on lower envs for now. Keep a clean seam (route guards + BFF auth middleware stubs) so OIDC/JWT + roles (Author/Reviewer/Admin) drop in later.

Design system & styling#

The Global Tokens / Visual Language Figma file is the visual source of truth. The integration is token-driven so the UI stays faithful and themeable:

Figma tokenscolor · type · spacing · radius · elevation
→
Tokens layerdesign-tokens JSON → CSS variables
→
Tailwind themetheme.extend reads the token variables
→
Componentsbuilt on tokens, never ad-hoc values
  • Tokens → Tailwind. Export the Figma tokens to a design-tokens JSON, generate CSS custom properties (e.g. --color-primary, --space-4, --radius-md), and map them in tailwind.config under theme.extend so Tailwind utilities resolve to the design system's values.
  • Components. Rebuild the design system's components (buttons, inputs, selects, tabs, cards, dialogs, tables) as Angular standalone components styled with those Tailwind tokens — the studio's building blocks.
  • Light/dark & theming. Because tokens are CSS variables, theme switches are variable swaps, not restyles.
Tokens captured. The real Equity Global Tokens (colour, greyscale, typography, elevation) have been transcribed into the frontend's Tailwind @theme (src/styles.css) — see docs/DESIGN-TOKENS.md. Primary #A32A29, Open Sans (400/600), the 8-step size scale and 5-step line-height scale, and the elevation shadows are wired. To verify against Figma: the dark-mode primary tint, and the radius/spacing scales (not shown on the shared sheets — current radius values are provisional).

§12 Phased roadmap

Sequenced to front-load value and de-risk the hardest correctness piece (patch generation + preview parity) first.

Phase 0 Foundations

Config BFF skeleton on a dedicated branch of this repo (reuses FormEngine models), Git-host integration (read repo, list catalogue), the in-process preview/resolve (with the small resolver-core refactor, §6), and the golden parity test-vector suite. Read-only catalogue browser in the SPA. Unauthenticated, lower-env target only.

Phase 1 · MVP Patch existing countries

Base form viewer + Patch author (visual diff → RFC 6902) for schema & UI, channel overrides, live preview, validation/lint, bundle download, and a first-cut in-app read-only diff/preview review (J2, J3, J8). Delivers safe, self-service tweaks/extensions to existing countries — the most frequent need.

Phase 2 New countries + translations

Journey builder (steps, branches, conditions, exit rules, workflow config) + clone-country workspace + cross-field rule builder + the translation mapper (J1, J4, J5, J9). Delivers end-to-end new-country configuration including key coverage.

Phase 3 Authoring depth & polish

New base-form authoring, versioning flow, full-fidelity reviewer mode, translation-store hardening, compare-countries/find-usages (J6, J7). Then wire the deferred auth & roles into the prepared seam, and open higher-environment targets.

§13 Risks & mitigations

RiskImpactMitigation
Preview/validation drifts from backendAuthors trust a preview that's wrongOption A (reuse backend resolver) as source of truth; golden test vectors run against both engines in CI (§6).
Generated patches are brittle (pointer targets move)Patch fails to apply after a base changeRegenerate patches against the current base on open; lint dangling pointers; preview applies patches so failures surface immediately.
Idempotent-seeding "my change didn't show up"Confusion, duplicate effortVersion-aware guardrail (§9) + clear prod vs non-prod publish guidance (J7).
i18n keys authored but not translatedBlank/placeholder text in the live appMissing-key detection against the translation source (Phase 3); flag before publish.
Rebuild latency (embedded resources)Publish → live isn't instantSet expectations in-UI (change is "merged", live after next deploy/seed); revisit DB-backed runtime config later if instant publish becomes a requirement.
Concurrent edits to the same countryMerge conflicts / clobberingBranch-per-change + PR handles it via git; optionally soft-locks/"someone is editing" hints in the tool.
Widget added in config but not in the appBroken render for customersWidget-catalogue validation against the frontend's supported set (J6).
Open/unauthenticated tool (v1)Anyone on the network can draft config & open PRsRestrict to lower-environment targets only; PR-merge (human) remains the real gate to any environment; deploy behind internal network; keep the auth seam ready to switch on before any prod target.
Resolver-core refactor for draft previewRegressing production form resolutionExtract a pure patch-apply core called by both paths; cover with the golden test vectors + existing FormEngine tests before/after the refactor.
Translations drift from config (missing store)Keys authored with no home for valuesConfirm the translation store (§14) early; mapper blocks/warns on missing keys and ships values in the same PR.

§14 Decisions record & remaining input

Resolved#

QuestionDecision
Frontend stackAngular v22+, TypeScript, Tailwind CSS.
Design systemFigma Global Tokens / Visual Language — components & tokens, wired into the Tailwind theme (§11).
RepositoriesSplit: frontend in a separate repo (form-config-studio); .NET BFF on branch feature/form-config-studio of this repo.
Config BFF stack.NET, reusing backend models, on the dedicated branch of this repo.
i18n / translationsKeys + translation mapper; store is a single JSON file shaped { "key": { "en": "…", "fr": "…" } }.
Sample dataMaintain a small set of default per-form sample payloads for preview & the rule tester.
Preview approachOption A — reuse the real components in-process via the BFF.
Reviewer experienceBoth — Git PR review and in-app read-only diff/preview.
EnvironmentsLower environments only for v1; higher envs deferred.
Access modelOpen / unauthenticated for now; auth & roles deferred with a seam kept.

Remaining dependency (not a blocker to start)#

  1. Confirm two token values against Figma. The Equity Global Tokens are transcribed into src/styles.css; only the dark-mode primary tint and the radius/spacing scales (absent from the shared sheets) need confirming. Names are stable, so it's a values-only drop-in.
Next step (in progress): (1) scaffold the separate form-config-studio Angular 22 + Tailwind repo with the tokens/translations/sample-data structure; (2) prepare the feature/form-config-studio branch for the .NET BFF (resolver-core refactor + preview/resolve); (3) turn this plan into a concrete backlog starting with the Phase 1 MVP.