Backend Architecture
The API (apps/api) is a NestJS 11 application organized into feature modules under src/modules/. It owns all access to PostgreSQL (via TypeORM) and to the blockchain (via CosmJS).
Module layout
Each module follows a consistent layout. Service classes always live in a services/ folder, one use case per file (e.g. services/create-token.service.ts). The module root holds wiring and cross-cutting declarations only:
src/modules/<module>/
├── <module>.module.ts # NestJS module wiring
├── <module>.controller.ts # HTTP controller(s)
├── <module>.errors.ts # error constants
├── services/ # one injectable use case per file
├── dtos/ # wire shapes (responses)
├── requests/ # request DTOs (validated input)
├── entities/ # TypeORM entities
├── mappers/ # entity ↔ model ↔ DTO translation
├── models/ # in-app domain models
├── listeners/ guards/ strategies/ …
The modules present today:
| Module | Responsibility |
|---|---|
auth | Login, register, JWT issue/refresh, password reset/change |
users | Internal-user management, roles, locale, pinned wallets |
invitations | Email invitations + token validation, resend/revoke |
organizations | Central/Commercial orgs, exchange address, freeze, per-org chain RPC |
bridging | Fiat↔CBDC disbursement & redemption requests |
multisigs | Accounts (groups): list, create, members, policy, balance, archive |
proposals | Mint/burn/transfer/group-change proposals (built on chain x/group) |
votes | Cast & list votes on a proposal |
whitelist | Allowed recipients for Distribution accounts |
operations | Unified read model over chain proposals and admin actions; activity log |
instant-payments | @alias-addressed retail payments, per-bank settlement accounts, Issuer oversight |
tokens | Token registry/config + autodetect |
dashboard | Supply/balance metrics aggregation |
control-panel | Aggregated admin surface (users, alerts) |
address-book | Saved counterparty addresses |
export | CSV export of transactions & governance |
app-config | Public runtime config (currencyName, native token id) |
chain | @Global() chain abstraction (CosmJS adapters + stubs) |
custody | Encrypted key storage + on-chain tx execution |
chain-tx-audit, idempotency, events, email | Cross-cutting infrastructure |
Entity → model → DTO
Substantive modules separate three shapes of the same data — the entity (TypeORM), the domain model (models/), and the DTO (wire shape) — with a mapper (@Injectable()) owning the translations (toService, toController, fromChain/fromEntity). This keeps derived fields, invariants, and sensitive-field gating (password/refresh-token hashes) in one place. Pure-orchestration modules (bridging) and internal-only entities (idempotency, chain-tx-audit) skip the layer.
Authentication & authorization
Three global guards are registered via APP_GUARD in app.module.ts, in order:
JwtAuthGuard— validates the access-token JWT on every request. Skip with@Public().RolesGuard— enforces@Roles(Role.ADMINISTRATOR)(simple role checks; legacy).PoliciesGuard— enforces@CheckPolicies({ action, subject })via CASL abilities (defineAbilitiesFor(roles)from@cbdc-issuer/shared). Preferred for fine-grained access; the dashboard and API share the same ability definitions.
Passport strategies: LocalStrategy (login), JwtStrategy (access token), JwtRefreshStrategy (refresh rotation).
Token flow
POST /api/auth/login→LocalStrategyvalidates the bcrypt password hash.- The API issues a short-lived access token (
JWT_ACCESS_EXPIRATION, default15m) returned in the body, and a refresh token (JWT_REFRESH_EXPIRATION, default7d) set as an httpOnly cookie. POST /api/auth/refreshreads the refresh cookie, rotates it, and returns a fresh access token. A grace window (AUTH_REFRESH_ROTATION_GRACE_MS, default 60s) absorbs concurrent-tab refresh races.
Configuration
Env vars are validated at startup with Joi (src/config/validation.ts). Missing/invalid values crash the process with a descriptive error — no silent runtime failures. JWT_SECRET/JWT_REFRESH_SECRET require ≥32 chars; DATABASE_MIGRATIONS_RUN defaults to true, DATABASE_SYNCHRONIZE to false.
Database & migrations
PostgreSQL 15 with TypeORM. Schema is owned by migrations in src/database/migrations/, which run automatically on startup (migrationsRun, disable with DATABASE_MIGRATIONS_RUN=false). Entities use autoLoadEntities; each module registers its own via TypeOrmModule.forFeature([...]). After changing an entity, generate a migration (pnpm db:migration:generate …) and register it in src/database/migrations/index.ts — migrations are statically bundled, so unregistered files are silently ignored.