Accumulated decisions, completed work, and ongoing context. Read at the start of every session. Update at the end of significant sessions. Coding standards live in
AGENTS.md.
- ElitaleRestro — restaurant management app: orders + inventory + menu + tables + billing.
- Users: restaurant owners/operators (see
buyeragent) and floor/kitchen staff (seestaffagent). - Stage: early scaffold. Prisma 7 wired up; first domain model (
User= restaurant manager, phone-primary) with Zod validators + repository + service + a Vitest suite. Phone-first login UI in place (OTP backend not wired). Migration pending DB connectivity.
Full menu CRUD shipped (plan: .plan/menu-crud.md, planned with buyer + staff agents). Bottom-up, TDD.
- Schema: enums
GstRegistrationType/MenuItemType/DietaryType/Disable86Reason;Restauranttax profile (gstRegistrationTypedefault UNREGISTERED,serviceGstRate?,pricesTaxInclusive,gstin,sacCode); modelsMenuCategory,MenuItem,MenuItemImage,MenuItemAvailability(the 86 = audit log),MenuItemVariant,ModifierGroup,Modifier,MenuItemModifierGroup. Migrationadd_menuapplied. - Tax (corrected + optional):
resolveItemTax()— UNREGISTERED→no GST; served→outlet service rate; PACKAGED_GOODS→owngoodsGstRate+HSN; COMPOSITION→not-separately-charged/inclusive. GST is opt-in. - Availability:
isItemAvailable()computes fromisActive+ category + open 86; 86 has reason + who/when + optionalresumeAt(auto-return after time passes; UI shows it). Manager-only (no POS/KDS surface yet). - Variants + modifiers (half/full, add-ons) included in v1. Reusable modifier groups via join.
- Images: Supabase Storage (S3 via
@aws-sdk/client-s3, envSUPABASE_S3_*incl.SUPABASE_S3_SECRET_KEY),sharp→WebP ≤1600px, ≤3/item, ≤5 MB.lib/storage.ts+services/menu-image.service.ts. - Layers:
lib/validators/menu.ts; reposmenu-category/menu-item/menu-item-image/modifier-group/menu-availability; servicesmenu-item(tax+availability+getMenu→MenuDTO)/menu-category/modifier/menu-availability/menu-image;lib/manager-auth.ts(getManagerContextOrNull) +withManagerValidation;actions/menu.actions.ts. - UI:
/dashboard/menu(server) →components/menu/(menu-manager,item-dialog,category-dialog,eighty-six-dialog,modifier-groups-dialog,image-manager). shadcn (dialog/switch/textareaadded) +useServerAction+sonner. Dashboard sidebar refactored:dashboard/layout.tsxholds the shell; sidebar "Menu" →/dashboard/menu.next.config.tsallows*.storage.supabase.coimages. - Gotcha: enforced
react-hooks/set-state-in-effect— dialogs mount-only-while-open (state fromuseStateinitializers, no reset effect). Addingmanager-authimport tohelpers.tsrequired mocking@/lib/manager-authin existing action specs. - Tests: 146 pass (was 75). tsc + eslint clean.
- Deferred: channel pricing, combos, multi-outlet, bulk CSV, POS/KDS/aggregator 86 surfaces, field-level audit.
POS + order lifecycle + billing/settlement shipped (plan: .plan/pos-orders.md, planned with buyer + staff agents). Bottom-up, TDD. Discounts + comps + multi-tender included in v1 (user decision).
- Schema (
add_ordersmigration): enumsOrderType(DINE_IN/TAKEAWAY/DELIVERY),OrderStatus(OPEN/COMPLETED/VOID),OrderLineState(UNSENT/FIRED/SERVED/VOID),PaymentMode(CASH/UPI/CARD/OTHER),DiscountType(NONE/PERCENT/FLAT).Restaurant.nextInvoiceSeq Int @default(1). ModelsOrder(orderNumber per-restaurant,invoiceNumber?,idempotencyKey @unique, money Decimals, discount/comp/roundOff fields, void audit),OrderItem(snapshots name/variant/unitPrice/taxRate/taxKind/taxInclusive/state/isComp/sortOrder — never trust client prices),OrderItemModifier(snapshot),Payment. - Billing (
services/billing.ts, pure/deterministic, client-safe):computeBill(lines, discount?)— inclusive/exclusive tax back-out, comp lines free, proportional discount on pre-tax base, CGST=SGST split, round to nearest ₹1. Reused client-side for live cart/settle previews (imported into client components deliberately — it has no IO). - Layers:
lib/validators/order.ts(cart/create/addItems/fire/serve/void/settle;idempotencyKey min8; settle refine requires discount value when type set);repositories/order.repository.ts(settleOrderusesprisma.$transactionto atomically bumpnextInvoiceSeq+ write payments); servicesorder(snapshot fromgetMenu, idempotency, per-line fire)/settlement(recompute authoritative bill, verify tender ≥ grandTotal −0.5)/sales(getTodaySales);actions/order.actions.ts(withManagerValidation). - UI:
/dashboard/pos(components/pos/:pos-terminal,menu-item-grid,item-config-dialogvariant/modifier/qty/note,cart-line-list,use-order-carthook,types) — tap→configure→cart, order type + table/customer/delivery-address, live totals,Send to kitchen(clientcrypto.randomUUID()idempotency key) → toast w/ Print KOT action./dashboard/orders(components/orders/:orders-boardOpen/Completed + today's sales,order-detailadd-round/fire/serve/void-line/void-order/settle,settle-dialogdiscount + multi-tender + change due,add-items-dialogreuses POS grid,reason-dialog,print-button). Printable KOT ([id]/kot, no prices) + Invoice ([id]/invoice, GSTIN/CGST/SGST/round-off,?copy=1→DUPLICATE, BILL OF SUPPLY when unregistered) viawindow.print(); dashboard chrome hidden with@media printinglobals.css. - Sidebar: added POS + wired Orders →
/dashboard/orders. - Table settle (SHIPPED —
.plan/table-settle.md): the Open tab groups orders by table (tableId ?? label:… ?? solo:id); tables with 2+ open orders showSettle table (N · ₹total).TableSettleDialogsettles the combined bill with one split payment. Backend: validatorsettleTableSchema({orderIds[], payments[]}); reposettleManyOrders(extractedsettleWithinTx, one$transaction, sequential invoices, all-or-nothing); servicesettleTable+ pureallocatePayments(waterfall split across orders,tenderednot stored per slice — change shown in UI only), verifies combined tender ≥ total −0.5; actionsettleTableAction(withManagerValidation). Separate invoice per order, no discount in v1. Sharedcomponents/pos/payment-entry.tsx(usePaymentEntry+PaymentEntryFields) now backs both single + table settle (refactoredsettle-dialog.tsx). - Shared:
OrderLineDTO.taxInclusiveadded so client settle preview matches server billing;lib/format.ts(formatCurrency/formatDateTime/formatTime). - Tests: 193 pass (was 158). tsc + eslint clean. New specs: billing, order validators, order repo (+
$transactionmock), order/settlement/sales services, order actions. - Deferred: KDS screen, per-fire KOT diffing (KOT prints all active lines), reservations/tables, offline queue, refunds/reopen, split-by-seat, reprint tracking.
Dining-tables CRUD + POS dine-in picker (plan: .plan/tables.md, planned with buyer + staff agents). Both panels: a table list without occupancy awareness is "a prettier text box" — silently opening a 2nd order on an occupied table was the #1 abandon trigger.
- Schema (
add_tablesmigration):DiningTable(label unique per restaurant,seats?,section?,sortOrder,isActive, soft-deletedeletedAt).Restaurant.tables.Order.tableIdnullable FK +tablerelation +@@index([tableId])(real link,tableLabelkept as a snapshot). - Layers:
lib/validators/table.ts(+createOrderSchema.tableId);table.repository.ts(findTableByLabelvia compound unique,reviveTable, soft-delete setsisActive:false);table.service.ts(getTables/listTablesForManager,createTablerevives a soft-deleted label instead of erroring,TABLE_LABEL_TAKEN/TABLE_FORBIDDEN,resolveTableForOrder);table.actions.ts.order.service.createOrderresolvestableId→ authoritative label + ownership. - UI:
/dashboard/tables(components/tables/:tables-managergrouped by section + soft-delete confirm,table-dialog). POS Dine-in →components/pos/table-picker.tsx(section-grouped tappable buttons, occupied = amber dot; tapping an occupied table warns → Open existing order / New order anyway). Free-text table stays as fallback when no tables exist.lib/tables.ts#groupTablesBySectionshared by manager + picker. POS page derivesoccupied(tableId→openOrderId) fromlistOrders(["OPEN"]). - Sidebar: Tables →
/dashboard/tables. - Tests: 210 pass (was 193). tsc + eslint clean. New specs: table repo, table service (revive/ownership/label-clash), table actions, order.service
tableIdresolution. - Deferred: occupancy state machine (dirty/reserved/timers), table tabs/transfer/merge, reservations, floor-map, drag-reorder, bulk quick-add ("T1–T20"), multi-outlet clone.
Upgraded /dashboard/settings from a lone GST card into a full restaurant profile (plan: .plan/settings-profile.md, planned with buyer + staff agents). Agents' hard rules baked in: FSSAI licence prints on the bill (legal), brand name ≠ legal entity name (both needed; legal name on the GST invoice), PAN never printed, service options must actually drive the POS.
- Schema (
add_restaurant_profilemigration):RestaurantFormatenum;Restaurantgained legalName, tagline, brandColor, logoUrl, coverUrl, address (line1/2, state, postalCode), website + socials (instagram/facebook/google), restaurantFormat,cuisines String[], seatingCapacity, fssaiLicense, fssaiExpiry, panNumber,serviceDineIn/Takeaway/Delivery(booleans),businessHours Json. NewRestaurantImagemodel (gallery, cap 8). Logo/cover = fixed storage keys + cache-busted url; gallery = per-image key. Videos (follow-up):VideoKindenum +RestaurantVideomodel (external LINK or uploaded FILE, cap 6, profile-only); Server ActionbodySizeLimitraised to 30 MB innext.config.ts(default 1 MB would have blocked >1 MB image/video uploads). - Layers:
validators/restaurant.ts(updateProfileSchema— FSSAI\d{14}, PAN, hours ×7,≥1service option;businessHoursSchema);restaurant.repository(updateRestaurant, gallery CRUD);restaurant-settings.service(getRestaurantProfile/updateRestaurantProfile/getServiceOptions/fssaiStatus);restaurant-image.service(sharp→WebP + Supabase Storage: logo/cover/gallery);settings.actions(profile + FormData image actions via a sharedrunFileUploadhelper).lib/business-hours.ts+lib/restaurant-format.tsare client-safe. - UI:
components/settings/—profile-header(cover banner + circular logo + completeness chip),restaurant-profile-form(identity / location+contact / compliance / service+hours / details, one Save),logo-uploader/cover-uploader/gallery-manager/videos-manager(immediate upload viause-image-uploadhook; videos also accept links, YouTube/Vimeo embedded vialib/video.ts),business-hours-field,service-options-field. FSSAI expiry shows amber/red warning. Reuses the existingtax-settings-form. - Integrations: Invoice now prints logo (capped) + brand name + legal entity name + full address + FSSAI No. (+ existing GSTIN). POS order-type buttons are filtered to the enabled service options (cloud kitchen → delivery only); the owner-chosen default (Service options → “Default”, column
Restaurant.defaultOrderType, refine: must be an enabled option) is pre-selected in the POS, falling back to the first enabled type. - Tests: 228 pass (was 210). tsc + eslint clean. New specs: restaurant repo (profile + gallery), settings service (profile/fssaiStatus/serviceOptions), image service (sharp/storage mocks), settings actions. Gotcha: the 3
makeRestaurantfixtures (restaurant.repository/service, restaurant-settings.service specs) needed all new columns added. - Deferred: multi-outlet per-outlet GSTIN/FSSAI, multi-brand-per-kitchen, region-aware compliance, service-charge %, reservations, autosave, "open now" from hours.
Manual stock log + recipe/BOM auto-depletion (plan: .plan/inventory.md, planned with buyer + staff agents). Agents' spine: on-hand is a running total of typed movements, never directly editable; bulk grids are the adoption decider; auto-depletion never blocks a sale (on-hand may go negative). Owner chose the fuller build (recipes IN v1).
- Schema (
add_inventorymigration):StockUnit+StockMovementType(RECEIVE/WASTE/CORRECTION/SALE_DEPLETION) enums;StockItem(name, unit, category?,onHandDecimal(12,3), reorder/par/cost?, supplier free-text, soft-delete, unique[restaurantId,name]);StockMovement(signedquantity+resultingOnHandsnapshot, reason/note,orderId?,createdById);RecipeComponent(MenuItem↔StockItem, qty in stock unit, unique[menuItemId,stockItemId]). - Layers:
stock.repository(applyMovement/applyMovements= atomicincrement+ movement in$transaction;applyCounts= set-to-counted CORRECTION);stock.service(CRUD w/ opening-stock movement + name-revive, receive/waste/bulk/count,getLowStockCount,mapStock.isLow);recipe.repository/recipe.service(getRecipe/setRecipeComponent/removeRecipeComponent/listRecipesgrouped/getRecipesMap);stock-depletion.service(depleteForLines/restoreForLinesaggregate recipe×qty).order.serviceintegration: create/add-items → deplete; void-line/void-order → restore — all.catch(() => undefined)(best-effort, never blocks the order).inventory.actions+recipe.actions. - UI:
/dashboard/inventory(inventory-managergrouped by category + low badge;stock-item-dialog,adjust-dialogreceive/waste + reason chips,bulk-receive-dialoggrid,count-dialoggrid) + history route/dashboard/inventory/[id]. Recipe editor: a "Recipe" action per item in the menu manager →recipe-dialog(menu page now also fetcheslistStock+listRecipes). Dashboard shows an amber "N items low" banner → inventory. Sidebar Inventory wired. - Tests: 268 pass (was 241). tsc + eslint clean. New specs: stock repo ($transaction mock), stock service, recipe service, depletion service, inventory + recipe actions; order.service extended (mock depletion).
- Deferred: suppliers-as-entities + purchase orders + goods-receipt, variance reports, multi-outlet transfers, per-variant/modifier recipes, unit conversion (purchase≠stock unit), batch/expiry, stock→menu-86 auto-block.
src/app/— rootlayout.tsx/page.tsx/globals.css;login/(phone-first login);admin/—layout.tsx(admin-gated),page.tsx(overview),users/,restaurants/(+new/onboard)src/proxy.ts— Next 16 Proxy: optimistic auth gate; unauthenticated →/loginsrc/actions/—helpers.ts(withValidation,withAdminValidation),restaurant.actions.ts(+ specs)src/services/—user.service.ts,admin-user.service.ts(listUsers),restaurant.service.ts(onboardRestaurant,listRestaurants) (+ specs)src/repositories/—user.repository.ts(CRUD +findUsersPaginated,getUserAuthState),restaurant.repository.ts(+ specs)src/hooks/—use-server-action.ts;src/types/—index.ts(ActionResult/Paginated),admin.ts(DTOs)src/lib/—utils.ts(+serializeForClient),prisma.ts,admin-auth.ts,auth-helpers.ts,constants.ts,validators/{shared,user,admin}.tssrc/components/—login-form.tsx,phone-input.tsx;admin/(nav, shell, tables, onboard form);shared/(PageHeader,EmptyState);ui/shadcn primitives (button,input,label,separator,field,select)prisma/schema.prisma—UserRoleenum +User(role/suspendedAt/deletedAt) +Restaurantmodelprisma.config.ts— Prisma 7 config;src/generated/prisma/— generated client (gitignored)vitest.config.ts— node env,@alias,src/**/*.spec.ts(54 specs)- Not yet present: Resend (email) integration; POS/order/inventory modules. (DB live; migrations
init+admin+add_otp_challengeapplied.)
Next 16.2.10 · React 19.2.4 · TypeScript 5 · Tailwind v4 · shadcn/ui + @base-ui/react · lucide-react · ESLint 9 · React Compiler.
Data: Prisma 7.8.0 + @prisma/adapter-pg + pg · dotenv. Auth: twilio (SMS OTP) + jose (session JWT). Validation: zod 4.4.3 + libphonenumber-js. Tests: vitest 4.1.10 · tsx. DB: Prisma Postgres (db.prisma.io), migrations applied.
- Architecture: layered
UI → Actions → Services → Repositories → DB(details inAGENTS.md). - Data layer: PostgreSQL (Supabase) + Prisma 7, using the new
prisma-clientgenerator (outputsrc/generated/prisma, imported via@/generated/prisma/client) and thepgdriver adapter (required by Prisma 7's query compiler). Client singleton insrc/lib/prisma.ts; only repositories import it. Datasource url set inprisma.config.tsfromDATABASE_URL. - Testing: Vitest is the runner (
npm test). TDD mandatory; co-locate*.spec.ts. Unit-test services by mocking the repository module; unit-test repositories by mocking@/lib/prisma(vi.hoisted+vi.mock). - User model:
User= platform account. Phone is the primary identifier (unique, E.164);emailoptional/unique. Roles viaUserRoleenum (MANAGERdefault,ADMIN,SUPER_ADMIN) +suspendedAt/deletedAtsoft-delete.Restaurant= onboarded entity owned by a manager (ownedRestaurantsrelation). - Auth (wired): custom phone-OTP login.
requestOtpAction→ 6-digit code, HMAC-hashed (AUTH_SECRET) into anOtpChallengerow, SMS via Twilio (lib/twilio.ts, Programmable Messaging +TWILLIO_FROM_NUMBER).verifyOtpAction→ check code → find/create user →joseJWT session cookierestro_session(lib/session.ts).getCurrentUserId()/requireUserId()read the session; proxy checks the cookie (optimistic). Chose a custom session over Auth.js (simpler, fully server-action-native). No Twilio Verify (no Verify SID). - Docs wiring:
AGENTS.md= coding standards (source of truth);MEMORY.md= state/decisions.CLAUDE.mdimports both. - Icons:
@tabler/icons-reactis the intended set for feature code; scaffold + shadcn blocks currently shiplucide-react. - Fonts: body/UI = Inter (
--font-sans), headings = Outfit (--font-heading, auto-applied toh1–h6inglobals.css), mono = Geist Mono (--font-mono). Wired inlayout.tsx+@theme inline. (Fixed a bug where--font-sansself-referenced an undefined var → serif fallback.)
buyer.agent.md— "ICP Lie Detector": 6 restaurant-owner personas (independent single-location, multi-outlet QSR chain, cloud kitchen, fine dining, cafe/bakery, bar/pub) to stress-test features, UX, pricing, and marketing in restaurant math (covers, food cost %, pour cost, shrinkage, aggregator commission).staff.agent.md— "Floor & Kitchen Ops": 5 daily-operator personas (server, cashier, kitchen expeditor/KDS, floor manager, stores clerk) to evaluate flows for taps, batch actions, rush-readiness, and offline resilience.dharmendra.agent.md— senior architect / full-stack engineer (pre-existing, retained).- Removed the empty
va.agent.mdplaceholder (replaced by the restaurant-specificstaff.agent.md).
- Populated
AGENTS.mdas the coding-standards / architecture source of truth (preserving the managednextjs-agent-rulesblock). - Populated this
MEMORY.md.
- Installed Prisma 7.8.0 (
prisma,@prisma/client) +@prisma/adapter-pg,pg,dotenv. npx prisma initscaffoldedprisma/schema.prisma(prisma-clientgenerator →src/generated/prisma) andprisma.config.ts(datasource url fromDATABASE_URL); existing.envpreserved.- Added
src/lib/prisma.ts— global-cachedPrismaClientsingleton via thePrismaPgadapter (named export, strict TS, boundary check onDATABASE_URL). - Added npm scripts:
db:generate,db:migrate,db:deploy,db:studio, andpostinstall: prisma generate. Gitignored/src/generated; ESLint-ignoredsrc/generated/**. - Verified:
prisma generate✓,tsc --noEmit✓,eslint src✓. - Blocked: first migration not run — Supabase direct connection is IPv6-only and unreachable from this IPv4 network (
P1001). See Gotchas.
- Schema: added
Usermodel (id cuid,phoneunique,phoneVerifiedAt?,email?unique,emailVerifiedAt?,name?,isActive, timestamps);prisma generate✓. - Validators:
lib/validators/shared.ts(phoneSchemaE.164,emailSchemanormalised,idSchema,nameSchema) +lib/validators/user.ts(registerManagerSchema,addEmailSchema). - Repository:
repositories/user.repository.ts—createUser,findUserById/ByPhone/ByEmail,updateUser. - Service:
services/user.service.ts—registerManager(phone-unique),getManagerById/ByPhone,addEmailToManager(email-unique). Domain errors:PHONE_ALREADY_REGISTERED,EMAIL_ALREADY_IN_USE,USER_NOT_FOUND. - Tests (Vitest): added
vitest.config.ts+test/test:watchscripts. 29 specs across validators/repo/service.npm test✓,tsc✓,eslint✓. - UI: added shadcn
login-05block (npx shadcn add login-05) and adaptedcomponents/login-form.tsxto phone-first (ElitaleRestro branding,telinput, client-sidephoneSchemavalidation, “we’ll text a one-time code”). Removed email/social login. Submit handler has aTODOfor the request-OTP server action. - Blocked:
prisma migrate devstill failsP1001; theuserstable is defined + generated but not yet created in the DB.
- Added
src/proxy.ts(export function proxy+config.matcher). Optimistic cookie check (edge-safe, no DB) for the Auth.js session cookie (authjs.session-token/__Secure-authjs.session-token). - Behaviour: unauthenticated → redirect to
/login?callbackUrl=…; authenticated on a public route → redirect to/. Public routes:/login. Matcher excludesapi,_next/*, and static asset files. - Until Auth.js sets the session cookie, every non-
/loginroute redirects to/login(the requested default).tsc✓,eslint✓.
- Fonts: Inter (body) + Outfit (headings, auto-applied to
h1–h6) + Geist Mono. Fixed--font-sanswiring bug (self-referenced undefined var → serif fallback). Tab title → “ElitaleRestro”. phone-input.tsx: reusable phone field — shadcnselect(Base UI) country column (flag + dial code) + national numberInput, composing E.164 vialibphonenumber-js. Country is auto-detected from the visitor's IP (GeoJSget.geojs.io, client-side so it works in local dev), falling back to browser locale, thenIN. Emits E.164 to the parent via an effect (so async country changes propagate). Wired intologin-form.tsx.- Added deps:
libphonenumber-js, shadcnselect.tsc✓,eslint✓, visually verified (IP → India 🇮🇳 +91).
- Studied
/Users/soni/work/elitale/coldbirds/sequenceand ported its layered plumbing. Plan in.plan/admin-panel.md. - Schema:
UserRoleenum (MANAGER/ADMIN/SUPER_ADMIN) +User.role/suspendedAt/deletedAt; newRestaurantmodel (owner = User).prisma generate✓. - Shared plumbing:
types/(ActionResult,success/failure,Paginated),actions/helpers.ts(withValidation/withAdminValidation, Zod-schema-inferred),hooks/use-server-action.ts,lib/utils#serializeForClient. - Admin gate:
lib/admin-auth.ts(getAdminContextOrNull/requireAdminPage, DB-backed role check) overlib/auth-helpers.ts#getCurrentUserIdseam (fails closed in prod;DEV_ADMIN_USER_IDfor dev preview). - Data/logic:
repositories/{user (+ list/authState), restaurant},services/{admin-user (listUsers), restaurant (onboardRestaurant, listRestaurants)},actions/restaurant.actions.ts. Onboard = find-or-create owner by phone + unique slug. - UI:
/admin(shell + nav + overview counts),/admin/users(table),/admin/restaurants(table +/newonboard form viauseServerAction+PhoneInput). SharedPageHeader/EmptyState. - Tests: 54 specs (repos/services/actions/admin-auth/helpers/validators).
npm test✓,tsc✓,eslint✓. - Not live yet: needs the two blockers resolved. No new deps; icons = lucide (avoid per-feature mixing).
.envDATABASE_URLnow points at Prisma Postgres (db.prisma.io,sslmode=require) — reachable; migrationsinit+adminapplied. The Supabase IPv6 blocker no longer applies.- Added
prisma/seed.ts(idempotent upsert) +db:seedscript; installedtsxto run TS scripts. - Seeded Dharmendra Soni (
+917597365803,soni@elitale.com) asADMIN— idcmrq5aupg0000u96iwmkj0lyr.tsc✓,eslint✓.
- Schema:
OtpChallengemodel (phone, hashed code, expiry, attempts, consumedAt) + migrationadd_otp_challenge. - lib:
otp.ts(generate + HMAC-hash),twilio.ts(sendSms),session.ts(joseJWTrestro_sessioncookie: create/get/destroy).auth-helpers.tsnow session-backed (getCurrentUserId,requireUserId). - repo/service:
otp.repository.ts;auth.service.ts(requestOtp— 30s resend limit;verifyOtp— 5-attempt cap, 5-min TTL, find-or-create user). - actions:
auth.actions.ts—requestOtpAction,verifyOtpAction(sets session),logoutAction. - UI: two-step
login-form.tsx(phone → 6-digit code viauseServerAction),/dashboard(protected,requireUserId+ sign-out). Proxy checksrestro_session; signed-in users land on/dashboard. - Tests: 72 specs.
npm test✓,tsc✓,eslint✓. Live:/login200,/dashboard307 (redirects when signed out). DEV_ADMIN_USER_IDis set to the admin id →getCurrentUserId()returns the admin in dev even without a session.
- Add-email-later flow: UI + action calling
addEmailToManager(Resend for verification/notifications). - First operational module (Menu or Order/POS), bottom-up per
AGENTS.md.
-
/dashboard/staff— manager-only. Add/edit/remove staff grouped by role (Waiter / Kitchen / Management), avatar + status badge + employee ID + phone + PIN-set indicator. Sidebar wired (UsersIcon). -
Roles/status enums:
StaffRole(WAITER/KITCHEN/MANAGEMENT),StaffStatus(ACTIVE/ON_LEAVE/INACTIVE),EmploymentType,Gender. ModelStaff(migrationadd_staff): full profile — photo, name, phone, email?, address, DOB, gender, joining/employment, emergency contact, notes. No salary/bank/Aadhaar (deliberately cut).@@unique([restaurantId, employeeCode]); soft-delete viadeletedAt(revive on re-add of same code). -
PIN, not login: "password" = a 4–6 digit POS PIN, HMAC-hashed via
lib/staff-pin.ts(hashStaffPin(pin, restaurantId)=createHmac("sha256", AUTH_SECRET).update(${restaurantId}:${pin})). PINs are shareable across staff (revised 2026-07-19, migrationstaff_shared_pin): the@@unique([restaurantId, pinHash])constraint,findStaffByPinHashlookup, andSTAFF_PIN_TAKENguard were all removed. Trade-off: a PIN can't identify who entered it — revisit if PIN login / attribution lands in v1.1. Manager phone-OTP stays the only real login; staff never log in. DTO exposeshasPin, neverpinHash. -
Layers:
staff.repository→staff.service(create/update/delete/resetPin, code-taken + pin-taken guards, revive-if-soft-deleted) +staff-image.service(sharp → 512×512 webp →staff/{id}/photo.webp, cache-busted url). Actionsstaff.actions.tsviawithManagerValidation; photo upload/remove are FormData actions (need an existingstaffId, so photo uploader shows only in edit mode). Validators inlib/validators/staff.ts(pin/^\d{4,6}$/). Attribution (per-staff order/void) deferred to v1.1.
-
Goal: managers set a PIN on
/dashboard/settingsto skip the SMS OTP on repeat logins. Owner-chosen model: phone + PIN from anywhere (weaker; hardened with compensating controls). OTP always stays as fallback. -
Login is phone-first + auto-routed (no method chooser):
startLoginAction({phone})→startLogin()inauth.service.tsreturns"pin"(phone has a usable PIN) or"otp"(sends code). Login form (login-form.tsx) has 3 steps: phone → pin (Forgot PIN? / lockout → OTP) → code. Removed the old debuguseEffect(console.log). -
Model:
UsergainedpinHash/pinUpdatedAt/pinFailedAttempts/pinLockedUntil(migrationadd_manager_pin, additive). Hashing = scrypt + per-user salt vialib/pin.ts(scrypt$salt$hash,node:crypto, constant-time) — NOT the deterministic staff HMAC (login PIN needs salt + slow KDF). -
Service
pin-auth.service.ts:setManagerPin/removeManagerPin/getPinStatus/verifyPinLogin. Lockout:MAX_PIN_ATTEMPTS=5→pinLockedUntil = now+15min(PIN_LOCKED); wrong PIN = genericPIN_INVALID(no enumeration). Successful OTP login resets counters (unlocks).verifyPinLoginre-checks eligibility (deleted/suspended/inactive) every time. -
Actions
pin.actions.ts:startLoginAction,verifyPinAction(→createSession),setPinAction/removePinAction(auth viagetCurrentUserId, throw/returnNO_SESSION). ValidatorsmanagerPinSchema/setPinSchema/verifyPinSchemainlib/validators/auth.ts. -
Settings UI:
sign-in-pin-card.tsx(status + Set/Change/Remove, no re-auth) +pin-dialog.tsx(PIN + confirm, masked). Page passesgetPinStatus(ctx.userId). - Accepted residual risk: phone+PIN-anywhere reveals PIN-presence via the router + has no possession factor. §12 upgrade path = device-binding + SMS-on-PIN-login alert (keep modular).
Restaurant.username String? @unique(migrationadd_restaurant_username, nullable-unique so additive; Postgres allows multiple NULLs). Distinct fromslug.- Auto-generated 7-char lowercase alphanumeric via
lib/username.ts(generateUsername,randomInt).generateUniqueUsername()inrestaurant.service.ts(loops vsfindRestaurantByUsername, mirrorsuniqueSlug). Set at onboarding; lazily generated + persisted on firstgetRestaurantProfilewhen null (resolveUsername). - Settings UI:
components/settings/username-card.tsx(input + Save + "Generate new");updateUsernameAction(unique-checked,USERNAME_TAKEN) +regenerateUsernameAction. ValidatorusernameSchema=/^[a-z0-9_]{3,20}$/(lowercased). DTO gainedusername.
/u/[username]/login— restaurant-scoped (username =Restaurant.username). Waiter/kitchen sign in via a name/photo picker → PIN pad (listLoginStafflists ACTIVE waiter/kitchen; tapping a tile supplies itsemployeeCodebehind the scenes — login action/service still take employeeCode+pin).MANAGEMENTexcluded; manager OTP login untouched. Tradeoff: the waiter/kitchen roster is visible on the public login page. Landing/u/[username]is a minimal role stub (POS/KDS = fast-follow).- Separate session:
lib/staff-session.ts→restro_staffjose JWT (sub=staffId,restaurantId,role), 12h, httpOnly. Fully independent of the managerrestro_session.lib/staff-auth.tsgetStaffContextOrNull()re-reads the Staff row every request (instant deactivation) + enforces ACTIVE + role∈{WAITER,KITCHEN} + session.restaurantId === staff.restaurantId. - Service
staff-auth.service.ts:verifyStaffLogin(restaurantId, employeeCode, pin)(useshashStaffPin; genericSTAFF_LOGIN_INVALID; lockoutMAX_STAFF_ATTEMPTS=5→ 60sSTAFF_LOGIN_LOCKED) +getStaffLoginRestaurant(username). Staff lockout colsloginFailedAttempts/loginLockedUntil(migrationadd_staff_login_lockout) + reporecordStaffLoginFailure/resetStaffLoginCounters. - Actions
staff-auth.actions.ts:staffLoginAction(resolve restaurant by username → verify →createStaffSession) +staffLogoutAction(username). ValidatorstaffLoginSchema(username+employeeCode+pin) inlib/validators/staff.ts. - Proxy:
/u/**routed onrestro_staff(separate from manager cookie);/u/[username]/loginpublic, other/upages require the staff cookie. UI:app/u/[username]/login+page(gated),components/staff-login/staff-login-form.tsx(numeric PIN pad) +staff-home.tsx. - Accepted residual (agents): shared PINs ⇒ identity comes from Employee ID (login attribution fine); v1.1 = waiter-POS/kitchen-KDS destination, name/photo grid on provisioned device, station-mode KDS, unique PINs for void/comp accountability, offline.
- The work screen behind the waiter login.
/u/[username](WAITER) = open-tables home (WaiterHome, lists OPEN orders) + New order./u/[username]/order/newand/order/[orderId]=OrderBuilder(mobile: search + category chips + one-tap items,ItemConfigDialogreused for variants/modifiers, pinned bottom cart bar + review sheet, optional after-the-fact phone, table grid for dine-in). Kitchen still gets the stub. - Reuses the POS logic wholesale:
order.servicecreateOrder/addItems+fireOrder/listOrders/getOrder,getMenu,getTables,getServiceOptions,useOrderCart+CartLine/toBillLine/computeBill. Send = fire-all, idempotent (idempotencyKey) for resilient retry (D1). - Attribution (D3):
Order.placedByIdwidened to nullable + newOrder.placedByStaffId(migrationadd_order_staff_attribution; also widenedPayment.receivedById+StockMovement.createdByIdto nullable so a null-userId waiter actor flows through).OrderContextgaineduserId: string | null+ optionalstaffId; waiter create writesplacedByStaffId,placedById=null. - Auth:
withStaffValidation(schema, handler, { role })inactions/helpers.ts(mirrors manager wrapper;NO_STAFF_SESSION/STAFF_FORBIDDEN). Actionsactions/staff-order.actions.ts:createWaiterOrderAction+addWaiterItemsAction(both{ role: "WAITER" }). Waiter has zero price/void/comp/settle powers (D4/D5) — those actions staywithManagerValidation; removing an unsent cart line before Send is the only "edit". - Gotcha:
helpers.tsnow imports@/lib/staff-auth→ every action spec that mocks manager/admin-auth must alsovi.mock("@/lib/staff-auth", () => ({ getStaffContextOrNull: vi.fn() }))(else it loads real prisma). - v1.1 (agents): true offline-first, kitchen KDS (add-on tickets flagged), coursing, split-by-seat, floor map, bar tabs + waiter payment, per-line attribution, tableside manager-PIN void.
- The kitchen work screen behind the KITCHEN login.
/u/[username](KITCHEN) =KitchenDisplay— a single vertical list of ticket cards (one per OPEN order with active kitchen lines), oldest-fired first. Each card: title (table/takeaway), elapsed timer, status pill, item lines (variant · modifiers · "note"), a flagged + Added group for add-ons, and one big advance button. ~10s polling viarouter.refresh()in auseEffectinterval (no websockets). Replaced the oldStaffHomestub. - Lifecycle (whole-ticket, one tap): line states extended
UNSENT → FIRED(=Waiting) → PREPARING → PREPARED → SERVED → VOID(migrationadd_line_prep_states, additive enum). Order kitchen status is derived, never stored: all active FIRED → WAITING; all PREPARED → READY; else PREPARING; all SERVED/VOID → off board. Advance is deterministic: any FIRED → button "Start" (FIRED→PREPARING); else PREPARING present → "Mark ready" (PREPARING→PREPARED); all PREPARED → no button (Ready for pickup). A fired add-on re-triggers "Start". - Add-on batching:
lineCreate(order.repository) now stamps onefiredAtper fire batch (was per-item). Tickets group active lines by exactfiredAt; earliest batch = original, later =isAddOn: true(rendered "+ Added"). - Waiter clears Ready: waiter taps Picked up on the add-items screen when a ticket is READY →
markPickedUpAction(role WAITER) →advanceLineStates(orderId, PREPARED, SERVED)→ lines leave the board. Kitchen cannot void/comp/price — status only. - Layers:
lib/kitchen.ts(client-safe pure:deriveKitchenStatus,kitchenAdvanceLabel, labels) →repositories/order.repository#advanceLineStates(orderId, from, to)(bulkupdateMany) →services/kitchen.service.ts(listKitchenTickets/advanceTicket/markPickedUp, reusesloadOwnedOrder) →actions/kitchen.actions.ts(advanceTicketAction{role KITCHEN},markPickedUpAction{role WAITER} viawithStaffValidation+kitchenTicketSchema). Types intypes/kitchen.ts(KitchenTicketDTO/Batch/Line); DTOOrderLineStatewidened. - Status visibility: shared
components/shared/kitchen-status-badge.tsx(KitchenStatusBadge, renders nothing when no active lines) on waiter home rows, manager open-tickets rows, and the waiter add-items header. - Tests: +26 specs (kitchen lib/service/actions), full suite 390 green.
tsc✓,eslint src✓. Route smoke:/u/x307 → login. - v1.1 (agents): station routing (hot/cold/bar), per-item bump, KDS delay nudge to waiter, station-mode kiosk, sound/flash on new ticket, offline queue.
- Public guest ordering at
/order/[username]?table=[tableCuid](username =Restaurant.username; table =DiningTable.id). Planned with buyer + staff agents; bottom-up TDD. Dine-in only, v1. - Owner decisions (locked, override of agents): direct-to-kitchen, NO staff-accept gate (agents strongly recommended a gate — accepted risk, documented v1.1 upgrade); auto-create OPEN order when the table has none (else append + fire); OTP once per table session; full menu (no category exclusion); SELF-ORDER badge on boards (not on KOT/invoice).
- Flow: guest opens link →
loadGuestOrderPagevalidates restaurant +selfOrderEnabled+ table (friendly fallbacks for disabled/invalid,notFound()for missing) → browse menu (getMenu) + build cart (reusesMenuBrowser,ItemConfigDialog,useOrderCart,computeBill) → Place order → phone + SMS OTP (skipped if already verified this seating) → order fires straight to kitchen. - Guest OTP (no user row):
services/guest-otp.service.ts(requestGuestOtp/verifyGuestOtp) reuses theOtpChallengetable +lib/otp+ TwiliosendSms, but never looks up/creates aUser(the managerauth.serviceOTP is gated to registered users — can't reuse). 30s resend, 5-attempt cap, 5-min TTL. - Verified-phone binding:
lib/guest-session.ts— joserestro_guestcookie scoped to{restaurantId, tableId, phone}, ~3h TTL, httpOnly (mirrorsstaff-session).guestPlaceOrderActionrequires it and re-checks cookie restaurant+table === submitted (a photographed QR for another table can't submit). - Order attribution: additive migration
add_self_order— enumOrderSource {STAFF, SELF_ORDER},OrderItem.source @default(STAFF)(per-line, so guest items appended to a waiter order are still badged),Restaurant.selfOrderEnabled @default(false). Threaded viaOrderContext.source→snapshotLines→ repoOrderLineWriteData.source; existing waiter/POS callers default to STAFF (no change). DTOOrderLineDTO.sourceadded; kitchenKitchenTicketBatch.isSelfOrdercomputed per fire-batch. - Layers: validators
lib/validators/guest-order.ts(request/verify/place, reusescartLineSchema);services/guest-order.service.ts(resolveGuestOrderTarget,placeGuestOrderappend-or-create withsource: SELF_ORDER,loadGuestOrderPagediscriminated union); actionsactions/guest-order.actions.ts(all public viawithValidation); pageapp/order/[username]/page.tsx; clientcomponents/guest-order/guest-order-page.tsx(phone viaPhoneInput, idempotency vialib/uuid). Proxy:/orderadded toPUBLIC_ROUTES. - Badge: shared
components/shared/self-order-badge.tsx(violet pill) on KDS batch header, waiter-home rows, manager orders-board cards. Settings toggle:components/settings/self-order-card.tsx(Switch) +setSelfOrderEnabledAction+ serviceget/setSelfOrderEnabled; off by default. - Table QR sharing (
/dashboard/tables): each table row has a Share button →components/tables/table-share-dialog.tsx(copy link, scannable QR, Download QR PNG for printing, Preview in new tab). Link =window.location.origin/order/<username>?table=<tableId>(client-side origin). QR viaqrcodedep (QRCode.toDataURL, 1024px) rendered throughnext/image unoptimized; downloaded via a base-uiButton render={<a download>}. Page passesusername+selfOrderEnabledfrom new lean servicegetSelfOrderShareInfo(restaurantId)(reusesresolveUsername); dialog shows an amber "turn it on in Settings" hint when disabled. Gotcha: readwindow.location.originvia auseState(() => …)initializer, NOTsetStateinuseEffect(the enforcedreact-hooks/set-state-in-effectrule errors). - Session persistence + new-order voice alerts (SHIPPED —
.plan/self-order-alerts-and-persistence.md):- Guest cart persists in localStorage (
lib/guest-cart-storage.ts, keyrestro.guestsession.<username>.<tableId>): saves cart draft + verified marker, ~3h TTL (matches cookie), cleared on successful order.guest-order-page.tsxrestores via arequestAnimationFrame(() => …)effect gated by arestoredRef(so the persist effect never clobbers the saved draft with the empty mount state); persists on cart/verified change.useOrderCartgainedreplaceAll(lines)for restore (POS/waiter unaffected). Place failingGUEST_NOT_VERIFIEDnow re-opens the OTP dialog instead of dead-ending. Server cookie stays authoritative for verification — localStorage is convenience only. - New-order TTS on both KDS + manager
/dashboard/orders, 10srouter.refresh()polling +SoundToggle(default ON). Shared pure helper inlib/announce.ts:newOrderAlerts(prevMap, sigs)diffs per-order active SELF_ORDER line counts → flags brand-new orders and guest add-ons (self-order count grew) while staying silent on staff add-ons;alertSignatureMapseeds the next diff;selfOrderAlertPhrase()= "Naya self order, T5". Manager board announces all new orders (self → distinct phrase, staff → genericnewOrderPhrase); KDS same, replacing its oldnewIdsorderId-only detection so guest add-ons to an existing ticket now speak. - Tests: +15 (announce, guest-cart-storage), suite 466 green. tsc/eslint/
next build✓.
- Guest cart persists in localStorage (
- Self-order page — verified badge, photo carousel, my-orders (SHIPPED —
.plan/self-order-photos-verified-history.md):- Verified badge: green "✓ Verified · ••210" pill in the guest header when verified. Page masks the cookie phone server-side (
maskPhoneinlib/format.ts→ last 3 digits); after in-session OTP the client masks its ownphonestate. - Menu photos + carousel: new
components/shared/image-carousel.tsx(emblaembla-carousel-react, swipe + dots,next/image; single image = no dots; avoid synchronoussetStatein the embla effect — subscribe to"select"only, initial index 0).ItemConfigDialoggained a carousel (images sorted primary-first) +shortDescription.MenuBrowserreworked into photo cards: a tap-the-card area (onOpenDetail) + a "+" quick-add button (onQuickAdd) — two sibling buttons (no nested interactive elements). Applied to both guest + waiter (OrderBuilder+GuestOrderPageupdated to the newonQuickAdd/onOpenDetailAPI).useOrderCartunchanged besides the earlierreplaceAll. - "Your orders" + live status: repo
findOrdersForGuest(restaurantId, phone, tableId, limit)(OR phone/table, recent, cap 15) → servicegetGuestOrders→ PII-freeGuestOrderSummaryDTO(id/number/time/table/status/kitchenStatus/itemCount/total/lines — no phone/name/address, since a table match could expose a previous occupant). Completed orders use the settledgrandTotal; open orders compute a running total. ActionguestMyOrdersAction()(no input; reads therestro_guestcookie →getGuestOrders; secure — client never supplies the phone). Page fetchesinitialOrderswhen verified;GuestOrderPageshows a "Your orders (N)" header button → bottom Sheet with per-order lines + a status pill (In queue / Preparing / Ready / Served / Paid / Cancelled), polled every 10s via the action (no menu re-fetch); refetches after verify + place; confirmation screen gained a "Track your orders" button. - Tests: +8 (maskPhone, findOrdersForGuest, getGuestOrders, guestMyOrdersAction), suite 474 green. tsc/eslint/
next build✓. Menu-item photos reuse the existing Supabasenext/imageremote-pattern config.
- Verified badge: green "✓ Verified · ••210" pill in the guest header when verified. Page masks the cookie phone server-side (
- Timezone + guest logout fixes (2026-07-20):
- All times pinned to IST.
lib/format.tsformatDateTime/formatTimenow passtimeZone: "Asia/Kolkata"(constTIME_ZONE). RSC render server-side (UTC) — without this, times showed UTC and could hydration-flash. Pinning makes server + client agree. Every time display routes through these two helpers (orders board, KOT, invoice, order-detail, inventory history, guest my-orders), so this one change fixes them all. (Multi-region later = threadRestaurant.timezone.) - Guest session = 2h + logout. Guest cookie TTL 3h→2h (
lib/guest-session.ts);getGuestSessionnow also returnsexpiresAt(from JWTexp×1000) —createGuestSessioninput isOmit<…,"expiresAt">. localStorageguest-cart-storage.tsTTL 3h→2h + storesexpiresAt;parseGuestSessionclearsverifiedonceexpiresAtpasses (cart activity can't keep a stale login alive). NewguestLogoutAction(destroys cookie).GuestOrderPage: "Log out" button in the header when verified (resets state + clears localStorage + cookie), and an auto-logout timer (setTimeoutatexpiresAt, deferred so no synchronous setState-in-effect) that logs out + toasts "Session expired". Page passesverifiedExpiresAtfrom the cookie; in-session verify setsexpiresAt = now + 2h. - Tests: +4 (format IST, storage expiry, logout action), suite 478 green. tsc/eslint/
next build✓.
- All times pinned to IST.
- Manager dashboard analytics (SHIPPED —
.plan/dashboard-analytics.md): replaced the 4 empty placeholder cards on/dashboard(deletedcomponents/section-cards.tsx) with real day + month analytics, auto-refreshing every 10s. Planned with buyer + staff agents.- Agent-driven decisions: killed the fake "Covers" card (0/6 owners accept a pax proxy — trust-killer) → replaced with a live "Open now" card (open orders count + ₹ open value + oldest-age). Payment mix is the trust anchor (must sum to today's sales). Split into quiet glance cards + a muted detail/trust row (tax·discounts·voids·order-type) that owners distrust if absent but don't want as hero cards. Deferred (agents flagged): per-outlet grid, per-brand/channel, true pax covers, comps/voids by employee.
- Layout: Today row (Sales +Δyest · Orders +AOV · Open now · Payments) → Month row (Month sales +Δlast-month · Month orders +AOV · Tables seated occupancy) → daily sales bar chart (recharts via
components/ui/chartChartContainer) → detail (top items today · muted GST/discounts/voids + order-type chips).AutoRefreshclient =router.refresh()every 10s, numbers update quietly. - Data:
services/dashboard.service.ts#getDashboard(restaurantId, now?)fetches month settled orders once + open orders + tables + today voids + last-month aggregate, derives everything in-memory. All day boundaries are IST via pureistParts/istMidnight(India has no DST → fixed +5:30; fetches sincemin(monthStart, yesterdayStart)so "yesterday" works on the 1st). Repo addedaggregateSettledBetween(Prismaaggregate_sum/_count, for the last-month delta — no row fetch). Open value via purecomputeBill; occupancy from open orders with atableId. Types intypes/dashboard.ts.nowparam makes the service deterministically testable. - Perf note: the month query re-runs every 10s (router.refresh re-runs the whole RSC) — fine for single-outlet v1; later optimization = poll only the live parts, refresh MTD/trend less often.
- Tests: +6 (repo aggregate,
istParts/istMidnight,getDashboardderivation + empty-day), suite 484 green. tsc/eslint/next build✓.
- Thermal-receipt invoice + custom footer (2026-07-20): rebuilt
app/dashboard/orders/[id]/invoice/page.tsxto a 58/80mm thermal-receipt look (font-mono,text-black,max-w-[320px], dashedHrdividers, centered header). Header order matches a real bill:TAX INVOICE/BILL OF SUPPLY→ legalName → brand name → tagline → address →M: phone→GSTIN:→FSSAI:— each conditionally hidden when absent (per request). Meta: Name (if present) · Date/Type+table · time · Bill No (invoiceNumber) · Token No (orderNumber). Item table = Item · Qty · unit Price · Amount. Totals: Sub Total+Qty · SGST/CGST (shows@x%only when all taxable lines share one rate) · Round off (signed) · Grand Total (bold, ₹). Dates via inline ISTen-GBformatters (dd/MM/yy + 24h). Payment breakdown dropped to match the receipt.- Custom footer note: additive
Restaurant.invoiceFooterNote String?(migrationadd_invoice_footer_note). Owner edits it in Settings viacomponents/settings/invoice-footer-card.tsx(Textarea, ≤300 chars) →setInvoiceFooterAction→ serviceget/setInvoiceFooterNote(blank ⇒ null). Invoice printsinvoiceFooterNoteor falls back to "Thank you! Visit again." Gotcha (recurring): after a Prisma migration,migrate devhere did NOT regen the client — runnpx prisma generatebefore tsc, and add the new column to the 3makeRestaurantfixtures. - Tests: +3 (service get/set footer, action), suite 487 green. tsc/eslint/
next build✓.
- Custom footer note: additive
- Tests: +41 specs, full suite 449 green.
tsc✓,eslint src✓,next build✓ (route/order/[username]dynamic). Gotcha: a"use server"file may export only async functions — exporting a plainconst GUEST_NOT_VERIFIEDstring invalidated all exports (build error "module has no exports"); made it a non-exported local. Also the 3makeRestaurantfixtures neededselfOrderEnabled. - v1.1 / deferred: the staff-accept gate (agents' #1 ask), per-batch table reassign on accept, takeaway path, marketing-consent DB, alcohol/category exclusion, append-path idempotency, guest live status, pay-now.
AGENT.mdvsAGENTS.md:.github/copilot-instructions.mdsays "AGENT.md", but the live file imported byCLAUDE.mdisAGENTS.md. TreatAGENTS.mdas the source of truth.- copilot-instructions template drift:
.github/copilot-instructions.mdwas adapted from the coldBirds project (its title still reads "coldBirds"). Some rules there are coldBirds-specific and do not apply here (e.g., Cloudflare/Resend SDKs, cold-email domain).AGENTS.mdreflects the restaurant project's actual conventions. - Next.js 16: read
node_modules/next/dist/docs/before writing Next code — APIs differ from training data. Notablymiddleware.tsis renamed toproxy.ts(export function proxy(request: NextRequest), optionalconfig.matcher); the oldmiddlewareconvention is deprecated. - Never hard-reset the database once one exists — it wipes NextAuth session rows and causes redirect/reload loops in dev. Prefer additive migrations; ask before any destructive change.
- Supabase direct connection is IPv6-only.
db.<ref>.supabase.co:5432has no IPv4Arecord →P1001on IPv4-only networks. Use the Supavisor pooler instead (usernamepostgres.<project-ref>, hostaws-0-<region>.pooler.supabase.com): session pooler:5432for migrations, transaction pooler:6543?pgbouncer=truefor the serverless app. Copy the exact string from Supabase → Project Settings → Database → Connection string (region appears to beap-south-1). Also confirm the project isn't paused (free tier auto-pauses). - Prisma 7 specifics: client is generated (not shipped in
@prisma/client) tosrc/generated/prismavia theprisma-clientgenerator; import from@/generated/prisma/client(model type isUser, aliased fromPrisma.UserModel). The query compiler requires a driver adapter (@prisma/adapter-pg) —new PrismaClient()won't connect without one.prisma.config.tsloads env viadotenvand holds the datasource url. - Notification/auth env vars (exact names): Resend →
AUTH_RESEND_KEY,RESEND_FROM_EMAIL(still a coldBirds/taxfromaddress — update before sending). Twilio →TWILLIO_A_SID,TWILLIO_AUTH_SECRET,TWILLIO_PRIMARY_TOKEN,TWILLIO_FROM_NUMBER(note the misspelledTWILLIOprefix — use verbatim or rename the vars). AlsoAUTH_SECRET,AUTH_URL,AUTH_TRUST_HOST,CRON_SECRET. - Twilio SMS: sending from the US
TWILLIO_FROM_NUMBERto Indian (+91) numbers is authorized — no DLT caveat. Real OTP is sent in every environment (dev + prod):requestOtpalways calls Twilio with noNODE_ENVgate; only the Vitest suite mockssendSms(automated tests must never fire real SMS).AUTH_SECRETis reused to HMAC OTP codes and sign therestro_sessionJWT.