Skip to main content

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…

6 viewsAbout 3 min read
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:

  1. changes only the literal exports listed in the static route manifest;
  2. switches @/lib/cms and @/lib/db imports to the export and stub providers;
  3. fails if a dynamic export or DB provider import remains;
  4. 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

  1. Edit the UTF-8 sources under courses/ and notes/.
  2. In an authenticated Admin session, run the schema migration and idempotent seed/upsert.
  3. Check Docker SSR Korean/English routes and /api/export.
  4. Rebuild the snapshot with the static-host deployment command.
  5. 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.

More in quality

All in this category →

Was this article helpful?