Structured error codes
ADR-2026-0001: Structured error codes
Section titled “ADR-2026-0001: Structured error codes”Context and problem statement
Section titled “Context and problem statement”Citizenry exposes HTTP APIs (identity, vault, planned salon) that third-party agents and internal services consume. Errors from these surfaces need to be:
- Machine-stable - clients dispatch on a short identifier.
- Self-describing enough to be grep-able in logs and to hint at the responsible subsystem without a lookup table.
- Catalogued - every emitted code resolves to a documented page so a debugging caller can land on an authoritative explanation.
Earlier the codebase used an ad-hoc ERR-P01-S##-NNNN scheme with numeric
service codes (S01..S05). The numeric service slot proved opaque: developers
had to memorise that S03 is vault, and grepping a log line for “vault”
returned no hits. Meanwhile no public catalog existed, so the code string
in responses was a dead-end for callers.
Decision drivers
Section titled “Decision drivers”- Multi-product workspace. citizenry-id is one of several products (planned: endue-salon, endue-ai); codes must namespace by product without collisions.
- HTTP-first surface. The chosen scheme should plug into RFC 9457 Problem Details without invention.
- Open source consumption. External RPs may not have access to our internal numbering registries; codes should self-document.
- Lint-enforceable. A regex and a code↔HTTP-status table must be expressible so CI can fail bad PRs automatically.
- Append-only. Codes are part of the public API contract; once issued, they cannot be re-meant.
Considered options
Section titled “Considered options”- All-numeric tokens (
ERR-P01-S03-3001) - status quo. Stable but opaque; service slot is unreadable without a side table. - All-slug tokens (
ERR-CTZ-VLT-3001) - most readable; requires stabilising product slugs early. - Hybrid: numeric product + slug service (
ERR-P01-VLT-3001) - readable service slot, stable product numeric (products are few and slow to add). - Stripe-style slug only (
vault_not_found) - abandons hierarchy; weak for a multi-service monorepo.
Pros and cons
Section titled “Pros and cons”| All-numeric | All-slug | Hybrid (chosen) | Stripe-style | |
|---|---|---|---|---|
| Grep readability | ✗ | ✓ | ✓ | ✓ |
| Product namespacing stability | ✓ | ✓ (if regulated) | ✓ | ✗ |
| Migration cost from current | 0 | high | low | high |
| Regex enforceability | ✓ | ✓ | ✓ | weak |
| Self-locates the subsystem | ✗ | ✓ | ✓ | ✗ |
Decision
Section titled “Decision”We adopt the hybrid scheme:
ERR-P##-XXX-NNNNP##- 2-digit numeric product (P01citizenry-id,P02endue-salon, …).XXX- 3–4-letter uppercase service slug (IDT,VLT,MAIL,WLT,TNT,AUTH,SLN, …).NNNN- 4-digit sequence; thousands digit encodes a fixed category whose allowed HTTP status set is enforced by CI.
The full registry, category table, and procedural rules live in
docs/reference/error-codes/guideline.md.
Every code emitted by the server MUST have a Markdown file under
docs/reference/error-codes/{code}.md derived from
templates/error-code.md. The
/docs create error-code skill scaffolds it
from the template.
The error envelope is RFC 9457 Problem Details with a type URI of
https://citizenry.id/errors/{code}. This page becomes the public catalog.
Consequences
Section titled “Consequences”Positive
Section titled “Positive”- Each code points at a documented page; external integrators can resolve errors without internal knowledge.
- Service slot is readable in logs and tracebacks (
-IDT-,-VLT-,-SLN-). - CI can fail PRs that introduce undocumented codes or mismatched HTTP statuses.
- TypeSpec → OpenAPI surface gains structured
typeURIs.
Negative / cost
Section titled “Negative / cost”- One-time migration: existing
S##codes are deprecated and dual-emitted during a transition window. - Slug registry needs governance - a typo’d slug at PR time is permanent unless caught in review.
Neutral
Section titled “Neutral”- Numeric product code retains opacity (
P01does not say “citizenry-id”), but products are few and stable, so the cost is small.
Implementation
Section titled “Implementation”- PR-1 - Land
docs/reference/error-codes/guideline.md,templates/error-code.md,templates/adr.md, this ADR, andCODE_OF_CONDUCT.md. (this change) - PR-2 - Update
packages/spec/common/errors.tspBaseErrorto add the RFC 9457typefield. Adjust response builders to filltypefrom${ISSUER_HOST}/errors/${code}. - PR-3 - Introduce
packages/spec/common/error-catalog.tspas the single source of truth. Backfill currently-thrown codes. - PR-4 - Add
packages/spec/scripts/lint-errors.tsand a root-levellint:errorspnpm script; wire into CI. - PR-5 - Migrate raise-sites from
ERR-P01-S##-NNNNtoERR-P01-XXX-NNNN; dual-emit during one release cycle via catalog aliases. - PR-6 - Publish the docs site (
apps/webroute/errors/[code]) consuming the catalog.
Compliance / follow-up
Section titled “Compliance / follow-up”- A subsequent ADR will lock down the slug registry process (who can mint a slug, how a clash is resolved).
- Re-evaluate after one release cycle: are deprecated
S##codes still observed in client traffic? If yes, extend the alias window.
References
Section titled “References”- RFC 9457 - Problem Details for HTTP APIs
- RFC 7807 - predecessor
- Alibaba Cloud error code conventions (slug-based
Service.Module.ErrorName). - SAP T100 message class model (numeric MSGID + MSGNO).