Imported from Kaushal-Rohit/canteen- (
AGENTS.md). Install upstream withnpx skills add Kaushal-Rohit/canteen-. Copyright stays with the author.
AGENTS.md — Cafe 11:11 Canteen SaaS
Read this before touching anything. If a rule here conflicts with an instruction in a session prompt, this file wins — stop and ask the human.
What this is
Multi-tenant prepaid ordering and kitchen-queue platform for college canteens.
QR → menu → cart → server-priced checkout → verified prepaid payment → token → kitchen → ready → pickup
Stack: Next.js (App Router) + TypeScript + Tailwind + shadcn/ui · Supabase (PostgreSQL, Auth, Realtime) · Vercel · Cloudflare · one payment gateway behind a PaymentService interface.
The product's core promise, and therefore the code's core constraint:
No successful payment = no confirmed order = no kitchen queue entry.
Authoritative documents — read before implementing
| File | Covers |
|---|---|
docs/ADR-001-deferred-identity.md |
Anonymous auth on load, OTP at checkout |
docs/ADR-002-split-state-machines.md |
payment_status and fulfillment_status |
docs/ADR-003-rpc-write-path.md |
Reads via RLS, writes via SECURITY DEFINER RPC |
docs/ADR-003a-service-role-lint-rule.md |
Service-role containment lint rule & AST selector |
docs/LICENSES.md |
Clean-room rules — read §4 before looking at any external repo |
lib/contracts/ |
Types, zod schemas, PaymentService interface |
supabase/migrations/ |
Schema of record |
These are the context. Do not ask for the original spec/feasibility/handoff PDFs to be pasted in — they are ~40k tokens and superseded by the ADRs wherever they disagree.
Non-negotiable rules
Violating any of these fails review regardless of whether tests pass.
Money
- All monetary values are integer paise, column type
int. Nevernumeric,float,decimal, ormoney. Never JS arithmetic on rupee floats. - Formatting to
₹happens only inlib/format/money.ts. - Never accept a price, total, discount, or amount from the client.
create_pending_ordertakes[{menu_item_id, qty, addon_ids[]}]; the server re-reads prices. The only exception isconfirm_paid_order'samount_paise, which comes from a signature-verified gateway payload and is used solely for an equality check.
Writes
4. No client-side writes to orders, order_items, or payments. Customers hold zero INSERT/UPDATE RLS policies on these tables. All writes go through the four RPCs in ADR-003. Do not add a fifth without a human decision recorded in that ADR.
5. Every SECURITY DEFINER function must set search_path = public, revoke execute from public/anon, and re-check tenancy internally — definer functions do not get RLS applied to their own queries.
6. SUPABASE_SERVICE_ROLE_KEY is read only in lib/db/admin.ts. Every function there takes canteenId as its first parameter. CI enforces this.
State
7. The cart lives in client state (localStorage + React). Do not create carts or cart_items tables. Do not persist a cart server-side.
8. A verified webhook is the only source of payment truth. Never trust a browser redirect, a client success callback, or a URL parameter. The return page polls/subscribes to database state.
9. fulfillment_status is NULL until payment succeeds. Only confirm_paid_order may write 'NEW' or set token_number.
10. Token allocation happens only via the queue_counters upsert inside confirm_paid_order. Never last_token + 1 computed anywhere else, and never on the client.
Identity & tenancy
11. canteen_id is on every business table and in every query predicate — including while there is only one canteen.
12. auth.uid() is present on every request (anonymous or identified). Do not write a second, unauthenticated code path.
13. Rate limits key on phone number and IP, never on user_id alone.
14. canteens.settings.require_otp_at_checkout is the only thing that varies OTP behaviour. No env-based or build-based branching.
Realtime 15. Realtime is a notification that state changed, never the state itself. On reconnect, refetch authoritative state from the database. The KDS shows a visible banner if no event or successful poll has occurred in 45s — kitchen staff must never silently read a frozen screen.
Sourcing
16. Do not copy code from any external repository. See docs/LICENSES.md §4. Never paste third-party source into a session. Never reference an external repo in a commit message.
Worktree file ownership
One agent per worktree. One worktree per module. One bounded task per session.
| Worktree / branch | Human owner | May edit | Must not touch |
|---|---|---|---|
agent/schema |
A | supabase/migrations/**, supabase/functions/**, supabase/seed.sql |
app routes, components |
agent/payments |
A | lib/payments/**, app/api/webhooks/**, app/api/checkout/**, lib/db/admin.ts |
UI, KDS, migrations |
agent/customer-ui |
B | app/(customer)/**, components/customer/** |
app/api/**, lib/payments/**, migrations |
agent/kds |
B | app/(staff)/**, components/kds/** |
api, payments, migrations |
agent/analytics |
B | app/(owner)/**, lib/analytics/** |
api, payments, migrations |
agent/tests |
either | tests/**, e2e/**, k6/** |
all application source |
Frozen — human-owned. Authorship, not keystrokes.
lib/contracts/** docs/ADR-*.md docs/LICENSES.md
AGENTS.md components/ui/** (shadcn primitives)
"Human-owned" means the content originates from a human or the architect seat and is
approved by a human before it is committed. It does not mean a human must physically type
the characters. The rule exists so that two agents cannot independently invent two
incompatible Order shapes — not to make the repository undeliverable.
Default: an agent may not create, edit, or delete these files. If a task appears to require it, stop, state what you need and why, and end the turn. Do not work around it with a local type.
Scribe exception — the only route by which an agent may write a frozen file. All four conditions must hold, and the agent must quote them in its report:
- A human has explicitly authorised this specific file in this specific task prompt.
- The prompt contains the complete intended content verbatim. An agent may never compose, summarise, infer, or improve the content of a frozen file.
- The agent transcribes it exactly. The only permitted deviations are mechanical fixes
required to compile or lint, each reported individually as
before → aftertogether with the error that forced it. - The change is committed on a branch other than
main, and a human reviews the diff before it is merged tomain. An agent maygit addandgit commita frozen file on a working branch; it may never merge one, force-push, or amend a commit a human has already reviewed.
Committing is not authoring. Staging and committing frozen-file content that is already on disk is version control, not authorship: conditions 2 and 3 do not apply to it. Condition 1 (explicit authorisation in the task prompt) and condition 4 (review before merge) do. An agent is therefore never blocked waiting for a human to run git — if authorised frozen-file content sits uncommitted in the working tree, commit it on a branch and say so in the report. Stalling on this cost three sessions before the rule was corrected; do not reintroduce the stall.
If any condition is unmet, the exception does not apply and the default stands. An agent that authors frozen-file content from its own judgement has committed the most serious process violation available on this project — see the ADR-003a incident, 2026-09-02.
git worktree add ../c11-payments -b agent/payments
git worktree add ../c11-customer -b agent/customer-ui
git worktree add ../c11-kds -b agent/kds
cd ../c11-customer && npm run dev -- --port 3001
Rules: rebase on main before every PR; branches live less than a day; migrations are serialised — only agent/schema creates migration files, one at a time (numbered filenames conflict trivially and messily).
Commands
npm run dev # :3000
npm run typecheck # tsc --noEmit
npm run lint
npm run test # vitest
npm run test:rls # two-tenant isolation suite — must pass before any merge
npm run test:payments # idempotency + concurrency suite
npm run e2e # playwright
supabase db reset # local: reapply migrations + seed
supabase gen types typescript --local > lib/db/database.types.ts
Conventions
- Server Components by default;
'use client'only where interaction requires it. - Route handlers validate input with a zod schema from
lib/contracts/schemas.tsat the boundary. No unvalidatedreq.json(). - Errors are typed codes from
lib/contracts/errors.ts, never raw strings. Never leak a Postgres error or constraint name to a client response. - Database constraint names are load-bearing (the error mapper and tests assert on them). Do not rename without updating both.
- Naming:
snake_casein SQL,camelCasein TypeScript, mapped at the data-access layer. - Timezone:
Asia/Kolkata.service_dateis derived from it — never from the client clock. - Phone-first. Design at 360px, then scale up.
Definition of done
[ ] typecheck, lint, unit tests, test:rls all green
[ ] zod validation at every new route boundary
[ ] no new client write to orders/order_items/payments
[ ] no money in float; no client-supplied price
[ ] canteen_id in every new query predicate
[ ] new/changed state transitions append order_status_history
[ ] tested at 360px if it has UI
[ ] PR body names the ADR it implements
Human-only, always
Never merged on one person's review, and never merged on an agent's assertion that its tests passed:
confirm_paid_orderand any SQL function touching money or tokens- webhook signature verification and idempotency
- every RLS policy
- anything reading
SUPABASE_SERVICE_ROLE_KEY - production migrations and secret handling
For these, the human runs the adversarial test by hand — double-click Pay, replay the webhook with curl, send the webhook before the redirect, query tenant B's data with tenant A's JWT. An agent saying "the test passes" is not evidence.