Local click-through preview
A throwaway copy of the whole product, running on your laptop, filled with invented Hajj/Umrah data so every screen has something to show. It exists so the owner (or anyone else) can walk the Wave 2 design and the new screens in a browser without touching the hosted project.
Everything here is fake and local. The customers, passports, bookings, payments, PNRs, incidents and account numbers are made up. The logins exist only in the Supabase container on your machine. Nothing in this document touches the production project, and the seed script refuses to run against any host that is not
127.0.0.1/localhost.
1. What you need once
- Docker running (the Supabase CLI starts Postgres, PostgREST, GoTrue, realtime and the edge runtime in containers).
psqlon yourPATH— the seed applies its SQL half through it.npm installdone in this repo.
2. Start the stack
The preview uses its own Supabase project id (alhuda-int-test) on ports
563xx, so it never collides with another local stack.
# from a scratch directory that holds only supabase/config.toml + a symlink
# to this repo's migrations and functions
supabase start --workdir <preview-workdir>
supabase status --workdir <preview-workdir> -o json # prints the keys below
The preview stack needs two things switched on in its supabase/config.toml
that a bare supabase init leaves off — without them the console fills with
503s from /functions/v1/session-manager and failed realtime sockets:
[auth.email] enable_confirmations must stay false (the default) so the demo
logins are usable the moment they are created. The edge runtime serves this
repo's supabase/functions, so symlink or copy that directory next to the
preview's config.toml.
To start over from an empty database:
3. Seed the demo data and the logins
export SUPABASE_URL=http://127.0.0.1:56321
export SUPABASE_SERVICE_ROLE_KEY=… # from `supabase status -o json`
export SUPABASE_PUBLISHABLE_KEY=… # the anon key, same output
export DEMO_DB_URL=postgresql://postgres:postgres@127.0.0.1:56322/postgres
node scripts/dev/seed-demo.mjs
It is idempotent — run it as often as you like. It:
- checks the access model the migrations built — every
RoleTypehas aRolerow, roughly 20 roles and 1,100 grants, no staff role without permissions, no portal role with one, the four screen permissions present,rls_restrictive_only_commands()empty, no ordinary role holding a super-admin-only right, no duplicate function overloads — and refuses to seed anything if any of that is wrong. It does not build the model.20260921110000_dr_seed_roles_and_permission_grants.sqlcreates theRolerows before granting against them, so a plainsupabase db resetproduces the whole thing on its own; - creates one login per role through the Admin API, plus a partner and a
customer portal login wired to demo records. Each staff login gets its role
in a direct database session (
psqlonDEMO_DB_URL) withapp.role_change_maintenanceon: outside that, a staff role changes only through a role request two people approve (ACC-079), and the API refuses a direct write even with the service-role key; - writes the groups, customers, bookings, passengers, visas, tickets, inventory, invoices and the accounting calendar;
- drives the money and duty-of-care flows through their own RPCs, signed in
as the right person — the cashier records receipts (they land
pending), the finance manager verifies them (maker ≠ checker, FIN-032 / ACC-020), the accountant raises a refund request, and the ops manager opens the incidents viaopen_incident. Nothing writes a forbidden state directly; - pre-sets the finance module's 4-digit PIN for every staff login;
- writes the credentials file and prints the row counts.
Every business row it creates has an id starting demo- (or, for uuid columns,
dec0de..-), so demo data is obvious in any table.
Credentials
The script writes the email list, the shared password and the finance PIN to
preview-logins.local in the repo root (git-ignored), or wherever you point
DEMO_LOGINS_OUT. The password is only in that file — it is not in this
document, not in the repo, and not in any commit.
Logins are preview-<role>@alhuda.co.in. They sit on the company domain
because the staff sign-in page allow-lists it (ALLOWED_DOMAINS in
src/pages/auth/Auth.tsx); the preview- prefix keeps them unmistakably fake.
Roles seeded: SUPER_ADMIN, CEO, GM, SALES_MANAGER, SALES_EXEC,
B2B_MANAGER, B2B_EXEC, OPS_MANAGER, OPS_EXEC, TICKET_MANAGER,
VISA_OFFICER, FINANCE_MANAGER, ACCOUNTANT, CHARTERED_ACCOUNTANT,
CASHIER, ADMIN_HR, IT_ADMIN, AUDITOR, plus AGENT (partner portal) and
CUSTOMER (customer portal).
4. Run the app
.env.preview.local (git-ignored) points the frontend at the local stack. It
also has to blank out the production values that the repo's committed .env
carries — Vite merges .env < .env.local < .env.preview < .env.preview.local,
so an empty value is an override but a missing line is not:
VITE_SUPABASE_URL=http://127.0.0.1:56321
VITE_SUPABASE_PUBLISHABLE_KEY=<local anon key>
VITE_SUPABASE_PROJECT_ID=alhuda-int-test
VITE_TURNSTILE_SITE_KEY= # empty = captcha off; a real key leaves Sign In disabled
VITE_SENTRY_DSN= # never report a local preview to the prod Sentry project
VITE_API_URL=
VITE_OXR_API_KEY=
VITE_EXCHANGE_RATE_API_KEY=
VITE_VAPID_PUBLIC_KEY=
Then:
Open http://127.0.0.1:5180/ and sign in at /auth.
Portal logins use their own doors: /partner/auth and /customer/auth.
5. Suggested tour
- Role dashboard (
/app) — sign in asCEO, thenOPS_MANAGER, thenCASHIER. Same page, different content: leadership sees the money and the groups at risk, operations sees the deadline radar and readiness, the cashier sees only what a cashier can act on. The sidebar badge counts are live. - Bookings (
/sales/bookings) — 20 bookings across every stage. Try the search box (Siddiqui,BKG-DEMO-0010), the status filter, and page through the list; paging is keyset-based, not offset. - A booking (
/sales/bookings/demo-bkg-01) — the journey stepper from Enquiry to Completed, the next best action, and per-traveller readiness with rule ids on each check. Then opendemo-bkg-10, which has real blockers: a passport with no expiry recorded and payments outstanding. - Customer 360 (
/customers/demo-c05) — Syed Iqbal is mid-trip. You get where he is today (hotel, room, roommate), his next flight, his family on the booking, the money across both his trips, and — because he is the traveller in the open critical incident — the emergency banner. - Journey board (
/operations/journey) — the two live groups: Batch A on day 9 of 16 in Madinah at 100% readiness, Batch B departing in three weeks at 45% with its blockers named and counted. - Duty of care (
/operations/incidents) — one open critical hospitalisation with its communication log, and one resolved medium hotel problem. Death is deliberately not seeded: open that flow live if you want to see it. - Airline block (
/inventory/quota/demo-blk-sv-oct) — 60 Saudia seats, the manifest that has filled them, third-party seats sold to a partner, the six contract deadlines and the release penalty bands. Then the deadline radar (/inventory/deadlines), holds (/inventory/holds— three active, one converted, one expired) and seat releases (/inventory/releases— one waiting for approval). - Finance (
/finance) — asks for the 4-digit PIN (in the credentials file) on each session. Inside: liquid funds, receivables, four receipts waiting for verification, the finance review queue, a refund request awaiting approval, two journals pending a checker and thirteen posted ones. Then approvals (/approvals) and reports (/reports). - Admin → Permissions (
/admin) — the permission catalog and the role-by-role grant matrix, plus the audit log the whole system writes to. - Portals —
/partner/authas the partner (their own bookings, invoices, purchased seats and commission) and/customer/authas the customer.
6. What is in the data
| Travel groups | 4 — one in travel, one departing in 3 weeks, one returned (Hajj), one upcoming |
| Customers | 30, including families with children and one infant |
| Bookings | 20 — draft, pending ops, pending finance, approved, needs correction, on hold, rejected, cancelled, partially cancelled; two partner-sourced |
| Passengers | 42, some with deliberately missing passports or documents so readiness shows blockers |
| Visa cases | 28, spanning all eight statuses |
| Tickets | 20 issued/cleared/on hold/not ready, plus one pending name-change request |
| Airline blocks | 3 (+ one FIT batch), with deposit, name-submission, ticketing, release and final-list deadlines |
| Holds / releases | 5 holds (3 active, 1 converted, 1 expired), 2 seat releases (1 awaiting approval) |
| Hotels / food / transport | 4 hotels with group allotments, 2 catering lines, 3 coach routes |
| Finance | 13 verified receipts, 4 pending verification, 1 refund request, 13 posted journals, 2 pending journals, 2 group invoices, 5 per-booking invoices, supplier bills and payments |
| Duty of care | 1 open critical hospitalisation, 1 resolved hotel problem |
| Pipeline | 6 leads, 4 quotations, 4 customer requests, 2 partners, 5 suppliers |
7. Known rough edges in the preview
These are findings about the app, not about the seed. They are listed so you are not surprised mid-demo.
- The seed no longer replays migrations, and no longer needs to. It used
to re-apply every migration whose text mentioned
"RolePermission", in filename order, on a database that already had all of them — replaying history out of order. That broke the access model three separate times, each found only by a long audit: it re-created theJournalEntry/JournalLinewrite policiesAS RESTRICTIVEso every posting was denied; it put back anagents.deletegrant a later migration had revoked; and it resurrectedrecord_payment's old 13-argument signature as an overload, after which no receipt could be recorded at all. Those workarounds are gone with the replay.reports.view,admin.view,hotels.viewandgroups.vieware all in the catalog and granted by20260921110000, so the old backfill is gone too. If the seed now aborts on the access model, the migrations are at fault — runscripts/db-test.sh supabase/tests/access_model.sqlto find out which. get_dashboard_stats()referencespublic."Ticket", a table that does not exist — the real table isTicketRecord. The Reports page catches the error and falls back to client-side counting, so it still renders, but the RPC is dead.- The customer portal reads
Payment.receivedBy, a column that exists insrc/types/index.tsand the mock data but has never existed in the database (src/lib/api.ts, theselectInChunkscall inhandleCustomerPortal). The select 400s, the portal's whole data load rejects, and the customer sees zeros. DroppingreceivedByfrom that column list fixes it. admin.currency.viewis not granted toOPS_MANAGER, so the Customers page logs one permission error while loading auxiliary data. The page is fine.
8. Tearing it down
Delete preview-logins.local and .env.preview.local when you are finished.
Both are git-ignored; neither is ever committed.