Skip to main content

What it is

Every runtime-configurable choice Parmana makes, which storage backend, which signature algorithm, where keys live, whether caller authentication is on, flows through exactly one function: loadConfig() in packages/shared/src/config/Config.ts. Everything downstream, from which Signer gets constructed to whether the process refuses to start at all, is derived from the single immutable object that function returns.

Why it was built

Config.ts’s own doc comment states the goal plainly: “Centralized immutable configuration. This is the only configuration model used by Parmana.” The practical reason is fail-closed startup. A process that boots successfully, passes /health, and only fails on its first real request (because a key file is missing, or a database is unreachable) is worse than a process that refuses to start at all. Several files across packages/api/src/bootstrap/ exist specifically to convert what would otherwise be a lazy, request-time failure into an eager, startup-time one, and their own comments say so directly: assertSigningKeyMaterialConfigured.ts opens with “Without this, FileKeyProvider only throws lazily… A production process could otherwise boot ‘successfully’, pass /health, and only fail on its very first real request.”

How it works

loadConfig(), section by section

loadConfig() (Config.ts:305-381) reads process.env exactly once per call and returns a frozen Config object with eleven sections: environment, storage, crypto, keys, secrets, authorization, policy, trust, api, auth, rateLimit, logging. Every value is parsed and validated through ConfigValidation.ts’s parse* functions, each of which throws immediately on an unrecognized value rather than silently falling back to a default. Two sections fail closed even harder, by refusing an unset value rather than just an invalid one: requirePolicyDirectory() throws if PARMANA_POLICY_DIR is unset, and assertSigningKeyMaterialConfigured.ts (a separate, startup-only check, not inside loadConfig() itself) does the same for PARMANA_KEY_DIR. .env resolution is location-independent: findEnvFile() walks up from Config.ts’s own directory until it finds a .env file, so every package in the monorepo shares the same configuration regardless of which package’s cwd a script happens to run from.

The full environment variable reference

Every variable below is read somewhere in this monorepo; .env.example is the canonical list (351 lines, heavily commented) and was the primary source for this table, cross-checked against Config.ts/ConfigValidation.ts for the ones that carry a real enum. A genuine discrepancy worth flagging directly: .env.example’s own comment on KEY_PROVIDER says “Only local (FileKeyProvider) has an implementing class today, setting this to any of the other four values does nothing.” That statement is now wrong. SignerBootstrap.ts (Chapter 3) implements aws-kms for real, via KmsSigner. The comment predates that work and was not updated afterward, exactly the kind of drift this book exists to catch rather than repeat.

*Bootstrap classes: composition roots, not dependency injection

Three classes in packages/crypto/src/ follow an identical shape: a static create() method, a private static cache, construction driven entirely by loadConfig().
  • CryptoBootstrap (CryptoBootstrap.ts) builds a CryptoProvider (hash + signature) for a given algorithm, registering built-in providers (SHA256HashProvider, Ed25519SignatureProvider, Dilithium3SignatureProvider) into HashRegistry/ SignatureRegistry and selecting by config.crypto.hashProvider/primarySignatureProvider. createHybrid() builds both primary and secondary providers at once for CRYPTO_MODE=hybrid.
  • KeyBootstrap (KeyBootstrap.ts) is the older KeyProvider composition root. Its own doc comment is unusually direct about its current status: KEY_PROVIDER accepts five values for forward compatibility, but “only FileKeyProvider (local) is actually implemented,” and it throws loudly for anything else rather than silently falling back to file-based keys, closing a real prior gap where a misconfigured aws-kms value used to parse cleanly and then quietly construct a FileKeyProvider anyway.
  • SignerBootstrap (SignerBootstrap.ts) is the newer, signing-capable sibling (Chapter 3 covers Signer vs KeyProvider in full). Unlike KeyBootstrap, it supports both local (LocalFileSigner) and aws-kms (KmsSigner) for real, and is deliberately not memoized the way KeyBootstrap.create() is, a static singleton here previously poisoned test isolation, since each test sets its own PARMANA_KEY_DIR and a cached signer would keep pointing at a deleted temp directory across tests.

