Backend source
The backend is one npm-workspaces repository written in strict TypeScript (ESM) on Node 22: a modular monolith with three processes built from one container image. Its design document is docs/architecture/BACKEND.md, and its developer guide is docs/DEVELOPMENT.md.
Layout
| Path | What |
|---|---|
openapi/digifir.yaml | The contract: every operation's request and response (230 operations). Validators and both SDK clients are generated from it. |
services/api | REST and server-sent events. Each module lives in src/modules/<module>/index.ts and implements contract operations by operationId. |
services/projector | Blocks into read models. Each handler lives in src/handlers/<module>/index.ts and applies its share of a block. |
services/worker | Jobs and the outbox. Each job lives in src/jobs/<job>/index.ts with its schedule and outbox topics. |
packages/* | Shared libraries, published inside the workspace as @digifir/<name> |
apps/console | The officials' desktop console, a package of its own |
sdk/ts, sdk/dart | The app SDKs; changed here only by regenerating their clients after a contract change |
deploy/helm | Helm charts of the backend and the console |
docs/ | Design, decisions, development, network, console and performance pages |
Packages
| Package | What it does |
|---|---|
canonical | RFC 8785 canonical JSON, NFC, SHA-256, ES256, the signed-action envelope; shared with the SDKs |
contract | Types generated from the contract, the operation table, Ajv validators |
errors | Problem codes, problem+json bodies, the apps' legacy error codes, chaincode errors |
config | The environment variables, checked once at start-up, with the pilot's guards |
db | Connection pools, the sql tag, transactions, migrations, the audit log writer |
ledger | The ledger interface, its fabric-gateway implementation, block and MSP verification, a recording mock for tests |
storage | Object names, presigned uploads and downloads, SHA-256 checks |
platform | Logging, OpenTelemetry, Redis, readiness, the internal server, process lifecycle |
actions | The signed-action relay: the action catalogue, routing and endorsers, submission lanes, the watchdog |
identity | Sessions, proofs, DIDs, the registry projection, officials |
service-keys, keystore, vault | The backend's service keys, its HSM (PKCS#11) or seed-derived keys, the identity vault's envelope encryption |
projection | Event channels and IDs, IST days, pages and record histories of the read side |
documents | Deterministic PDFs with Indic fonts and verification QR codes |
notify, sms-gateway, mail | Templates in ten languages, FCM, DLT SMS, email |
adapters | CCTNS, e-Sakshya, FSL LIMS, eCourts, ICJS, eSign, the call bridge, places; each with its simulator |
outbound | Outbound HTTP that never follows redirects and reads bounded bodies |
testing, smoke, e2e | The api under test, a contract-checking client, test databases; the smoke walk, network walk, end-to-end suite and load test |
How a request is handled
Every request to the api passes the same pipeline before its handler runs:
- A request ID is accepted or generated.
X-Digifir-Appis checked against the minimum app version.- The caller is authenticated by the operation's security scheme.
- Rate limits are charged, before the body is read.
- The body is read with the strict canonical parser on signed routes, within the route's size limit.
- The request is validated against the contract, then app attestation is checked where the operation takes it.
- The handler checks role and jurisdiction. Read-model queries always carry the visibility rule, so a list never shows what a detail would refuse.
- In tests and staging the response is validated against the contract too.
- Every error leaves as
application/problem+json.
Adding an operation
Add the operation to
openapi/digifir.yaml.Regenerate everything generated from it, in the same change:
shnpm run contract:generate # packages/contract npm run generate -w @digifir/sdk # sdk/ts dart run tools/codegen-dart/bin/generate.dart # sdk/dart npm run openapi:lintImplement it in the module that owns its tables:
tsimport { defineModule } from '../../modules.js'; export default defineModule({ name: 'directory', register(api) { api.operation('listStations', async ({ query }) => { const page = await findStations(api.deps.db, query); return { status: 200, body: page }; }, { rateLimit: { routeClass: 'lookups' }, cacheControl: 'public, max-age=300' }); }, });Update the operation count in the tests that hold the contract to its own rules, and record the change in one line of
docs/architecture/CHANGES.md.
Keep the SDKs backward compatible: add operations and optional members only, because the apps build against them.
NOTE
A module owns its tables: only its code writes them. Other modules call it only through the service interface it provides (api.services).
Checks
| Command | Checks |
|---|---|
npm run typecheck, npm run lint | Strict TypeScript and ESLint with type information |
npm test | Unit and integration tests with PostgreSQL, Redis and SeaweedFS; every endpoint's requests and answers held to the contract; every ledger call to a recording mock |
npm run smoke | The three processes over HTTP against a scripted Fabric peer |
npm run contract:check, npm run openapi:lint, npm run testvectors:check | Generated types up to date, the contract's lint, the canonical-encoding vectors shared with the chaincode |
npm test -w @digifir/sdk, dart test in sdk/dart | The two SDKs |
npm run smoke:network, npm run e2e:network, npm run load:network | The network walk, the end-to-end scenarios and the load test on the development network |
CI runs npm audit --omit=dev, typecheck, lint, unit and contract tests, the OpenAPI lint, the test vectors, integration tests, the smoke walk and the Dart SDK on every change.
Sources: digifir-backend README.md, docs/DEVELOPMENT.md, docs/architecture/BACKEND.md sections 1.1 to 1.4.