A free, forkable, mobile-first analytics web app that turns Orderbird POS transactions into banking-grade growth metrics (cohorts, retention, LTV) for non-technical restaurant owners.
V1 tenant: one ramen shop (the founder's friend). Architecture is multi-tenant-ready from day 1 so any restaurant owner can fork or self-host.
adapter-cloudflare)pg_cron@supabase/ssr (cookie-based SSR). Never @supabase/auth-helpers-sveltekit.pg_cron → pg_netSee CLAUDE.md for the full tech-stack rationale and "What NOT to Use" list.
rba-dev and rba-test..env.test.example → .env.test and fill in the TEST project's URL, anon key,
and service-role key.npm installsupabase login && supabase link --project-ref <dev-ref> && supabase db pushsupabase link + supabase db push against the
TEST project ref.public.custom_access_token_hook. See
docs/reference/auth-hook-registration.md
for the exact dashboard steps. Without this step, RLS will deny every query silently.npx vitest run — all Phase 1 integration tests should go green against the TEST
project.bash scripts/ci-guards.sh — all four CI guards should exit 0.public.memberships linking that user to the seeded restaurant
(see supabase/migrations/0005_seed_tenant.sql).CI Guards, Tests, and DB Migrations (DEV)
workflows run automatically.The v1 ingest is CSV-driven. No Orderbird scraper yet.
orderbird_data/README.md) OR drop an existing one at:orderbird_data/5-JOINED_DATA_YYYY-MM-DD/ramen_bones_order_items.csv
INSERT INTO public.restaurants (id, name, timezone)
VALUES (gen_random_uuid(), 'Your Restaurant', 'Europe/Berlin')
RETURNING id;
Copy the returned UUID — you'll need it for memberships and RESTAURANT_ID in .env.npx tsx scripts/ingest/upload-csv.ts <path-to-csv> orderbird-raw dev/ramen_bones_order_items.csv
npm run ingest
psql "$SUPABASE_DB_URL" -c "SELECT COUNT(*) FROM public.transactions;" returns > 0.supabase db push
pg_cron extension via Supabase Dashboard → Database → Extensions → pg_cron → Enable.SELECT jobname, schedule FROM cron.job WHERE jobname = 'refresh-analytics-mvs';
-- expect: 0 3 * * *
SELECT refresh_analytics_mvs();npm run build.svelte-kit/cloudflare.env.example under # --- destination: cf pages project env ---.main — CF Pages builds and deploys.INSERT INTO public.memberships (user_id, restaurant_id, role)
VALUES ('<auth-user-id>', '<restaurant-id>', 'owner');
supabase secrets set ANTHROPIC_API_KEY="sk-ant-..."
supabase secrets set SUPABASE_SERVICE_ROLE_KEY="<your-service-role-jwt>"
supabase functions deploy generate-insight --no-verify-jwt
deploy — looks like https://<ref>.supabase.co/functions/v1/generate-insight.generate_insight_url = full function URLgenerate_insight_bearer = your service_role JWT (same as SUPABASE_SERVICE_ROLE_KEY)supabase db push
SELECT jobname, schedule FROM cron.job WHERE jobname = 'generate-insights';
-- expect: 15 3 * * *
curl -X POST "<function-url>" \
-H "Authorization: Bearer $SUPABASE_SERVICE_ROLE_KEY" \
-H "Content-Type: application/json" -d '{}'
SELECT business_date, substring(headline, 1, 40), fallback_used FROM public.insights ORDER BY generated_at DESC LIMIT 1; returns a row.LICENSE). Keep it or replace with your org's license.analytics, restaurant-analytics, sveltekit, supabase, cloudflare-pages, forkable, pos-integration../scripts/fork-dryrun.sh
Exit 0 means every required file, migration, and env-var documentation is in place.Phase 1 validates session persistence at the supabase-js setSession layer only.
Phase 4 copies the reference files in docs/reference/ into src/ and re-validates
FND-06 end-to-end through an actual browser refresh via @supabase/ssr cookie
hydration:
docs/reference/hooks.server.ts.example → src/hooks.server.tsdocs/reference/+layout.server.ts.example → src/routes/+layout.server.tsdocs/reference/login/ → src/routes/login/Phase 1 is pure infrastructure: tenancy schema, auth hook, RLS, materialized-view wrapper template, CI guards, and the integration test harness.
Phase 2 ships a CSV loader that reads an Orderbird export from Supabase Storage and upserts it into public.stg_orderbird_order_items (raw line items) + public.transactions (deduped, card-hash-scoped customer rows).
Env vars (see .env.example):
SUPABASE_URL — DEV or PROD Supabase project URLSUPABASE_SERVICE_ROLE_KEY — service-role key (server-only, never commit)RESTAURANT_ID — tenant UUID from supabase/migrations/0005_seed_tenant.sqlORDERBIRD_CSV_BUCKET — typically orderbird-rawORDERBIRD_CSV_OBJECT — object path inside the bucket, e.g. dev/ramen_bones_order_items.csvPII note: the CSV contains card PANs and must never hit the repo. See docs/reference/pii-columns.txt for the hashed-only columns the loader persists.
# Stage CSV into Supabase Storage (one-off)
npx tsx scripts/ingest/upload-csv.ts ./orderbird_data/.../ramen_bones_order_items.csv orderbird-raw dev/ramen_bones_order_items.csv
# Dry-run: prints the report without touching DB
npm run ingest -- --dry-run
# Write mode: upserts staging + transactions
npm run ingest
The loader emits a single JSON line on stdout:
| Field | Meaning |
|---|---|
rows_read |
Raw CSV lines parsed (one per Orderbird line item) |
invoices_deduped |
Unique positive-total invoices destined for transactions |
staging_upserted |
Rows written to stg_orderbird_order_items (= rows_read in write mode) |
transactions_new |
Net-new invoice rows inserted this run |
transactions_updated |
Existing invoices touched by the upsert path |
cash_rows_excluded |
Cash line items excluded from card-hash customer tracking |
missing_worldline_rows |
Card rows where the Orderbird worldline join failed — monitor this |
errors |
Parse/upsert errors (should always be 0) |
If missing_worldline_rows grows meaningfully run-over-run, Worldline is silently dropping card references. Founder should investigate the POS export before we trust customer cohorts.
Re-running npm run ingest on the same CSV is a no-op at the row-count level: transactions_new=0, physical row counts unchanged. The natural key (restaurant_id, source_tx_id) plus a 2-day overlap window drives the upsert. See .planning/phases/02-ingestion/02-04-REAL-RUN.md for a verified real-data run.
tests/ingest/fixtures/README.md documents the 11 semantic scenarios the loader handles (split bills, negative invoices, missing worldline joins, etc.) and is the source of truth for the founder-facing interpretation.
.planning/PROJECT.md — vision and non-negotiables.planning/REQUIREMENTS.md — FND-01..FND-08 acceptance criteria.planning/ROADMAP.md — five-phase roadmapCLAUDE.md — tech-stack rationale and forbidden patterns