Startup order, traced from server.ts

packages/api/src/server.ts runs, in this exact order:
  1. assertStorageConfigured(), refuses to start with PARMANA_STORAGE=supabase and no DATABASE_URL.
  2. assertSigningKeyMaterialConfigured(), materializes PARMANA_KEY_MATERIAL_JSON into PARMANA_KEY_DIR for any file not already present, then confirms default.private.pem/ default.public.pem exist (skipped for KEY_PROVIDER=aws-kms, which has no local file for that key by design).
  3. await assertKmsSigningKeyReachable(), for KEY_PROVIDER=aws-kms only: constructs a real SignerBootstrap-backed signer and confirms the configured KMS key actually exists and is reachable, before binding the port.
  4. assertPaytmConnectorConfigured(), refuses to start with only one of PAYTM_CONNECTOR_URL/PAYTM_CONNECTOR_SHARED_SECRET set.
  5. createExecutionSystem(), createApplication(), createCallerAuthenticator(), createRateLimitStore() (twice, once per limiter, see the tutorial 115 case study in Chapter 12), the actual application graph.
  6. app.listen(PORT, HOST), the port only binds after every check above has passed.
  7. runPolicyGovernanceIntegrityCheckAtStartup() and schedulePolicyGovernanceIntegrityCheck(), fired after the port is bound, deliberately fail-open (Chapter 14), these must never delay traffic from being accepted, unlike steps 1-4.
  8. createGracefulShutdown() wired to SIGTERM/SIGINT.
The split between steps 1-4 (fail-closed, before the port binds) and step 7 (fail-open, after) is a deliberate, named discipline, not an accident of ordering, durable storage and signing key material are things a process cannot function without at all, while a Policy Governance integrity mismatch is something to detect and log, not something that should keep a healthy process from serving traffic.

How it enables things, with a concrete example

Every tutorial in examples/tutorials/ that spins up a real createApplication() instance (the large majority of them) depends on loadConfig() succeeding first, examples/tutorials/89-readiness-probe specifically exercises the boundary between “storage is configured but unreachable” (NOT_READY, 503) and “storage isn’t configured to need reaching at all” (READY, memory-backed), which is this exact configuration model in action. examples/tutorials/113-kms-key-id-resolution exercises resolveKmsKeyId(), one concrete function this configuration ultimately drives (Chapter 3 covers it in depth).

How to validate this yourself

  • packages/shared/src/config/Config.ts, ConfigValidation.ts, StorageProviders.ts, KeyProviders.ts, SecretsProviders.ts, CryptoAlgorithms.ts, TrustProfiles.ts, the full config model and its validation.
  • packages/api/src/bootstrap/assertStorageConfigured.ts, assertSigningKeyMaterialConfigured.ts, assertPaytmConnectorConfigured.ts, the fail-closed startup checks, each with a doc comment explaining exactly what it prevents.
  • packages/api/src/server.ts, the real, ordered bootstrap sequence.
  • packages/crypto/src/CryptoBootstrap.ts, KeyBootstrap.ts, SignerBootstrap.ts, the three composition roots.
  • .env.example, the canonical, heavily-commented variable reference (verify against ConfigValidation.ts for anything you’re not sure is still accurate, per the KEY_PROVIDER drift noted above).

Integration requirements

A minimal working .env needs, at least: PARMANA_POLICY_DIR, PARMANA_KEY_DIR (with a real default.private.pem/default.public.pem pair present, or PARMANA_KEY_MATERIAL_JSON set), and either PARMANA_STORAGE=memory (no further storage config needed) or PARMANA_STORAGE=supabase plus DATABASE_URL/SUPABASE_URL/SUPABASE_SERVICE_ROLE_KEY/ SUPABASE_ANON_KEY. Caller authentication needs either PARMANA_API_KEYS populated or PARMANA_AUTH_DISABLED=true (development only). KEY_PROVIDER=aws-kms additionally needs AWS_REGION and either AWS_ROLE_ARN (Vercel OIDC federation) or a real AWS credential chain reachable in the runtime environment (~/.aws/credentials locally, an instance/task role elsewhere).