Skip to content

16 — Backend Cutover Plan & Readiness Checklist

Status: 📐 The plan for leaving the frontend-only phase. Nothing here has started. Read 15 for where code goes, 07 for the schema, 14 for Supabase. This doc is what order, what replaces what, and how we know each step is done.

🟢 বাংলা ভূমিকা: ফ্রন্টএন্ড-অনলি পর্ব থেকে বেরোনোর পরিকল্পনা — কোন ক্রমে কাজ হবে, আজকের কোন localStorage key বা mock ফাংশন কোন টেবিল/কোয়েরি দিয়ে বদলাবে, আর প্রতিটা ধাপ "শেষ" কখন বলা যাবে। কোড কোথায় যাবে তা ১৫-তে, স্কিমা ০৭-এ, Supabase ১৪-তে।


1. Readiness — what is already true before day one

PrerequisiteStateEvidence
Every screen designed and walked in a browser✅docs/feature-lists.md re-audited against code; 226 Playwright journeys green
Every page reads through one async layer✅src/lib/data/{tenant,platform}.ts — pages already await
Every write goes through one hook per entity, with a stable interface✅src/hooks/use-* (29 hooks, §3 maps each)
Pure business logic separated from UI✅src/lib/order-totals.ts, allergy-filter.ts, packages.ts, package-schedule, feature-resolution.ts, table-session.ts, staff-invite.ts, branch-draft.ts, signup.ts, custom-domain.ts — all unit-tested
Domain types are the contract✅src/types/* — mappers must produce exactly these
Schema designed and machine-checked✅docs/backend/schema.prisma — prisma validate clean, 49 tables
Security SQL written and exercised✅docs/backend/supabase/policies.sql — isolation suite passed on Postgres 17 (docs/14 §8)
Every UI string translated, incl. errors-to-be🟡1815 keys at parity; the errors.<code> keys (docs/15 §6) get added with the first action
Accounts, secrets, domains❌§4 — needs the owner of the business, not code

🟢 বাংলা সারাংশ: প্রথম দিনের আগেই যা তৈরি: সব স্ক্রিন ব্রাউজারে যাচাই করা, সব পেজ একটা async লেয়ার দিয়ে পড়ে, প্রতিটা entity-র জন্য একটা hook, pure লজিক আলাদা ও টেস্ট করা, স্কিমা prisma validate পাস, সিকিউরিটি SQL Postgres 17-এ পরীক্ষিত। বাকি: অ্যাকাউন্ট, সিক্রেট আর ডোমেইন — এগুলো কোড না, ব্যবসার মালিকের কাজ।


2. Order of work

Each phase ends deployable. The site never has a "half on localStorage, half broken" state in production: a phase ships behind nothing but its own completeness, because until a hook is cut over, it keeps working exactly as today.

#PhaseScopeDone when
0FoundationAdd prisma, @prisma/client, @supabase/ssr, @supabase/supabase-js, zod, server-only, argon2. git mv docs/backend/schema.prisma prisma/; supabase init; copy policies.sql into supabase/migrations/. src/server/{db,auth,http} skeleton. .env.example → .env.local. CI gets a Postgres service.prisma migrate dev + security migration apply on a clean local stack; the isolation suite (docs/14 §8) runs in CI and passes
1Read cutoverprisma/seed.ts loads every mock array with its mock id. Each src/lib/data function body becomes a repo call inside withTenant/withPlatform.All 226 E2E journeys pass against the seeded database with zero page changes
2AuthSupabase email auth for owners/managers/platform; staff invites become real (email + token); shift PIN sign-in; requireStaff/requirePlatform on every dashboard/staff route; delete SessionGuard and ros-sessionSigned-out → /login, enforced on the server; a Swadh Ghar manager editing the URL to another tenant gets 404; signing-in E2E suite rewritten to real accounts
3The core loopsessions (scan → sitting, calls), orders.placeOrder/advanceStatus, Realtime on orders + table_calls, cart submit → action with clientRequestIdGuest on a phone orders; kitchen tablet sees it within 1 s without reload; waiter call arrives; retrying a submit yields one order; an old QR print is refused
4Owner writesmenu, packages, floor/QR, staff, branches, promos, tenant branding/domain, uploads to StorageEvery dashboard board persists across devices; ros-* keys for those entities no longer read (grep proves it)
5Moneypayments (cash/card-terminal first, then bKash/Nagad/Stripe webhooks — docs/08), refunds, loyalty earn/redeem, receipts from DBA paid order's Payment row exists; a replayed IPN changes nothing; refund produces its own row
6Platformplans, invoices (+ PDF to invoices bucket, email), VIP, feature overrides, support, audit, platform usersEvery platform-admin board persists; tenant sees its own invoices only
7Operationsinventory + BOM decrement on completion, reservations, CRM segments, notifications dispatch, analytics as SQL aggregates, cron jobsStock drops when an order completes; nightly jobs idempotent
8Custom domains liveVercel Domains API on requestDomain, verify-domains cron, proxy lookup with cacheA real customer domain serves its menu with TLS; the pending state is visible to the owner until then (docs/13 §6)
Flow diagram

Phases 4, 6 and 7 can run in parallel between people; 0 → 1 → 2 → 3 cannot — each is the ground the next stands on.

🟢 বাংলা সারাংশ: প্রতিটা পর্ব শেষে সাইট deploy করার মতো অবস্থায় থাকে — যে hook এখনো কাটওভার হয়নি সেটা আজকের মতোই কাজ করে। ক্রম: (০) ভিত্তি — ডিপেনডেন্সি, স্কিমা, সিকিউরিটি মাইগ্রেশন; (১) পড়া — সিড করা ডাটাবেজের বিরুদ্ধে সব ২০৭ E2E পাস, কোনো পেজ না বদলে; (২) অথ — সার্ভারে যাচাই, SessionGuard মুছে ফেলা; (৩) মূল লুপ — স্ক্যান, অর্ডার, রিয়েলটাইম; (৪) মালিকের write; (৫) টাকা; (৬) প্ল্যাটফর্ম; (৭) অপারেশন; (৮) কাস্টম ডোমেইন। ০→১→২→৩ ধারাবাহিক; ৪, ৬, ৭ সমান্তরালে চলতে পারে।


3. What replaces what

3.1 localStorage keys → tables

Key (today)Written byBecomesModule
ros-live-orders:{t}cart submit, boardsorders, order_line_items, order_timeline_eventsorders
ros-my-order-ids:{t}cartstays (docs/15 §7) — server checks access—
ros-table-session:{t}:{table}use-table-sessiontable_sessionssessions
ros-cart:{t}:{table}:{session}cartstays until "Place order"—
ros-guest-name:{t}:{table}name promptstays; snapshotted into lines/orders—
ros-table-calls:{t}guest buttons, waitertable_callssessions
ros-table-tokens:{t}QR regeneraterestaurant_tables.qrToken (+ qrTokenRotatedAt)floor
ros-floor-plan:{t}tables floorzones rows; restaurant_tables.floorX/Y/zoneIdfloor
ros-qr-printed:{t}onboardingtenants.qrPrintedAttenants
ros-menu-images:{t}menu managermenu_items.images + menu-images bucketmenu
ros-menu-allergens:{t}menu managermenu_items.allergensmenu
ros-menu-names:{t}menu managermenu_items.name + a nameBn columnmenu
ros-menu-edits:{t}menu managermenu_items updates (price, description, descriptionBn, category, spice, diet tags, available), inserts for added, soft deletes (deletedAt) for removed; menu_categories inserts, and updates of name/nameBn from categoryPatchesmenu
ros-menu-sold-out:{t}KDSmenu_items.available (a shift-scoped flag, cleared nightly)menu
ros-reviews:{t}guest orders panelreviewscrm
ros-packages:{t}packages boardmeal_packages (+ items, choices); hiddenIds → isActive=falsepackages
ros-package-images:{t}packages boardmeal_packages.images + package-images bucketpackages
ros-branding-text:{t}settingstenants.name, cuisine, cuisineBn, tagline, taglineBn, logoTokentenants
ros-branding-images:{t}settingstenants.logo, tenants.coverImage + branding buckettenants
ros-layout-width:{t}settingstenants.layoutWidthtenants
ros-accent:{t}settingstenants.accent (still echoed to localStorage for the pre-paint script, Rule 4)tenants
ros-accent:platform, ros-theme, ros-glass-intensityappearancestay — viewer preference—
ros-sound:{kitchen|waiter}shift screensstays — per device—
ros-custom-domain:{t}settingscustom_domainstenants
ros-promos:{t}promos boardpromos (extra → rows, overrides → updates)promos
ros-promo-design:{t}promo CMSpromo_landing_designs.designtenants
ros-branches:{t}branches boardbranchesbranches
ros-active-branch:{t}switcherstays — per device—
ros-staff-invites:{t}staff boardstaff_invitesstaff
ros-loyalty:{t}:{phone}checkoutloyalty_accounts, loyalty_transactionscrm
ros-onboarding-dismissed:{t}checkliststays — per viewer—
ros-sessionlogin screensdeleted — Supabase Auth cookiesstaff/auth
ros-platform-tenant-feature-overridesfeature flagstenant_feature_overridesfeatures
ros-platform-custom-plans / -plan-edits / -deleted-plansplan buildersubscription_plans (isCustom, deletedAt)billing
ros-platform-plan-overridestenant detailtenants.planId (+ audit entry)billing
ros-platform-invoice-overrides / -generated-invoicessubscriptionsinvoicesbilling
ros-platform-invoice-email-logsubscriptionsinvoice_emails (+ invoices.emailSentAt)billing
ros-platform-vip-overridestenant detailvip_overridesbilling
ros-platform-support-ticketssupport inboxsupport_tickets, support_ticket_messagessupport
ros-platform-audit-logeverywhereaudit_log_entriesaudit
ros-roadmap-watchlistroadmapstays — per viewer—

Verification for §3.1: after phase 4/6, grep -rn '"ros-' src/hooks must list only the keys marked stays. That grep is the done-check, not a feeling.

3.2 src/lib/data → queries

FunctionQuery (inside withTenant(tenantId) unless noted)
getTenant(slug)tenants by slug with customDomain; public read — called before any tenant scope exists, so it runs in withPlatform and returns only public fields
getAllTenantSlugs()withPlatform: select slug where status in (active, trial) — only for generateStaticParams
getBranchesbranches + count(tables), count(orders today), sum(total today) in Tenant.timezone
getMenucategories (sorted) + items with variants/addons — cached, tag tenant:<id>:menu
getPackagespackages + items + choice groups/options — cached, tag tenant:<id>:menu
getOrders / getOrderorders + items + timeline, newest first; dashboards page by createdAt
getTables / getTablerestaurant_tables + zone; qrUrl built by mapper
getStaff / getRidersstaff_members where not deactivated; rider stats are aggregates over today's orders
getOwnerEmailfirst staff_members with role owner
getCrmCustomerscustomers + count/sum/max(orders) + loyalty points
getReviews, getReservations, getInventory, getSuppliers, getStockMovements, getPurchaseOrders, getDeliveryZones, getPromos, getPromostraight table reads, tenant-scoped
getSalesTrend, getTopItemsaggregates over orders/order_line_items for the last 7 days in Tenant.timezone — candidates for a materialised view (docs/09)
getAuditLogaudit_log_entries for tenant, newest first, paged
getAllTenants, getPlatformStats, getRevenueTrend, getInvoices, getPlatformUsers, getPlatformAuditLog, getAllTenantAuditLogs, getSupportTickets, getNotificationTemplates, getNotificationDispatchLogwithPlatform reads; MRR = sum over active tenants of plan price after VIP override

The components listed at the bottom of src/lib/data/tenant.ts that still import mock-data directly are phase-1 work too: each gets its data as a prop from its page, and cart-sheet.tsx's reads move into placeOrder (it currently decides the next order number and promo validity in the browser — exactly what §15/5 forbids).

3.3 Derived values — never stored

UI fieldComputed from
Tenant.mrrplan price, VIP override
Tenant.tablesCount, Branch.tablesCountcount(restaurant_tables)
Tenant.ordersToday, Branch.ordersToday/revenueTodayorders since local midnight
Tenant.rating, MenuItem.rating/ratingCountavg/count(reviews) (item rating rolled up nightly)
Branch.staffCountstaff assigned to the branch's tables
CrmCustomer.totalOrders/totalSpent/lastOrderAt/loyaltyPointsorders + loyalty account
StaffMember.shiftsThisWeek, rating; Rider.activeDeliveries/completedToday/earningsTodayorders / shifts in the week
Supplier.itemsSuppliedinventory items with that supplier
RestaurantTable.qrUrl/table?table=<id>&token=<qrToken>
Invoice.tenantName, SupportTicket.tenantName/tenantSlug, TableCall.tableLabel/waiterName, Reservation.tableLabeljoins

🟢 বাংলা সারাংশ: §৩.১-এ আজকের প্রতিটা localStorage key-র পাশে লেখা আছে সেটা কোন টেবিলে যাবে, নাকি ইচ্ছাকৃতভাবে ব্রাউজারেই থাকবে। কাজ শেষের প্রমাণ একটা grep: src/hooks-এ শুধু "stays" চিহ্নিত key-গুলো থাকবে। §৩.২-এ src/lib/data-র প্রতিটা ফাংশন কোন কোয়েরি হবে, আর §৩.৩-এ UI-র যেসব সংখ্যা কখনো কলাম হিসেবে সংরক্ষিত হবে না (MRR, আজকের অর্ডার, রেটিং ইত্যাদি — সব কোয়েরিতে হিসাব হয়)।


4. Decisions and accounts the business must supply

These block phases, and none of them is a coding question.

NeededFor phaseNotes
Supabase organisation + 2 projects (ros-staging, ros-prod), region ap-south-1 (Mumbai) or ap-southeast-1 (Singapore)0Closest regions to Dhaka; pick one and keep DB + Vercel functions in it (docs/14 §1)
Vercel team, production domain restaurant-os.com + wildcard *.restaurant-os.com0, 8Wildcard needs Vercel nameservers (docs/13 §5)
Transactional email sender (Resend/Postmark) + verified domain (SPF, DKIM)2Invites, password reset, invoices
SMS gateway (SSL Wireless / BulkSMS BD / Twilio) + approved sender id7Order updates, OTP if ever used
bKash & Nagad merchant accounts (sandbox first), Stripe account5Merchant onboarding takes weeks — start early
Upstash Redis3Rate limiting PIN sign-in, order submit (docs/12 §3)
Real legal text replacing the templates at /legal/*before launchThe pages say they are templates
Data retention decision: how long to keep guest phone numbers3Needed for the privacy policy and a purge cron

🟢 বাংলা সারাংশ: কোড না, ব্যবসার সিদ্ধান্ত ও অ্যাকাউন্ট — এগুলো ছাড়া পর্বগুলো আটকে থাকবে: Supabase প্রজেক্ট (মুম্বাই বা সিঙ্গাপুর রিজিয়ন), Vercel ও wildcard ডোমেইন, ইমেইল সেন্ডার, SMS গেটওয়ে, bKash/Nagad/Stripe মার্চেন্ট অ্যাকাউন্ট (সময় লাগে — আগে শুরু করুন), Upstash, আসল আইনি টেক্সট, আর অতিথির ফোন নম্বর কতদিন রাখা হবে সেই সিদ্ধান্ত।


5. Seeding

prisma/seed.ts is the bridge that makes phase 1 provable: the database starts with exactly the data the E2E suite already asserts against.

  • Import the mock arrays from src/lib/mock-data/* (they are plain TS — the seed reuses them rather than copying).
  • Keep every mock id (tn_swadh_ghar, sg-…, ff-16, plan ids starter…). Tests, deep links and FEATURE.* constants refer to them.
  • Generate what the mocks don't have: zones from the distinct zone ids (the five seeded ones get labelKey), qrToken from the mock table, one order_counters row per tenant starting above the highest mock number, staff_members.pinHash from demoPin(staffId) so the staff-login screen keeps working in staging.
  • Run in one transaction per tenant under withPlatform; upsert everywhere so the seed is re-runnable.
  • Never run against production: the script refuses unless DATABASE_URL's host matches the local or staging project.

🟢 বাংলা সারাংশ: সিড স্ক্রিপ্ট mock-data থেকে হুবহু একই ডেটা ডাটাবেজে বসায়, mock id অপরিবর্তিত রেখে — যাতে বর্তমান E2E টেস্টগুলো ডাটাবেজের বিরুদ্ধেও চলে। mock-এ যা নেই (zone, order counter, PIN hash) তা তৈরি করে। বারবার চালানো যায় (upsert), আর প্রোডাকশনে চলতে অস্বীকার করে।


6. Testing the backend

LayerToolWhat it proves
Pure logicVitest (exists, 322 tests)Unchanged — the server reuses these functions
MappersVitestEvery enum value round-trips; Decimal → number; derived fields
ServicesVitest + PGlite (in-process Postgres 17, the same harness used to verify policies.sql)placeOrder rules, idempotency, schedule windows, plan limits — fast, no Docker
SecurityVitest + PGlite, running policies.sqlThe docs/14 §8 isolation assertions, on every CI run
IntegrationSupabase local stack (supabase start) in CIRealtime delivery, Storage signed URLs, auth cookies
JourneysPlaywright (exists, 207)Same specs against the seeded DB; the signing-in spec rewritten for real auth

New E2E journeys the backend makes possible — none can be written today because they need two devices sharing state:

  1. Guest phone places an order → kitchen tablet shows it, no reload.
  2. Owner changes a price on the laptop → the next scan on a phone shows it.
  3. Owner rotates a table's QR → the old URL is refused, the new one works.
  4. Two guests submit at the same instant → consecutive order numbers.
  5. Manager of tenant A cannot load any tenant B URL (404, not 403 — existence is not leaked).

🟢 বাংলা সারাংশ: বিদ্যমান ৩২২টা ইউনিট টেস্ট আর ২০৭টা E2E অপরিবর্তিত থাকবে। নতুন: mapper টেস্ট, PGlite দিয়ে service ও সিকিউরিটি টেস্ট (Docker ছাড়াই দ্রুত), আর Supabase লোকাল স্ট্যাকে ইন্টিগ্রেশন টেস্ট। ব্যাকএন্ড আসার পরেই যেসব journey লেখা সম্ভব: অতিথির অর্ডার রিলোড ছাড়া কিচেনে দেখা, দাম বদলালে পরের স্ক্যানে দেখা, পুরনো QR প্রত্যাখ্যাত হওয়া, একসাথে দুটো অর্ডারে পরপর নম্বর, আর অন্য টেন্যান্টের URL-এ 404।


7. Deliberately later

Designed-for but not in phases 0–8, so nobody builds them by accident: shared live table carts (one cart across several phones — needs a carts table and presence), offline-first POS with sync (docs/roadmap Phase 25), delivery aggregator ingest, multi-currency per tenant, AI insights. The schema leaves room for each without a breaking migration.

🟢 বাংলা সারাংশ: যা ইচ্ছাকৃতভাবে পরে: একাধিক ফোনে একটাই লাইভ টেবিল কার্ট, অফলাইন-ফার্স্ট POS, ডেলিভারি অ্যাগ্রিগেটর, টেন্যান্ট প্রতি একাধিক মুদ্রা, AI ইনসাইট। স্কিমায় এগুলোর জায়গা আছে, ভাঙা মাইগ্রেশন ছাড়াই যোগ করা যাবে।