| name | nitro |
| description | Own the Snapmatch static release contract: Nitro v3 through TanStack Start, Firebase Hosting from the selected app's `dist/client` at `BASE_PATH=/`, optional GitHub Pages isolation, deployment-failure diagnosis, exact-`origin/main` release verification, and live Hosting smoke checks. Use for Firebase deploys, red deployment checks, `configure-pages` failures, hosting verification, and static artifact or base-path changes. |
Owns the static-build contract that emits apps/<app>/dist/client/ for Firebase Hosting. There is
no apps/<app>/nitro.config.ts file in Snapmatch — Nitro v3 is driven internally by TanStack
Start's Vite plugin via tanstackStart({ spa: …, prerender: … }). Firebase Hosting is canonical;
GitHub Pages compatibility is an optional layer on the same artifact.
When to invoke
- Touching
firebase.json, a deploy workflow, apps/<app>/package.json's build script, or
apps/<app>/vite.config.ts's base resolution.
- Diagnosing any reported deployment failure or red deploy-related GitHub check.
- Publishing an exact
origin/main commit to Firebase Hosting or proving what is live afterward.
- Adding or fixing the canonical
BASE_PATH=/ contract or an optional Pages mirror base path.
- Diagnosing a missing
404.html, missing .nojekyll, or 404'd asset paths in the deployed site.
- Deciding which app in the monorepo gets published.
Owns
- The
BASE_PATH env contract — Firebase Hosting builds at /; only an optional Pages mirror uses a
repository subpath.
- Root
firebase.json Hosting configuration, including public: apps/<app>/dist/client and the SPA
rewrite to /index.html.
- Optional post-build Pages compatibility files:
404.html and .nojekyll. Firebase Hosting does
not depend on either file.
- Deployment-status classification across PR CI, the optional Pages mirror, and Firebase Hosting.
- The canonical Hosting release proof chain: exact remote-main SHA, clean checkout, Hosting-only CLI
release, and read-only smoke against the canonical URL.
- The optional Pages workflow's selector, opt-in gate, Turbo filter, and artifact verification.
- The
prerender.failOnError: true invariant — a missing prerender route fails CI (Pillar 4).
Defers to
tanstack-start-spa-prerender (Wave 3, forward) — for the application-level SPA + prerender switch (tanstackStart({ spa: { enabled: true, prerender: { outputPath: "/index" } }, prerender: { enabled: true, crawlLinks: true } })).
tanstack-router-pwa-deep-links (Wave 3, forward) — for the Workbox navigation-fallback contract that resolves deep links offline against the prerendered shell.
vite — for vite.config.ts's resolveBase() helper that consumes BASE_PATH.
t3-env — for public Firebase Web configuration and the build-time environment gate. Hosting CLI
credentials are not browser env.
turborepo — for turbo run build --filter=@snapmatch/<app> task ordering and the build cache.
bun-runtime — for invoking bun run build and bun install --frozen-lockfile in CI.
Snapmatch stack rules
- There is no
apps/<app>/nitro.config.ts. TanStack Start's Vite plugin owns Nitro configuration. Don't introduce a standalone Nitro config — it duplicates and races with the framework-level config.
- The static artifact lives at
apps/<app>/dist/client/, NOT .output/public/. Vite writes there
directly via TanStack Start's SPA + prerender pipeline. Root firebase.json must point Hosting at
this exact directory.
- Pillar 4 (CLI-gate-first): the build is part of
bun run build and any prerender error MUST fail the gate (tanstackStart({ prerender: { failOnError: true } })).
- Firebase Hosting is canonical and builds with
BASE_PATH=/. Hosting serves from a dedicated
domain root and its SPA rewrite handles deep links. Never build the canonical artifact with a
repository-name base path.
- An optional GitHub Pages mirror may drive
BASE_PATH from
actions/configure-pages@v5's base_path output. That Pages value must never leak into the
Firebase Hosting build.
- There is no Firebase Hosting deploy workflow under
.github/workflows/ today.
.github/workflows/deploy.yml is named optional-github-pages and cannot prove that Firebase was
deployed. A green skipped Pages run proves only that its opt-in guard worked.
- The optional Pages workflow runs only when the repository variable
PAGES_ENABLED is exactly
true, after Pages is configured with GitHub Actions as its publishing source. Otherwise it skips
cleanly and must not fail a main push. Do not set enablement: true with the default
GITHUB_TOKEN—the action requires a separate token with repository/Pages administration
permission for enablement.
- The package is
nitro (v3), not nitropack (v2). Imports come from nitro / nitro/vite / nitro/storage if you ever need them — but you generally won't, because TanStack Start mediates.
- The Hosting deploy publishes one selected app per run. Keep the workflow selector and
firebase.json.hosting.public aligned; fail before deploy if that artifact does not exist.
- The default
firebase:deploy is Hosting-only. Anonymous Auth provisioning, domain Rules/index
deployment, and VITE_FIREBASE_DOMAIN_SYNC_ENABLED=true are separate, explicit rollout actions;
never couple them to a static artifact publish.
- Firebase Hosting is static infrastructure, not an application backend. Do not add Nitro server
handlers or claim that Auth, App Check, or Firestore reconciliation exists because Hosting works.
Snapmatch's anonymous Auth/sync implementation is a separate browser boundary, not a Nitro feature.
Deployment failure triage
Do not rerun or modify anything until the failing system is identified.
- Record the run URL, workflow name, event, exact commit SHA, failing job, failing step, and exact
error. A red check labeled “deploy” is not enough evidence.
- Classify the failure independently:
ci is the pull-request release gate.
optional-github-pages is the secondary mirror.
- Firebase Hosting is evidenced by Firebase CLI/release output and the canonical Hosting URL.
- Inspect job dependencies and skipped jobs. A successful prepare job with skipped Pages build and
deploy jobs is the correct result when
PAGES_ENABLED is not exactly true; it is not a
Firebase release.
- Treat
actions/configure-pages@v5 returning a Pages-site 404 as Pages configuration evidence.
Do not report Firebase Hosting as failed, and do not set enablement: true with the default
GITHUB_TOKEN. Configure Pages with GitHub Actions as its publishing source before opting in.
- Fix the smallest owning surface, run
bash scripts/check-deployment-policy.sh, and require the
normal PR gate before merging.
Canonical release proof
For a production release, keep four proofs separate: green PR CI, the merged remote-main SHA,
successful Firebase Hosting CLI output, and a live canonical-site smoke. Missing evidence is
“unverified,” not success.
- Wait for the PR's full
bun run check workflow to pass, then merge.
- Fetch
origin/main, record its full SHA, and use a clean temporary worktree detached at that
exact SHA. Never switch or clean a user worktree with unrelated changes.
- Confirm
HEAD equals the freshly fetched origin/main, run bun install --frozen-lockfile, and
run bun run check. When diagnosing stale build output, force the selected app's Turbo build
instead of accepting cached logs as the only build evidence.
- Run
bun run firebase:deploy:production. It remains Hosting-only, while explicitly injecting
VITE_FIREBASE_DOMAIN_SYNC_ENABLED=true into the firebase.json predeploy build. Accept the
release only when the command exits zero and Firebase reports the Hosting version released.
Never substitute or couple the Auth or Firestore deploy commands.
- Load the canonical
https://snapmatch-b9c4d.web.app/ URL in a real browser and verify HTTP 200,
the Snapmatch title, the connectionChecks/web connectivity marker, and no page errors. When
production domain sync is enabled, also require the generated PLAYER.##### alias and a bounded
Join the Fight transition to its Game queue.
- Report the full SHA, PR/check links, Firebase Hosting URL, and each proof separately. An optional
Pages result—green, red, or skipped—is never canonical Hosting evidence.
Patterns
apps/<app>/vite.config.ts — base-path resolver
function resolveBase(): string {
const raw = process.env.BASE_PATH;
if (!raw || raw === "/") return "/";
return raw.endsWith("/") ? raw : `${raw}/`;
}
export default defineConfig(async ({ mode }) => {
Object.assign(process.env, loadEnv(mode, process.cwd(), ""));
await import("./app/env");
return {
base: resolveBase(),
plugins: [
tanstackStart({
srcDirectory: "app",
spa: { enabled: true, prerender: { outputPath: "/index" } },
prerender: {
enabled: true,
crawlLinks: true,
autoSubfolderIndex: true,
failOnError: true,
},
}),
],
};
});
Local dev and canonical Firebase Hosting: BASE_PATH=/ (or unset) → /. An optional GitHub Pages
mirror: BASE_PATH=/snapmatch → /snapmatch/.
apps/<app>/package.json — build script
"build": "vite build && cp dist/client/index.html dist/client/404.html && touch dist/client/.nojekyll"
The cp and .nojekyll marker keep the same artifact compatible with an optional GitHub Pages
mirror. Firebase Hosting ignores .nojekyll and uses the rewrite in firebase.json; neither file is
load-bearing for the canonical target.
Root firebase.json — canonical Hosting target
{
"hosting": {
"public": "apps/web/dist/client",
"ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
"rewrites": [{ "source": "**", "destination": "/index.html" }]
}
}
Use the selected app in place of web. The rewrite serves the SPA shell for a direct navigation;
static asset files still win before the rewrite. Keep Firestore rules/index config in the same root
Firebase configuration when Firestore is enabled. Keep the browser-session anonymous Auth provider
in that root configuration too, but deploy it separately with bun run firebase:deploy:auth.
Canonical Firebase Hosting build and deploy
There is currently no automated Firebase deploy workflow. From the clean worktree pinned to the
freshly fetched origin/main SHA, use the repository-owned commands:
bun install --frozen-lockfile
bun run check
bun run firebase:deploy:production
The last command is Hosting-only and runs the production build through Firebase's predeploy hook
with domain synchronization explicitly enabled. firebase:deploy:hosting is the isolated static
Hosting command and follows the repository-default disabled domain boundary.
Local Firebase CLI authentication is operator state; never commit or print service-account JSON,
private keys, refresh tokens, or FIREBASE_TOKEN. The Firebase project ID and browser Web config
are public identifiers, not credentials.
Optional GitHub Pages mirror
If the repository keeps a Pages mirror, run a separate build whose BASE_PATH comes from
actions/configure-pages@v5, then upload apps/<app>/dist/client with
actions/upload-pages-artifact@v3 and deploy with actions/deploy-pages@v4. That workflow is
secondary; it must not redefine the canonical artifact path or Firebase root-base contract. Keep
the whole build/deploy chain behind PAGES_ENABLED=true; disabled Pages must leave prepare green and
skip both downstream jobs.
Add a route to the prerender list
Crawled links from the home page are picked up automatically (crawlLinks: true). For routes the crawler can't reach (islands, dynamic links), pass them explicitly via TanStack Start's prerender.routes:
tanstackStart({
prerender: {
enabled: true,
crawlLinks: true,
failOnError: true,
routes: ["/games/sample", "/games/sample/level-1"],
},
});
Custom domain
A Firebase Hosting custom domain still builds at BASE_PATH=/. Domain mapping is Hosting
infrastructure and does not require application code changes.
Anti-patterns
- Don't create
apps/<app>/nitro.config.ts — TanStack Start drives Nitro; a standalone config duplicates and races with framework-level config.
- Don't reference
.output/public — that's the upstream Nitro doc default, but TanStack Start's SPA mode writes to dist/client/ instead. The deploy workflow uploads from apps/<app>/dist/client/, not .output/public/.
- Don't import from
nitropack — that's v2; v3 is nitro / nitro/vite / nitro/storage.
- Don't hardcode
/snapmatch/ (or any repository name) into the canonical build. Firebase Hosting
uses BASE_PATH=/; a Pages-only build may receive a dynamic subpath.
- Don't rely on
404.html as Firebase's SPA fallback — configure the Hosting rewrite to
/index.html. Keep 404.html only for the optional Pages mirror.
- Don't introduce
serverHandlers, routeRules.cache, routeRules.swr, routeRules.proxy, or
useStorage — Firebase Hosting serves static files; these need a deliberately authorized server
runtime.
- Don't set
prerender.failOnError: false to make a build pass — the missing route is the bug, fix the route.
- Don't call a Pages failure a Firebase deployment failure. Identify the workflow, job, step,
event, and SHA first.
- Don't treat a local branch named
main, a green/skipped Pages run, or Firebase CLI exit alone as
complete release proof. Pin the freshly fetched origin/main SHA and verify the canonical site.
- Don't add
enablement: true, a PAT, or an app token merely to silence configure-pages 404s.
Pages is optional; configure it explicitly before setting PAGES_ENABLED=true.
- Don't commit Firebase CLI credentials or service-account JSON.
.firebaserc, firebase.json,
Firestore rules, indexes, and public web identifiers are configuration; credentials are not.
Triggers on
nitro, firebase hosting, firebase deploy, deployment failed, deploy check, live deployment, exact
main deploy, hosting smoke, firebase.json, github_pages, PAGES_ENABLED, BASE_PATH, baseURL,
prerender, SPA fallback, 404.html, .nojekyll, dist/client, configure-pages, deploy-pages,
upload-pages-artifact