The SSR/Fly CMS contract and a static build profile
An example content service serves the same screens through two deliberate data boundaries. Docker SSR reads PostgreSQL, while a static host builds pages from the /api/export snapshot captured at delivery time. Forcing b…
Table of contents
An example content service serves the same screens through two deliberate data boundaries. Docker SSR reads PostgreSQL, while a static host builds pages from the /api/export snapshot captured at delivery time. Forcing both into one runtime would make the static build require database credentials or make the mirror lag behind an SSR change without an explicit release.
The public function contract
apps/web/src/lib/cms-contract.ts lists the minimum functions both adapters must provide. cms.ts and cms.static.ts expose cmsAdapterContract, so removing a function from either side fails TypeScript verification immediately.
The contract does not merge the data providers:
- SSR applies SQL filters, ordering, pagination, and pool timeouts.
- Fly reads the export payload once and reproduces the same lookup meaning in memory.
- Writes are SSR-only; the Fly adapter intentionally rejects
createInquiry.
The static build profile
Next.js dynamic and dynamicParams are statically analyzed, so changing them safely with one environment variable is not enough. scripts/prepare-fly-build.mjs runs only inside the image build layer and:
- changes only the literal exports listed in the static route manifest;
- switches
@/lib/cmsand@/lib/dbimports to the export and stub providers; - fails if a dynamic export or DB provider import remains;
- leaves the working tree untouched.
Adding a new route without updating the manifest therefore fails the Fly build instead of silently publishing an incomplete contract.
Content delivery order
- Edit the UTF-8 sources under
courses/andnotes/. - In an authenticated Admin session, run the schema migration and idempotent seed/upsert.
- Check Docker SSR Korean/English routes and
/api/export. - Rebuild the snapshot with the static-host deployment command.
- Check static-host health, both locales, series/lesson/note routes, and the canonical host.
A successful DB seed without a Fly redeploy leaves users on the old snapshot. Conversely, a successful Fly build with no corresponding row in the DB export should produce a 404 for that series or note; that is a useful consistency signal.
Completion criteria
| Verification axis | SSR | Static mirror | Equality evidence |
|---|---|---|---|
| Data source | Live PostgreSQL read | Versioned export snapshot | Zero content-field drift |
| Search and order | SQL | In-memory adapter | Same fixture results |
| Writes | Authenticated server path | Unavailable | Fail-closed error |
| Secrets | Runtime secret | Discarded after build fetch | Final-image inspection |
| Refresh | DB upsert and revalidate | New static release | Bilingual public smoke |
files → authenticated upsert → SSR/export drift 0 → static build → public route smoke
If an intermediate stage fails, later stages cannot close the release. Rebuilding static pages from a stale export only deploys the old snapshot more reliably.
- SSR and Fly expose the same adapter function list.
- The build-profile residue checks pass.
- The secret is absent from Docker
ARG,ENV, and the final image. - After seeding, Docker and Fly return the same bilingual content.