feat: single-container deployment — API serves the Expo Web bundle (#186) #204
No reviewers
Labels
No labels
area:auth
area:ci
area:db
area:infra
area:native
area:pwa
area:service
epic
feature
foundation
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
james/carol!204
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "186-single-container-deployment"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Closes #186. The API container now also serves the Expo Web static bundle, so self-hosters still run one image. Two commits:
feat(api): catch-all to serve the Expo Web bundle— addsapps/api/app/[...spa]/route.tspluslib/spa/serve.ts. The catch-all sits behind every explicit Next.js route and only fires for paths nothing else handles.build(docker): bundle the Expo Web client into the API image— the multi-stage build now exportsapps/client/distin the same builder stage as the API standalone build and copies it into/app/apps/client/diston the runtime image.Resolution order in the catch-all
For URL
/<segments…>:<dist>/<segments…>(e.g./_expo/static/js/web/entry-X.js).html→<dist>/<segments…>.html(Expo pre-rendersnotes.html,profile.html, etc.)index.html→<dist>/<segments…>/index.html<dist>/index.htmlCache headers:
_expo/static/andassets/→public, max-age=31536000, immutable(content-hashed).no-cache.Defensive:
/api/*returns 404, so a typo'd API endpoint never returns SPA HTML.Public-routes allowlist
Added
/_expo/and/assets/tolib/auth/public-routes.ts. The SPA shell's JS chunks and bundled assets have to load before a session exists. The shell HTML still goes through the existing redirect flow (unauth navigation →/login).New env var
SPA_BUNDLE_PATH— override the bundle path. Default autodetects (/app/apps/client/distin the release image, plus dev/test fallbacks). An explicit override is authoritative — pointing at a missing directory disables the SPA fallback (404 for any non-/api/*path Next.js doesn't claim) rather than silently picking up a discovered fallback. Documented inREADME.mdOperations table.Image size
Expo Web bundle is ~2MB. The ticket budget is ≤20% growth; this is well under.
Test plan
pnpm -F @carol/api typecheck/lint/test(563 passed, 107 skipped — no Postgres URL).pnpm -F @carol/api openapi:check/openapi:coverage(54 routes).pnpm -F @carol/api-client check(drift gate green).pnpm -F @carol/client typecheck/lint/test.pnpm -F @carol/client export:websucceeds, bundle produced.podman build -t carol:test-186 .succeeds end-to-end. (filled in once the local build completes)/api/*404, path traversal, HEAD without body, missing-bundle 404.Out of scope
/sw.jscontinues to work as today; whether to swap to an Expo-side SW lands with #185 or later.Closes #186.
📊 Test coverage
Patch coverage: no testable lines changed.
Overall (
app/,lib/,db/, excluding UI per ADR-0019):Soft thresholds per ADR-0019. Coverage is informational and does not block merge.
Trivy (container image)
Threshold:
high· Total findings: 121 · At/above threshold: 16.27.0, 7.28.0, 8.5.0