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 aCryptoProvider(hash + signature) for a given algorithm, registering built-in providers (SHA256HashProvider,Ed25519SignatureProvider,Dilithium3SignatureProvider) intoHashRegistry/SignatureRegistryand selecting byconfig.crypto.hashProvider/primarySignatureProvider.createHybrid()builds both primary and secondary providers at once forCRYPTO_MODE=hybrid.KeyBootstrap(KeyBootstrap.ts) is the olderKeyProvidercomposition root. Its own doc comment is unusually direct about its current status:KEY_PROVIDERaccepts five values for forward compatibility, but “onlyFileKeyProvider(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 misconfiguredaws-kmsvalue used to parse cleanly and then quietly construct aFileKeyProvideranyway.SignerBootstrap(SignerBootstrap.ts) is the newer, signing-capable sibling (Chapter 3 coversSignervsKeyProviderin full). UnlikeKeyBootstrap, it supports bothlocal(LocalFileSigner) andaws-kms(KmsSigner) for real, and is deliberately not memoized the wayKeyBootstrap.create()is, a static singleton here previously poisoned test isolation, since each test sets its ownPARMANA_KEY_DIRand 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:
assertStorageConfigured(), refuses to start withPARMANA_STORAGE=supabaseand noDATABASE_URL.assertSigningKeyMaterialConfigured(), materializesPARMANA_KEY_MATERIAL_JSONintoPARMANA_KEY_DIRfor any file not already present, then confirmsdefault.private.pem/default.public.pemexist (skipped forKEY_PROVIDER=aws-kms, which has no local file for that key by design).await assertKmsSigningKeyReachable(), forKEY_PROVIDER=aws-kmsonly: constructs a realSignerBootstrap-backed signer and confirms the configured KMS key actually exists and is reachable, before binding the port.assertPaytmConnectorConfigured(), refuses to start with only one ofPAYTM_CONNECTOR_URL/PAYTM_CONNECTOR_SHARED_SECRETset.createExecutionSystem(),createApplication(),createCallerAuthenticator(),createRateLimitStore()(twice, once per limiter, see the tutorial 115 case study in Chapter 12), the actual application graph.app.listen(PORT, HOST), the port only binds after every check above has passed.runPolicyGovernanceIntegrityCheckAtStartup()andschedulePolicyGovernanceIntegrityCheck(), fired after the port is bound, deliberately fail-open (Chapter 14), these must never delay traffic from being accepted, unlike steps 1-4.createGracefulShutdown()wired toSIGTERM/SIGINT.
How it enables things, with a concrete example
Every tutorial inexamples/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 againstConfigValidation.tsfor anything you’re not sure is still accurate, per theKEY_PROVIDERdrift 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).