What it is
This codebase ships with real configuration for two deployment targets: Vercel (vercel.json, serverless Functions) and Fly.io (fly.toml, Dockerfile, a long-running
process). Both run the identical application code; what differs is the execution model, and
that difference has produced at least one real production incident worth understanding in
detail.
Why it was built this way
Two genuinely different deployment shapes were supported deliberately: Vercel for zero-maintenance serverless hosting with automatic scaling, Fly.io for a conventional always-on process with a persistent filesystem and predictable warm state. Neither is presented as strictly better in this codebase; they trade off differently, and thePARMANA_STORAGE/repository-selection logic (Chapter 13) is written to work correctly under
both.
How it works
Vercel
vercel.json:
api/index.ts. includeFiles is
notable: it explicitly bundles policies/** into the deployed Function, since the Function’s
filesystem is otherwise whatever the build produced, nothing more. This is also the exact
filesystem that turned out to be read only at runtime, which is the deployment-specific
incident below.
Fly.io
fly.toml:
Dockerfile), always running at least one
machine, with Fly’s own health check hitting GET /ready every 30 seconds, the same readiness
probe covered in Chapter 19. Unlike Vercel, this filesystem is genuinely persistent and
writable for the life of the machine.
The read-only filesystem incident, a concrete illustration
PolicyChangeApprovalService.approve() (Chapter 7) writes the newly approved policy content via
PolicyRepository.save(). The original implementation, FilePolicyRepository, writes to the
local filesystem, this works perfectly on Fly.io (writable, persistent) and in local
development, but Vercel’s serverless Functions run on a read only filesystem outside
/tmp. The very first real production approval attempt against the deployed Vercel instance
failed immediately with EROFS. The fix, SupabasePolicyRepository (added 2026-09-16, see
Chapter 13), writes to a policies table in Postgres instead, which works identically on both
platforms. This is the clearest concrete example in this codebase of why “which platform am I
deploying to” is not a cosmetic choice, it changes which filesystem assumptions are safe to
make in application code.
AWS KMS and Vercel OIDC
For real production signing (as opposed to a local Ed25519 key file), this codebase integrates with AWS KMS via Vercel’s native OIDC federation, no static, long-lived AWS credentials stored anywhere.KmsSigner.ts reads exactly two environment variables directly:
packages/crypto/src/providers/signer/KmsSigner.ts directly; these are
the only two process.env reads in that file, KmsSigner requires AWS_REGION to be set is a
real thrown error if AWS_REGION is missing.) docs/operations/aws-kms-vercel-oidc-setup-guide.md
documents the Vercel side of the setup (vercel env add AWS_REGION production, and the trust
policy connecting Vercel’s OIDC issuer to the IAM role). docs/operations/2026-09-15-kms-migration-troubleshooting-guide.md
records the real incidents hit while migrating to this from static keys, cross-check its
specific claims against current source rather than assuming they still describe live bugs; the
migration referenced there is complete as of this session.
KEY_PROVIDER=local remains the only fully implemented local-key path; setting KEY_PROVIDER
to anything else (aws-kms, azure-key-vault, gcp-kms, hsm were once silently ignored, a
real gap closed by making KeyBootstrap.create() throw explicitly naming the unimplemented
value, see docs/VERIFICATION-GAPS.md gap 40 in the July session’s table). The actual KMS
signing path used in production goes through KmsSigner/SignerKeyProviderAdapter
independently of that specific KEY_PROVIDER flag, verify the current wiring in
packages/crypto/src/SignerBootstrap.ts if you need the precise selection logic.
Concrete example
There is no example tutorial that actually deploys to either platform (that would require real cloud credentials and is out of scope for a hermetic tutorial suite), butdocs/site/guides/deploy-patterns.mdx and docs/operations/aws-kms-vercel-oidc-setup-guide.md
walk through the real, current deployment steps for each target.
How to validate this yourself
vercel.json,fly.toml,Dockerfile(repo root) for the two real platform configs.packages/crypto/src/providers/signer/KmsSigner.tsfor the exact AWS env vars actually read.packages/policy/src/SupabasePolicyRepository.ts’s own doc comment for the EROFS incident account.supabase/migrations/20260916060000_add_policies_table.sqlfor the schema the fix depends on.docs/operations/2026-09-15-kms-migration-troubleshooting-guide.mdanddocs/operations/aws-kms-vercel-oidc-setup-guide.mdfor the full operational history.
Requirements
Vercel deployment:AWS_REGION,AWS_ROLE_ARN(for KMS signing) or local key material viaPARMANA_KEY_MATERIAL_JSON(for local Ed25519 signing, not recommended for production but functional).PARMANA_STORAGE=supabasewithDATABASE_URLset, since the filesystem cannot be relied on for anything written at runtime, including approved policy content.- Every other env var from Chapter 20’s requirements table.
- The same core env vars,
PARMANA_STORAGE=memoryis viable here if you genuinely want no external database (the filesystem is real and persistent), butsupabaseremains the production-realistic choice for durability across machine restarts. fly.toml’s health check expectsGET /readyto respond within its configured timeout (5s); see Chapter 19 for what that route actually checks.