Read this file before every non-trivial task in a fork. It defines
the contract between zerostack (the template, tracked as upstream)
and the product code in this fork.
This file is companion to forking-checklist.md
and the root AGENTS.md. When they disagree, this file
wins inside a fork.
The rule of thumb: does upstream ship updates to this file?
- Yes → it's frozen or customize, do not edit casually.
- No → it's extend, write whatever the product needs.
Auth, SEO, CI, and infrastructure. Edits here cause merge conflicts
the next time we pull upstream/main. If you think one of these
needs changing, say so to the user — they'll decide whether to
patch the fork (and absorb a future conflict) or upstream the change.
# Auth + Supabase wiring
packages/supabase/src/{browser,server,service,index}.ts
apps/web/src/lib/supabase/{client,server,proxy,service,env}.ts
apps/web/src/lib/admin.ts
apps/web/src/proxy.ts
apps/web/src/app/login/
apps/web/src/app/auth/
# SEO surface
apps/web/src/app/robots.ts
apps/web/src/app/sitemap.ts
apps/web/src/app/opengraph-image.tsx
# Template surfaces (frozen — extend by composition, not by rewrite)
apps/web/src/app/app/layout.tsx
apps/web/src/app/app/app-nav.tsx
apps/web/src/app/app/settings/
apps/web/src/app/app/admin/
apps/web/src/components/feedback/
apps/web/src/components/waitlist/
apps/web/src/components/theme-*.tsx
apps/web/src/components/ui/
# Build + CI
.github/workflows/ci.yml
.husky/
turbo.json
pnpm-workspace.yaml
tsconfig*.json (root + packages/config/)
# Migrations that ship with the template
supabase/migrations/20260529000000_init_notes.sql
supabase/migrations/20260529001000_init_profiles.sql
supabase/migrations/20260529002000_init_waitlist_feedback.sql
Frozen files have a few legitimate exceptions in a fork:
apps/web/src/app/app/app-nav.tsx— you may add extra nav links if the upstream design exposed a slot for them. If not, propose to upstream instead.apps/web/src/components/ui/— these are generated by shadcn. Adding new components viapnpm dlx shadcn@latest add ...is fine. Editing existing ones is not.- Migrations are append-only. Adding a new migration file is extending (see below). Editing or deleting an existing template migration is frozen — it would diverge schemas across forks.
Branding, copy, and config that every product changes. .gitattributes
in zerostack marks these merge=ours, so git merge upstream/main
keeps the fork's version on conflict.
apps/web/src/lib/site.ts
apps/web/src/app/layout.tsx # metadata, fonts, providers
apps/web/src/app/page.tsx # landing page
apps/web/src/components/site-nav.tsx
apps/web/src/components/site-footer.tsx
README.md
LICENSE
.env.example
package.json (root) # name, version, scripts
Editing here doesn't require a flag; it's the expected surface.
If you add a new file that's purely product copy (e.g. /pricing),
it's extend, not customize. The customize list only covers files that
also exist in upstream.
Your product lives in these directories. Upstream rarely touches them; when it does, conflicts are real signals (someone duplicated a name) and worth reading.
apps/web/src/app/app/<your-feature>/ # new feature routes
apps/web/src/app/<your-marketing>/ # /pricing, /about, /blog, …
apps/web/src/components/<feature>/ # product-specific components
packages/contracts/src/index.ts # ADD schemas; don't remove template ones
supabase/migrations/2026MMDD…_<name>.sql # new migrations
docs/<your-doc>.md # product-specific docs
The template notes feature was deliberately left in
apps/web/src/app/app/ so you have a reference. You may delete it
once your product replaces it — that's the user's call, made during
forking (see step 5 of forking-checklist.md). Once the choice is
made, record it at the top of this file (the fork's copy) so I don't
re-add notes later thinking it was missing.
- Create
supabase/migrations/<YYYYMMDDHHMMSS>_<name>.sql. Copy the structure from20260529000000_init_notes.sql:id uuid primary key default gen_random_uuid()user_id uuid not null references auth.users(id) on delete cascade- timestamps
alter table … enable row level security- explicit policies for select / insert / update / delete, all
gated by
auth.uid() = user_id
- Add a schema to
packages/contracts/src/index.ts—<name>Schemafor the row,create<Name>Schemafor the input. Mirror thenoteSchema/createNoteSchemapattern. - Create the route under
apps/web/src/app/app/<feature>/:page.tsx— Server Component fetching rows + rendering.actions.ts— Server Actions for create/update/delete.<feature>-form.tsx—"use client"form bound to the action viauseActionState.
- Validate every input with the
create<Name>Schemabefore touching the DB. Parse every row with<name>Schemabefore rendering — Zod is the boundary check between Postgres and the UI. - Do not run
pnpm db:typesin a fork unless the user asks. The placeholder is permissive enough for product code; regenerating is a one-time-per-fork chore that lives in the checklist.
Create apps/web/src/app/<route>/page.tsx. Use the existing landing
as a reference for typography and spacing. Add <a href="#…"> entries
to site-nav.tsx if it should be discoverable.
If the page needs metadata, export metadata: Metadata from the page
file — metadataBase and the default title template come from
apps/web/src/app/layout.tsx (frozen).
Append a new <Card> to apps/web/src/app/app/admin/page.tsx. The
service-role client and isAdminEmail guard are already wired up.
If the admin needs its own routes (/app/admin/users,
/app/admin/stats), create them under apps/web/src/app/app/admin/
and reuse the existing guard at the top of each page.
The template does not ship an email integration. When the product
needs one, the recommended path is Resend with RESEND_API_KEY in env
and a small lib/email.ts helper. Coordinate with the user before
adding the dependency.
This is not a fork-level change. Supabase Auth supports OAuth
through dashboard config; flip the toggles in
Authentication → Providers and add the OAuth callback URL. Code in
apps/web/src/app/login/ stays the same.
Stop. Ask the user. Two paths:
- The change is product-specific. Use composition instead — wrap the frozen component, don't edit it. Add a sibling file that replaces the use of the frozen one in your product code.
- The change should benefit every zerostack fork. Make it in the
upstreamclone of zerostack, ship it, thengit merge upstream/maininto the fork.
If neither works and you really must patch a frozen file in the fork,
document the patch in docs/upstream-drift.md (create if missing) so
the next merge knows where the conflict will appear.
The root AGENTS.md rules apply unchanged:
- Conventional Commits with a why body.
- No session-local references, no secrets.
- Comments only when WHY is non-obvious.
- Incremental commits — one logical step per commit.
The pre-commit hook (pnpm lint-staged + pnpm typecheck) is frozen.
Don't disable it; if it slows you down, the cache makes the second
run sub-second.
You're working in a fork, and you've spotted a bug, dead doc link, or inconsistency in upstream zerostack (frozen files, shipped docs, CI config). Apply this decision tree before doing anything:
Is the change useful only to THIS product?
│
┌────────┴────────┐
Yes No
│ │
Patch locally Is it a security issue?
in the fork. │
Do not upstream. ┌────┴────┐
Yes No
│ │
Private Is it a verifiable bug
disclosure OR a doc fix < 20 lines?
(security │
advisory). ┌─────┴─────┐
Yes No
│ │
Open a PR Is it a substantial
upstream. feature / refactor?
│
┌────┴────┐
Yes No
│ │
Open an Keep it local.
Issue Below threshold.
("proposal")
Do not open issues or PRs upstream below this bar:
-
✅ Real bug in shipped behaviour (auth flow, RLS policy, build, CI gate, type error from a default clone).
-
✅ Documentation typo, broken link, or outdated instruction that misleads a future reader.
-
✅ Security vulnerability — but private channel only, not public issue. See
.github/SECURITY.md. -
✅ Concrete proposal for a feature that benefits every fork and the user explicitly agreed to upstream it.
-
❌ Personal preferences ("I'd rename this", "I'd format this differently").
-
❌ Refactors without a defect.
-
❌ Vendor integrations (Stripe, Resend, OAuth provider, analytics) — those belong in forks.
-
❌ "Wouldn't it be nice if…" without a concrete user.
-
❌ Anything you can fix locally in the fork without affecting other forks.
When in doubt: don't open it. The maintainer prefers missing a mediocre suggestion over drowning in noise.
Always check first:
# Are there existing issues / PRs covering this?
gh issue list --repo zerox9dev/zerostack --search "<keywords>"
gh pr list --repo zerox9dev/zerostack --search "<keywords>"If a duplicate exists, comment there instead of opening a new one.
gh issue create --repo zerox9dev/zerostack --web--web opens the prefilled form. Pick the right template:
- Bug report for reproducible defects.
- Improvement proposal for new ideas (mandatory before any feature PR).
Fill every required field. Concrete reproduction beats long prose. Cross-link the upstream commit your fork is based on so the maintainer can rebase to that snapshot.
The maintainer requires a real GitHub user account for the PR — you
cannot push to zerox9dev/zerostack directly. The flow:
# 1. One-time: fork zerostack on the user's account
gh repo fork zerox9dev/zerostack --clone=false
# 2. Clone the user's fork to a working directory outside the product
cd /tmp
gh repo clone <username>/zerostack zerostack-upstream-pr
cd zerostack-upstream-pr
git remote add upstream git@github.com:zerox9dev/zerostack.git
git fetch upstream
git checkout -b fix/<short-slug> upstream/main
# 3. Apply the smallest patch that fixes the problem
# — copy from the product fork, do NOT bring along product code
pnpm install
pnpm typecheck && pnpm lint && pnpm build
# 4. Commit with conventional-commits, push, open PR
git add -p # stage exactly the patch — nothing else
git commit -m "fix(scope): one-line subject
Body: what's wrong and why this fixes it. Reference the upstream
issue if one exists. Keep the surface minimal — see CODEOWNERS for
which files require maintainer review.
"
git push -u origin fix/<short-slug>
gh pr create --fill --webStrict requirements:
- One concern per PR. Don't bundle the fix with a tangential cleanup, even a tempting one.
- Don't touch frozen files unless the bug is in a frozen file itself (and then say so in the PR body).
- No product code. Anything specific to this fork's product must not appear in the diff.
- PR template filled completely. Skipped checkboxes are a closed PR.
- CI must pass. Push again after fixing if it doesn't.
You are unlikely to need this flow often. The default is:
- Fix the problem locally in the fork.
- Note it in this fork's
docs/upstream-drift.md(create the file if missing) so future merges withupstream/mainknow about the patch. - Continue with the product work.
Upstreaming is for cases where keeping the patch local would mean solving the same problem in every future fork — i.e. it's a template-level concern, not a product-level one.