Appearance
Roadmap
Aletheia is self-deployable risk management infrastructure for payment companies, marketplaces and fintechs. This page says what the platform does today and what has been deliberately left for later. It is generated: the "Deferred, by theme" section below is the project's register of postponed work rendered for the site, so the two never disagree.
What works today
Identity and tenancy. Zitadel provides identity, with one organisation per tenant and project roles mapped to API permissions; Postgres row-level security sits under every repository. Applicants never sign in: they open a signed, expiring link. See Concepts.
Rules and workflows. Six rule types (comparison, list lookup, score threshold, CEL expression, velocity over ingested events, and app calls), grouped into rule sets with aggregation policies and score bands; a draft rule can be dry-run and backtested against recorded evaluations before it is published. Workflows run on Temporal with idempotent activities and steps that evaluate rules, call apps, branch, wait for a collection, open cases and emit decisions. See Rules.
Vendors and documents. Vendors are apps: WebAssembly modules a tenant installs with its own configuration and secrets, run by a separate app runner in a sandbox that holds one tenant's data for one call and reaches only the hosts the install approved. Calls have per-action timeouts, retries and idempotency keys, a task queue of their own, circuits, asynchronous callbacks and signed webhook ingestion; tenants upload and publish their own apps beside the platform's catalogue, where OpenSanctions screening and Sumsub document verification are the reference vendors, next to deterministic mocks. Documents upload straight to S3-compatible object storage, where their type and size are checked, images are re-encoded and PDFs with active content are refused. See Apps, the App catalogue and Documents.
The collection terminal. The applicant app (apps/collection-terminal) opens a tenant's collection flow from a link, hosted or embedded in a merchant's page through an iframe loader: a welcome screen, steps with conditional fields and branches, saved answers, document uploads, a review before the submit and a screen that follows the decision. See Collection flows and embedding.
The admin console. The staff app (apps/admin-console): case queues with claiming, SLAs, notes and attachments; subjects with a timeline and decision history; runs; editors for rules, rule sets, lists, workflows and collection flows with validation, version diffs and four-eyes approval before publish; apps with their installs, versions, approvals and health; the audit trail with digested exports; tenant settings. See Admin console, Admin console: Define and Audit.
Running it. OpenTelemetry traces, metrics and correlated logs with dashboards and alert runbooks; container images on GHCR, a one-VM compose stack and a Helm chart with migrations in the pipeline; Changesets for the published packages and tagged platform releases; replay tests over recorded workflow histories and a rule-evaluation benchmark. See Deployment, Operations, Releasing and Performance.
Building on it. This site, with a guided path from an empty machine to a first decision (Get started, the tutorial and the pilot runbook); an app authoring guide written against the published SDK, with testing helpers that run the built module through the real host, a scaffold and a standalone example; and six example policy packs that load through the API or a CLI.
How the register works
The register is docs/plans/deferred.md. An item is added whenever a change leaves known work for later and removed when it ships. Each item says what exists today, what is missing and, where there is one, what would trigger the work. Items that code comments refer to carry a short id: B1 to K for admin console features that wait on the API, and API-n for API additions the collection terminal waits on. CI fails when this page and the register disagree (pnpm roadmap:check).
Deferred, by theme
Everything below is a decision to postpone work, recorded in the register of postponed work together with what would trigger picking the item up. These are statements of intent, not deadlines: the project has no dates, and an item ships when its trigger arrives.
Identity, tenancy and authorization
- SpiceDB authorization. The API authorizes through the
Authorizerinterface (packages/auth/src/authorizer.ts), implemented over Zitadel project roles. Object-level rules (deciding a case, publishing a definition, approving a change, working a queue) need a SpiceDB implementation behind it. Intended schema: atenantwithadmin,analystandintegrationrelations (managefor admins,viewfor admins and analysts); acasewith its tenant and anassignee(viewthrough the tenant,decidefor the assignee or a manager); a definition set with apublisher(publishfor the publisher or a manager). Role relationships would be written withTOUCHfrom a token's roles claim when it is verified, so nothing has to sync Zitadel into SpiceDB. The same object rules would give analysts a narrower run scope (only the runs behind the cases in the queues they see), which the handoff'sanalystRunsScopeasks for. - Zitadel Login V2. The stack uses Zitadel's built-in login everywhere. Revisit if branding or passkey flows need the newer login UI.
- Multi-organisation users. One Zitadel session per organisation; switching means signing out and in. A session-level organisation switcher is a console follow-up.
- Tenant self-service. Tenants are provisioned by operators (
pnpm tenant:add). An admin UI or instance-level API for provisioning is pending a notion of instance admins. - Private-key JWT for machine callers. Service users use personal access tokens via introspection in development and CI; production guidance should prefer private-key JWT.
- Local user mirror. The reviewer directory (with a 60 s cache) and Settings › Team read Zitadel live; a person's last activity comes from the audit log. Mirror users into a local table when SpiceDB relations or offline reporting need them.
- Token revocation. The API verifies access tokens (JWTs) locally until they expire, so a console sign-out or idle lock does not invalidate a token already issued. Short token lifetimes or a revocation check would close the gap.
Rules and workflows
- jsonata and JSON Logic expression languages. CEL is the only language. jsonata would run with guardrails inside a worker thread; JSON Logic would back a visual condition editor.
- Tenant-configurable velocity keys. Velocity rules group by subject and four promoted columns (ip, device_id, card_fingerprint, email). Other keys need a secondary key table and a configuration UI.
- Automatic workflow triggers. A workflow definition records a
trigger(manual,subject.createdorevent), but nothing acts on it: every run starts throughPOST /workflow-runsor as a replay, and ingested events are rule inputs only. Starting a run when a subject is created, or a monitoring run when events of chosen types arrive, is the next step; such runs would record their own runtriggerbesideapi,consoleandreplay. - Workflow simulation. The workflow editor validates structure only; dry-running a workflow against sample data is not built.
- Retiring a definition. A version is archived only when a newer one is published; no route retires a key.
GET /definitions/usageis the check such a route would make first. - Rolling aggregates. Velocity is computed on the fly over indexed ranges. Time-bucketed counters or TimescaleDB continuous aggregates when per-key windows grow large or distinct counts over long windows get slow.
- Events partitioning and retention. The events and rule-evaluations tables are created unpartitioned with partition-ready primary keys; monthly range partitions (pg_partman) and retention jobs are an operations step once volume demands it.
- List versioning. Lists are live data, so backtests use current list contents rather than the contents at evaluation time.
- Backtest backfill. Backtests replay stored evaluation snapshots only; there is no backfill from earlier decisions.
- CSV import over 5 MB. Larger imports should stream through Postgres
COPYinto a staging table. - Backtest history paging.
GET /backtestshas noruleKeyfilter, so the admin console's "previous backtests" for a rule pages through the tenant's whole list and filters client-side; add the query parameter when histories grow. - Backtest progress precision. Progress is informational: a retried batch can be counted twice (capped at the total). The summary travels with the workflow and is exact.
- CEL budget is cooperative. The 50 ms expression budget races a timer against evaluation and only interrupts at an await, so a purely synchronous comprehension runs to completion (the benchmark measured 140 ms and 500 ms cases). Production list lookups do I/O, so the budget holds there; a hard limit needs a worker thread or an interpreter with a step counter.
Apps and vendors
- Blocking a module without a restart. Installs, their configuration and their secrets are read on every call, but the operator's blocklist (
APP_BLOCKED_SHA256) is read when the API and the workers start, so blocking a module everywhere takes a restart of both. A blocklist in the database with a shared invalidation channel (database notify or a Temporal signal) would block it at once; the trigger is the first module an operator has to stop in a hurry. - Shared circuits and fallbacks. Each worker process keeps its own app circuits (open after five failures in a row, a trial call every 30 seconds), so replicas open them separately and the thresholds are fixed; a circuit cannot be reset by hand, and no fallback app takes over when one fails.
- Self-hosted yente for screening. Screening uses the hosted OpenSanctions API. yente plus Elasticsearch (about 5 GB of memory) can be added as a compose profile when a deployment needs on-premise screening or the commercial bulk-data licence.
- Other document-verification vendors. Sumsub is the real adapter; Veriff, Persona and Onfido/Entrust apps follow the same asynchronous app contract when a customer needs them.
- Re-screening. The
opensanctionsapp shipsscreenBatch(up to 100 queries in one request) for periodic re-screening of existing subjects; the admin console action and the scheduled job that would use it are not built yet. - Callback row cleanup. Completed and timed-out
app_callbacksrows and theapp_webhook_eventskept for de-duplication are kept forever; add retention when the tables grow past what the indexes handle cheaply. - Funnel event cleanup.
submission_eventsrows are kept forever, though the drop-off reads 90 days at most; a sweep can drop older ones once the table grows. - Sumsub sandbox test in CI.
catalog/sumsubis tested against a local vendor stub only, so the request signing, the multipart upload, the webhook digest and the hand-off are checked against the Sumsub sandbox by hand. A test gated onSUMSUB_APP_TOKENandSUMSUB_SECRET_KEY, run by the local CI when they are set, would close the gap;catalog/opensanctionsandOPENSANCTIONS_API_KEYare in the same position. - Platform apps published without a release. Platform apps come only from the release's catalogue, which the API publishes when it starts. An operator command or route that publishes a platform version would ship a vendor fix without a platform release; worth it once vendor changes outpace releases.
- Automatic upgrades of platform apps. A release that brings a newer version of an installed platform app leaves the install on its version, marked
updateAvailable, until an administrator moves it. An install that opts into minor upgrades (new hosts and new secrets still waiting for an administrator) would keep tenants current; the trigger is tenants that run many apps. - A host-side cache for apps. Nothing outlives a call, so an app that needs a vendor token (an OAuth access token, a session) fetches it again on every call. A cache per install held by the host, with an expiry and the same redaction, would keep it between calls; the trigger is a vendor whose token endpoint is slow or rate-limited.
- Per-tenant app quotas. Fairness is a limit on calls in flight per runner process and per tenant (
APP_RUNNER_CONCURRENCY,APP_RUNNER_TENANT_CONCURRENCY); nothing caps a tenant's or an app's calls, HTTP requests or vendor spend over time. Quotas and their metering are needed before tenants share a runner pool under a commercial agreement. - Module signing and provenance. A module is named by its SHA-256 and checked against it wherever it travels, but nothing says who built it or from what source: uploads and the catalogue carry no signature or build attestation. Signing and a provenance check at upload would let an operator trust a module's origin, not only its integrity; the trigger is tenants installing apps from third parties.
- Remote apps over HTTP. Every app is a WebAssembly module the platform's runner executes. An app served as a vendor's or a tenant's own HTTP service, called with the same envelopes, would admit integrations that do not compile to WebAssembly (native dependencies, long sessions); it waits for one that needs it.
- Runner on wasmtime. The runner uses Extism's JavaScript SDK on Node, whose worker mode (what enforces a call's deadline) is marked experimental. A runner on Extism's wasmtime-based runtime, behind the same
/v1routes and host interface, would take that mode out of the path; the trigger is a release of the JavaScript SDK that breaks the deadline, the memory cap or asynchronous host functions. - An operator catalogue of tenant apps. An operator sees tenants' uploads only by their hashes (
APP_BLOCKED_SHA256) and through each tenant's own routes: there is no instance-wide list of the modules tenants uploaded, the hosts they call and where they are installed. An operator view, and a review before a tenant's version is published, wait for a notion of instance administrators, like tenant self-service.
Documents and storage
- Virus scanning. Documents pass through a no-op scanner (
DOCUMENT_SCANNER=none). Add the ClamAV implementation (clamdINSTREAMover TCP, 3 GiB container, EICAR test) before production use. - HEIC uploads.
image/heicis not an accepted document type: images are re-encoded on ingest to strip metadata and polyglot payloads, and the prebuilt libvips bundled withsharpcannot decode HEVC. Accept HEIC once a libheif-enabled build (or a conversion service) is in the stack. Whether phones convert HEIC photos to JPEG when a browser's file input picks them is not checked. - Deep PDF inspection.
inspectPdfis a byte scan for active-content names; names hidden by#xxescapes or inside compressed object streams are not detected. A real parser (or rasterising PDFs to images) closes that gap when documents carry real risk. The scan also refuses/OpenActionand/AA, which ordinary PDFs use to set their opening view; how often that turns legitimate files away is not measured. - Object storage versioning and object lock. Garage has neither; production evidence retention should target S3 with versioning and Object Lock through the storage interface.
Case work
- Email and chat notifications. Notifications (an assignment, a breached SLA, an approval request, a failing app or webhook) appear in the console's bell and as webhooks; nothing is emailed or posted to chat, and nobody can mute a kind. A per-person channel would hang off the worker's dispatcher (
apps/worker/src/outbound). - Auto-assignment and queue ownership. Queues are saved filters and a run can give its case a routing label they select on; assigning cases to people by rule, and queues that own their cases, can be layered on
assigneeIdlater. - Case reopen. A closed case cannot be reopened.
- Mentions and rich notes. Case notes are plain text of up to 10 000 characters, without mentions of colleagues or formatting.
- Keyboard-driven review. The workbench and case page have no shortcuts to approve, reject or open the next case.
- Bulk actions that fail part-way.
POST /cases/bulkapplies ids one at a time and lists the cases that cannot change, but an unexpected error answers 500 after earlier ids were applied, so the caller cannot tell which were. It should report every id.
Definitions and policy packs
- Policy pack import screen. The console's import dialog takes an uploaded or pasted pack, lists problems as flat
path: messagelines and shows the created versions only after the import. It should show the API's per-item report and, before anything is written, the versions an import would create, which needs a validate-only mode onPOST /definitions/import. - Transactional pack import.
POST /definitions/importvalidates every item before writing and then writes sequentially, because the route layer has no handle to open one transaction across repositories. A unit-of-work dependency onAppDeps(one scoped transaction passed to everycreateVersion,upsertandpublish) would make a part-way failure leave nothing behind; today it leaves inert drafts. - Pack export with dependencies.
GET /definitions/export?keys=exports exactly the definitions named: a workflow comes without the rule sets, rules, flows and lists it depends on, so the pack fails to import elsewhere unless every dependency is listed by hand. Aclosure=trueparameter (and--closureon the CLI) would add each dependency once, in a stable order. - Flow checks on save and publish. Saving or publishing a collection flow applies only the core schema (unique step ids, existing entry and targets, field shapes); the validator behind
POST /collection-flows/validateis advisory. A published flow can therefore hold duplicate field keys, a select without options or a branch cycle, which leave applicants unable to finish.
Audit
- Audit hash chain and retention. Exports ship with digests; a per-tenant hash chain over audit rows (tamper evidence) and retention policies come later.
- Scheduled export destinations. Scheduled audit exports write to the deployment's object storage, in UTC, buffering each file in the worker (256 MiB at most). A tenant's own bucket (with a credential reference), streaming multipart uploads for larger files and schedules in a tenant time zone are not built.
- Settings changes in the audit trail.
tenant.settings.updatedrecords the embed origins only; changes to the SLA and approval settings leave no detail in the trail.
Security and privacy
- Per-tenant token secrets. One
SUBMISSION_TOKEN_SECRETsigns every tenant's collection links. An admin revokes a tenant's links by moving itslinksValidAfter; rotating the secret itself still invalidates every tenant's links. - Free text is not masked. Responses are redacted for the caller (personal data without
subjects:pii, run context and app payloads withoutruns:context), but free text comes back as stored: a list-lookup rule's reason quotes the value it found (an email on a blocklist, say), and decision messages, notes, approval comments, file names and rejection reasons are unmasked. 'unsafe-eval'in the console's CSP. The console's schema forms validate with Ajv, which compiles each JSON Schema at run time, so its Content-Security-Policy allows'unsafe-eval'(scripts still load from its own origin only). Precompiled or CSP-safe validators would remove it.- The console's
/apiproxy is open-ended. nginx forwards every API route and method except/api/docs, inbound vendor webhooks and applicant routes included, and the dev server forwards everything; an allow-list of what the console calls would narrow it. - Search terms in logs. The API logs request URLs with their query strings (
apps/api/src/app.ts), which carry search terms such as names, and nginx's error log writes the full request line on upstream errors; the access logs of both apps already keep paths only. - Gates that differ between console and API. The audit event sheet shows request metadata to admins only but copies and prints the whole payload.
- Mutual TLS to the runner. The API, the worker and the app runner authenticate each other with one shared bearer token (
APP_RUNNER_TOKEN) over the cluster network, and the chart's NetworkPolicy limits who reaches the runner. Mutual TLS between them is left to a service mesh; certificates of their own would matter on a deployment without one. - An external secret manager for the store. App secrets and outbound webhook signing secrets are sealed in Postgres under one deployment key (
SECRET_STORE_KEY). Keeping them in a secret manager (Vault, a cloud KMS), or wrapping the key with one, would add rotation without re-sealing and an audit of their use outside the database; the trigger is a deployment whose policy forbids keys held by the application. - Local origins on production Zitadel applications. The Zitadel seed turns on development mode for any application with an
http://origin, so addinghttp://localhost:5176to a productionadmin-consoleapplication, to let a local console sign in, puts that application in development mode; use a separate one.
Operations
- Deploying without the repository. The repository is private, but parts of a deployment still come from a checkout of it: the compose stack (
deploy/compose: the compose file, the Caddyfile, the env example and the Postgres init scripts), the chart's README (deploy/helm/README.md, outside the packaged chart), the Grafana dashboards and alert rules (deploy/grafana), the Zitadel seed (pnpm --filter @aletheia-dev/auth seed) and tenant provisioning (pnpm tenant:add). Each needs a published home (a deploy bundle beside the images, the chart itself, or a command in an image) before a customer deploys on their own. - Error-biased sampling. Traces are head-sampled (parent-based ratio); errors are not force-kept. Tail sampling in the collector (keep every trace with an error span) is the production answer and needs a collector pipeline, which the LGTM dev image does not model.
- Benchmark regression comparison. The benchmark gates on fixed p95 budgets and keeps its results per commit; comparing against the previous run and failing on relative regression waits for a stable reference machine.
- Load test on the reference VM. The numbers in
docs/performance.mdcome from a laptop running the compose stack, where Postgres and the Temporal server saturate first: API latency met its target (p95 under 300 ms at 50 virtual users), but 14 % of runs outlived their 30 s budget. Re-measure on the reference VM before quoting the numbers as sizing guidance. - In-cluster dependency subcharts. The Helm chart assumes external Postgres, Temporal, Zitadel and object storage; optional subcharts for a self-contained dev cluster can follow.
- Managed service recipes. Temporal Cloud and managed Postgres (RDS, Cloud SQL) values and notes are not written yet.
- Log-based alerting and exemplars. Alerts come from metrics only; Loki alert rules and exemplar links from histograms to traces are not configured.
- Worker autoscaling on queue depth. The HPA on Temporal task-slot usage is documented but off; scaling on task-queue backlog needs the Prometheus adapter and Temporal's backlog metrics.
- Package builds before TypeScript 7. tsup's declaration build sets
baseUrl, which TypeScript 6 deprecates, sotsconfig.base.jsoncarries"ignoreDeprecations": "6.0". TypeScript 7 drops the option: the packages need a declaration build without it (tsdown, ortscemitting declarations) first, and the setting goes with tsup. - Blue/green chart upgrades. Upgrades are rolling with a migration hook; a blue/green or canary strategy for incompatible interpreter changes is not provided.
- API address resolved once in the web image. nginx resolves the API host when it starts, so in compose a recreated API container is unreachable from both apps until the web container restarts; a
resolverwith a variable upstream would re-resolve it. - Outbound webhook metrics and retention. Deliveries and notifications have no metrics or alert rule (a given-up delivery is a log line, a notification and a Home item), and
webhook_deliveries,notificationsand their read marks are kept forever; delivery counters by outcome and a retention job follow once tenants rely on webhooks. - Webhook delivery throughput. Each worker attempts eight deliveries at a time and one worker dispatches up to 200 events every two seconds; a tenant with a slow receiver delays the others' deliveries on that worker. Per-endpoint concurrency and fairness wait for real volumes.
- Index builds that block writes. Migration
0015creates its list indexes withoutCONCURRENTLY, blocking writes on the tables it indexes (workflow runs among them) while it runs, and the migration lint does not flag such builds; on a deployment with data, apply it in a quiet window.
Docs
- Versioned docs. The site documents
mainonly. Per-release snapshots of the docs come with 1.0, when the API and SDK stop changing between minors. - Translated docs. English only; VitePress supports locales once customers need another language and someone commits to keeping it current.
- Hosted search. Search is VitePress's built-in local index, built at deploy time; a hosted service (Algolia DocSearch or similar) is worth it once the site outgrows the client-side index.
example-appCI job goes strict.examples/apps/acme-screeningis built and tested against@aletheia-dev/app-sdk,@aletheia-dev/app-hostand@aletheia-dev/corepacked from the workspace (scripts/ci-local/example-app.sh), installed over its lockfile, because the SDK's first release is not on npm yet. Once it is, refresh the example'spackage-lock.jsonand letnpm ciinstall the published SDK, so a release that breaks external authors fails CI.create-aletheia-appnpm initializer.pnpm app:new(scripts/app-new.mjs) scaffolds an app fromtemplates/app, in-repo or standalone, but only from a checkout, which app authors outside the project do not have; they follow the authoring tutorial instead. Annpm create aletheia-apppackage gives them the scaffold.- Platform release notes on the docs site. Each platform release's notes (
scripts/release-notes.mjs: features, fixes, migrations, workflow compatibility) go to its GitHub release, which the private repository keeps from customers; the changelog page says so. Rendering them into the site when it is published, from the releases, needs no commit tomain; worth it before customers upgrade between releases. - Screenshots and recorded walkthroughs in the tutorial.
docs/start/first-decision.mddescribes the console and terminal screens in words beside each API call. Screenshots and a recording wait until the UIs settle, since every layout change would make them wrong.
Admin console
- Walk-through on a real deployment. The console is tested against mocks only: sign-in and its return path, the
/apiproxy with a real token, uploads to object storage (an attachment on a case), the schema forms under the CSP and every write path have not run end to end. Deploy the current code, sign in as an admin and as an analyst and walk every screen; check the inline PDF preview in Safari and Firefox (Chrome works). Then revisit keeping Settings out of the analyst navigation (queue managers reach Settings › Queues from the workbench). - Expired sessions. A 401 on a load sends the user to sign-in, losing open dialogs, and a 401 on a save, claim, decide or approve shows a generic error toast; neither tries one silent token refresh first, and there is no session-expired card. Toasts for 5xx errors offer no Retry, and the 403 page names neither the refused API route nor where to ask for access.
- Lists cut off without saying so. A case shows at most 50 documents and 50 vendor checks, a run 50 invocations, 50 callbacks and 50 cases, and an approval card's backtest reads the last 50 backtests, none with a "more" marker.
- Request load. The definition lists fetch pending versions with one request per row, and the runs page resolves workflows the same way, uncapped and with failures swallowed; the runs page reloads every 5 s without waiting for or cancelling the previous request; Home makes about 18 requests on an admin's first load, some of them twice; and every mount refetches, since there is no shared query cache.
- Editor chunks. The Define area imports the schema forms (rjsf and Ajv, 119 kB gzipped) and CodeMirror (137 kB gzipped) statically, so the rules list loads both before an editor opens.
- Audit export in memory. The export download holds the file three times (the chunks, the decoded text and a Blob) while it hashes it; it should stream and hash incrementally.
- SLA overrides and drafts. Settings › SLA reads each workflow's latest version, so a workflow with a newer draft drops out of the per-workflow overrides.
- Accessibility. Accent text (#ec3013) is below 4.5:1 on the console's backgrounds (3.5 to 4.2:1) on the active navigation item, ghost buttons, outline tags, card kickers, plain links and the link buttons of several areas; the darker accent (#ae1800) reaches 6.4:1. Reasons for disabled controls are only in
title; below 1280 px the health indicator is a colored dot; no table header is sortable (aria-sort; cases sort through a select); a run's steps grid is ARIA roles ondivs; fourrole="menu"lists handle Escape but no arrow keys; and the workflow canvas has not been tried with a keyboard. - Tests and CI. Sign-in (
AuthProvider,SignInGate), 401 recovery,hooks.tsand overlays have no tests. The database suites for runs, case counts, claim-next, bulk actions, audit by id and app health skip in CI, whose test job sets noDATABASE_URL, and the smoke scripts call none of those routes and never load the console. The console's nginx config and headers are checked only byhelm test, in a job that runs whendeploy/or theDockerfilechange. - Build id. The console shows no version or build id.
- Duplicated code. The flow preview's sample check (
src/areas/flows/sampleCheck.ts) restates the validation of@aletheia-dev/collection-flow, which a lint rule keeps out of the console, so the two can drift. The_newcreate path is defined in the palette and insrc/areas/define/governance.ts, and editor styles are copied intosrc/areas/define/define.css. - Field properties and the live preview. Fields accept a
unitand authormessages, which the terminal renders, but the field editor has no inputs for them, and saving a flow in the console drops both (cleanFieldinsrc/areas/flows/flowModel.ts). The live preview renders the shared@aletheia-dev/collection-flow-uicomponents in the console's look, counts every step and ignores branches; it should render the terminal's components with the tenant's theme, at phone and desktop sizes, so authors see what applicants see. - Language and week start. The console is in English and formats dates in the browser's locale; a workspace language and first day of the week wait on a message catalogue like the terminal's.
- Search inside names.
GET /searchmatches subject names, legal names and external ids by prefix, which the expression indexes cover; finding a word inside a name needs trigram indexes (pg_trgm). Results carry no highlight ranges: the palette marks the typed text itself. - Screens the API already supports. Not built yet: a bundled starter pack and a tutorial link on Home; optimistic claim and assign, and the open case's row highlighted in the workbench; a 90-day window on a subject's risk history and "widen range" in filtered empty states; "was x in vN" hints beside changed fields, published and draft versions in one cell on the workflow and flow lists, and dragging step types and fields; on Connect, a health filter and a 24-hour window, a failures-only view, an invocation's raw request and response (
GET /app-invocations/{id}) and a status filter on callbacks; related records by name in the audit event sheet and a File column on past exports.
Collection terminal
- Previews with sample data. A flow preview link (
/preview) opens the version empty: the sample answers typed in the console's live preview do not travel with it. - Walk-through on a real deployment. The web image serves
apps/collection-terminalon port 5177 behind thecollecthost, with the/apiproxy, a paths-only access log, a Content-Security-Policy, 404s for unknown addresses and deployment-wide branding from theCOLLECTION_TERMINAL_*settings. It is tested against the mock API and inside the web image, not yet end to end: a link from a run, uploads to object storage, the submit resuming the run, and the frame on a merchant's page. - Bundle size and request waterfall. The terminal ships about 136 kB of gzipped JavaScript (a 97 kB entry chunk with React and Zod, then a 39 kB app chunk) against a target of 90 kB; CSS (7 kB) and the loader (under 1 kB) meet their targets of 15 kB and 2 kB. Getting there needs package work rather than app work: a
zod/minibuild of@aletheia-dev/collection-flow's validation, or core schemas that tree-shake (pure annotations or per-module output). The first step also waits on two dependent requests after the entry script (the app chunk, then the submission with the tenant's look) against a target of two. - Performance on a phone. Not measured. The target, under Lighthouse mobile throttling (slow 4G, 4× CPU slowdown): the first step interactive within 3.5 s, and at the 75th percentile LCP ≤ 2.5 s, INP ≤ 200 ms and CLS ≤ 0.1.
- Accessibility checks. The terminal is built for WCAG 2.2 AA but has no automated check (no axe in its tests) and no manual pass with VoiceOver on iOS and macOS, TalkBack and NVDA, hosted and embedded, light and dark, at 320 px and at 400 % zoom. Page titles name the step and the tenant but not the position ("step 2 of 3").
- Browser targets.
vite.config.tssets nobuild.target, so the build follows Vite's default (Chrome and Edge 111, Firefox 114, Safari 16.4). Set the supported browsers explicitly (including Samsung Internet and the in-app browsers of mail and chat apps) and try them. - Unsaved answers. Answers save shortly after each change and on Continue, but not when a field loses focus or the page is hidden (
pagehide), and closing a hosted page with unsaved answers gives no warning (beforeunload). - After the submit. "You can close this page" is missing on the processing, approved, not approved and failed screens (the first screen says it updates by itself); the support contact shows only on the review, not approved and already-sent screens; the compact screen inside a frame has neither. Status polling stays at a fixed 2 s for up to 10 minutes and should back off after the first minute. No "Done" action clears the token from the tab (
forgetTokeninsrc/token.tsis unused). - Link and error states. Load failures are mapped by HTTP status rather than by error code: a missing submission (404) shares the "link not valid" screen, and the server-error and offline screens offer no support contact (the hosted header's Help aside). The
aletheia:errorposted on a load failure carries no request id; the app keeps one for 5xx errors only. - Poor networks. Saves retry with back-off, but reads are not retried automatically, and the upload to object storage (an XHR in
packages/api-client/src/documents.ts) has no timeout. - Broken flow definitions. A flow the validator would reject renders without crashing, but the applicant is stuck: a select without options keeps Continue blocked, and a branch cycle comes back from the submit as a 400 without issues, shown as "Check this answer". Neither gives a reference to quote to support or a way on (see "Flow checks on save and publish").
- Embedded frames. The origin check runs only with
embed=1: framed without it, the page renders as hosted, guarded only by the deployment-wideframe-ancestors. The loader hears about step changes but does not scroll the frame's top into view, and the embedding page hears nothing about upload outcomes. - Content-Security-Policy. The terminal's policy (rendered by
deploy/web/entrypoint.sh) allowsform-action 'self'where'none'would do, and falls back toconnect-src https:when no storage origins are configured. - The terminal's own words in other languages. Flows come in the tenant's languages, but the terminal's own words (buttons, notices, step counts, the welcome screen's lists, error messages from the engine's defaults) stay English and dates and numbers are formatted for
en-US, so a French form reads "Continue", and on a right-to-left page an English sentence ends with its full stop at the wrong end. Copy lives in per-modulecopy.tsfiles with hand-written plurals and a few inline strings. Needed: a message catalogue with plural rules and locale-aware formatting, and its translations for the languages tenants offer. - Theme tokens. Colors, radii, fonts and borders are tokens a tenant's branding overrides at run time, but spacing (about 70 values) and font sizes are literals, so density cannot be themed, and one white is hard-coded (
src/ui/ui.css). - Client error reporting. None. An optional reporter, off unless a deployment enables it, would send errors with their request ids and no personal data.
- Web Component embed. The iframe loader is the only way to embed; a Web Component built from the same Vite library build can follow if merchants need inline UX.