尚未收集仓库信息。
尚未检测到更新。GitHub 发布版本后会显示在这里。
制作者的介绍仅有上面的一句话。 更多详情请阅读仓库 README。 仓库 README ↗
正在加载完整 README。
查看完整 README ↗Invoice ↔ purchase-order ↔ goods-receipt matching for restaurants — Hebrew-first, English-ready.
A restaurant photographs a supplier invoice; RestoMatch reads it (OCR), matches every line against what was ordered and what was actually received, and shows the owner, in shekels, how much money leaked this month (over-priced lines, short deliveries, unordered items, duplicate invoices…). The receiver at the back door can check a Hebrew delivery in English ("Receiver in English").
| Users | owner · manager · receiver · bookkeeper · chef (per-restaurant roles, multi-tenant) |
| Languages | Hebrew (default, RTL) and English (LTR) — ?lang=en on any URL, or the switcher |
| Core output | the monthly leak figure (₪, integer-agorot math, asserted by a "leak canary" test) |
Client-facing technical overview (hosting, data residency, security, costs, limitations):
docs/HANDOFF.md. Agent/automation contract:AGENTS.md. Current build state:STATE.md. Production go-live checklist:docs/GO-LIVE.md.
flowchart LR
subgraph Clients
B[Browser<br/>Next.js web app]
M[Expo mobile app<br/>prototype]
end
subgraph Vercel
W[apps/web<br/>Next.js 15 · App Router<br/>Auth.js v5 · tRPC v11]
C[Vercel Cron<br/>worker watchdog]
end
subgraph Supabase
PG[(Postgres 16<br/>+ pgvector<br/>RLS per restaurant<br/>+ job_queue · heartbeat)]
ST[(Storage<br/>private invoice-scans bucket)]
end
subgraph Worker["apps/worker (Docker · Fly.io / any container host)"]
Q[job_queue consumers<br/>ocr-invoice · match-invoice · import-catalog<br/>outbox-dispatch · cron jobs]
H[/health · heartbeat/]
end
subgraph External
AN[Anthropic Claude Vision<br/>OCR]
RS[Resend<br/>email]
WA[WhatsApp Cloud<br/>mock today]
end
B -->|HTTPS · tRPC| W
M -.->|tRPC| W
W -->|restomatch_app role<br/>RLS enforced| PG
W -->|signed URLs| ST
W -->|enqueue jobs<br/>job_queue| PG
PG -->|claim<br/>SKIP LOCKED| Q
Q -->|owner role<br/>tenant-checked in code| PG
Q --> AN
Q --> RS
Q -.-> WA
H -->|worker_heartbeats| PG
C -->|reads heartbeat,<br/>emails owner if stale| W
Request path: browser → Next.js middleware (auth, locale, CSP nonce) → server components / tRPC
procedures (packages/api) → Postgres as the non-owner restomatch_app role inside a transaction
that sets app.current_restaurant_id, so Row-Level Security scopes every query to one restaurant.
Async path: an upload enqueues ocr-invoice → the worker runs OCR (Claude Vision, or stub
fixtures without a key), stores invoice lines, links products (pgvector catalog matcher), then
chains match-invoice → the 3-way matching engine (packages/matching) writes a match_run with
discrepancies → the approvals engine routes them to the right role → notifications go to the outbox.
pnpm workspaces + turbo. Every package exports its TypeScript sources directly (no build step).
| Path | What it is |
|---|---|
apps/web | Next.js 15 web app: marketing site, dashboard, admin, auth (password + TOTP 2FA + magic link), tRPC route, cron route |
apps/worker | Postgres job-queue worker: OCR, matching, catalog import, cadence alerts, outbox dispatch, retention; /health server + heartbeat (worker_heartbeats); Dockerfile + fly.toml |
apps/mobile | Expo (React Native) prototype — see Mobile |
packages/api | tRPC routers, tenant guards, approvals engine, entitlements, notifications |
packages/db | Drizzle schema, hand-written SQL migrations (drizzle/00NN_*.sql), RLS policies + provisioning, seed, db:doctor |
packages/matching | The 12-discrepancy matching engine + integer-agorot money math + leak canary |
packages/charts | KPI / leak / supplier SQL used by the dashboard |
packages/ocr | OCR pipeline, Claude Vision provider, stub provider, reconciler |
packages/catalog | Product catalog matcher (exact SKU → aliases → pgvector) |
packages/procurement | MarketMan / Zestt purchase-order sync clients |
packages/queue | Postgres job queue (job_queue, FOR UPDATE SKIP LOCKED) shared by web + worker |
packages/billing | Plans, entitlements, provider-agnostic billing rails |
packages/crypto | argon2id password hashing |
packages/observability | Sentry-compatible error hooks (no-op until configured) |
packages/types | Shared zod schemas / types (OCR payloads, cadence, fit score) |
packages/ui-tokens | Design tokens ("command center for money", dark, RTL-aware) |
Requirements: Node 22 (.nvmrc), pnpm 10.33 (via corepack), and either a local PostgreSQL
(Linux apt / macOS Homebrew) or Docker. Everything else is handled by one script:
./scripts/dev-setup.sh # deps · Postgres 16 + pgvector · migrate + RLS (test + dev DBs)
# · seed the demo restaurant · write apps/web/.env.local
pnpm --filter @restomatch/web dev # → http://localhost:3000
Options: --no-seed, --docker (force docker compose), DEMO_TODAY=YYYY-MM-DD (seed "today's"
deliveries for another date). Re-running is safe.
Demo logins (restaurant "כפר הזיתים"): owner@, manager@, receiver@ (English UI),
bookkeeper@, [email protected] — password RestoMatch-Demo-2026!. The owner is a
platform admin (/admin). Magic links (no email provider locally) are printed in the dev-server log
and written to $MAGIC_LINK_FILE.
Run the worker too (otherwise uploads are not processed and the dashboard shows a "worker is down" banner):
pnpm dev:worker # loads apps/web/.env.local; tsx watch; health on :8080/health
# prove the queues end-to-end (stub OCR → match), self-cleaning:
pnpm --filter @restomatch/worker exec tsx scripts/smoke-queues.ts
If the container/laptop restarted: service postgresql start
(or docker compose up -d).
Local values are written by dev-setup.sh into apps/web/.env.local. The full annotated list is
.env.example; production values and where to get them are in
docs/ENV-PRODUCTION.md. The essentials:
| Variable | Web | Worker | Purpose |
|---|---|---|---|
DATABASE_URL | ✓ | ✓ | owner connection (worker, migrations, Auth.js identity layer) |
DATABASE_URL_APP | ✓ | non-owner restomatch_app role — RLS enforced (required in prod; boot guard) | |
QUEUE_DATABASE_URL | ✓ | ✓ | optional separate connection for the job queue; defaults to DATABASE_URL |
AUTH_SECRET, AUTH_URL, APP_URL | ✓ | Auth.js | |
AUTH_ENC_KEY | ✓ | encrypts TOTP secrets at rest (back it up) | |
RESEND_API_KEY, EMAIL_FROM | ✓ | ✓ | email (magic links, invites, alerts) |
ANTHROPIC_API_KEY | ✓ | real OCR (without it, only stub-fixture jobs run) | |
NEXT_PUBLIC_SUPABASE_URL, …_PUBLISHABLE_KEY, SUPABASE_SERVICE_ROLE_KEY | ✓ | invoice scan storage | |
CRON_SECRET, WORKER_ALERT_EMAILS | ✓ | worker watchdog cron |
Both apps fail fast at boot in production when a required variable is missing
(apps/web/lib/env.ts, apps/worker/src/env.ts). There is no Redis: the job queue, cron schedules,
rate limits and the worker heartbeat live in Postgres (migration 0061, docs/REDIS-REMOVAL.md); a
leftover REDIS_URL only logs a warning.
export DATABASE_URL_TEST=postgres://postgres:postgres@localhost:5432/restomatch_test
pnpm test # every package, serially (DB-backed suites, RLS attack suites, leak canary)
pnpm typecheck # every package incl. web and mobile
pnpm lint # ESLint (apps/web)
pnpm --filter <pkg> exec vitest run <file> # one file
pnpm db:doctor # read-only schema health report (DATABASE_URL=…)
# Browser suite (Playwright) — creates/seeds its own restomatch_e2e DB and starts `next dev`:
pnpm --filter @restomatch/web test:e2e # auth (password, 2FA, magic link) + smoke
pnpm --filter @restomatch/web test:e2e:smoke # every main page in he + en
E2E_SKIP_SEED=1 pnpm --filter @restomatch/web test:e2e:smoke # reuse the seeded e2e DB
# Production build of the web app
cd apps/web && set -a && . ./.env.local && set +a && pnpm build
The smoke suite asserts, for every dashboard and public page in both languages: HTTP < 400, no
bounce to /login, correct <html lang dir>, no error boundary, no console errors, and no
Hebrew left in the English UI chrome.
CI (.github/workflows/ci.yml, on every PR): typecheck + tests against Postgres 16 + pgvector
with every migration applied, RLS provisioning + completeness gate + db:doctor; worker tests
(Postgres only); web lint + next build (type-checked) + tsc; the worker Docker image build; Playwright
(non-blocking during its soak); pnpm audit and dependency review. CodeQL and secret scanning run
in their own workflows.
| Component | Where | How |
|---|---|---|
| Web | Vercel (apps/web, build turbo run build --filter @restomatch/web) | git-connected; env vars per docs/ENV-PRODUCTION.md; cron in apps/web/vercel.json |
| Database + storage | Supabase | DATABASE_URL=<direct owner URL> pnpm db:migrate, then provision-rls (docs/RLS-PROVISIONING-RUNBOOK.md, docs/DB-RUNBOOK.md) |
| Worker | any container host (Fly.io config included) | docker build -f apps/worker/Dockerfile . from the repo root; fly deploy --config apps/worker/fly.toml --dockerfile apps/worker/Dockerfile . |
The worker image is multi-stage (≈670 MB), runs as the non-root node user with node as PID 1
(clean SIGTERM shutdown), and has a HEALTHCHECK on /health. During the pilot the worker can also
run on the owner's Mac (apps/worker/scripts/start-worker.sh + launchd plist). Step-by-step
production rollout: docs/GO-LIVE.md.
Worker hosting options (same image everywhere; build context = repo root; one instance is
enough for the pilot — jobs are claimed with SKIP LOCKED, so extra instances are safe if you scale later):
| Host | Setup |
|---|---|
| Fly.io (config in repo) | fly launch --no-deploy --copy-config --name restomatch-worker → fly secrets set DATABASE_URL=… ANTHROPIC_API_KEY=… RESEND_API_KEY=… EMAIL_FROM=… APP_URL=… → fly deploy --config apps/worker/fly.toml --dockerfile apps/worker/Dockerfile .. Set primary_region next to the database. |
| Render | New → Background Worker → Docker, Dockerfile path apps/worker/Dockerfile, context .; add the same env vars. (Or a Web Service with health check path /health if you want the HTTP probe.) |
| Railway | New service from the repo → Settings → Build: Dockerfile apps/worker/Dockerfile, root /; Variables: the same env vars; health check /health. |
| Any Docker host | docker run -d --restart=always -p 8080:8080 --env-file worker.env restomatch-worker |
Behind a TLS-intercepting proxy, build with --secret id=ca,src=/path/ca.crt (see the Dockerfile
header).
GET /api/healthz; worker GET /health (liveness) and /health/ready (database select 1).worker_heartbeats every 60 s; Vercel
Cron hits /api/cron/worker-heartbeat every 5 min and emails WORKER_ALERT_EMAILS when it is
stale; the dashboard shows a banner.[queue] job=… attempt=… failed: …) and sent to Sentry
when SENTRY_DSN is set.pnpm db:doctor (read-only; safe against production).pg_dump -Fc and rehearse with pnpm --filter @restomatch/db rehearse-cutover./admin (platform admins: users.is_platform_admin or PLATFORM_ADMIN_EMAILS) —
restaurants, plans, leads.apps/mobile is an Expo 52 / React Native 0.76 prototype (owner KPIs, approvals queue,
receiving). It typechecks and bundles (cd apps/mobile && pnpm exec expo export --platform android; pnpm --filter @restomatch/mobile start for Expo Go, with EXPO_PUBLIC_API_URL pointing
at the web app), but it has no sign-in — every API call it makes is
rejected as unauthenticated — so it is not demo-able against real data. The web app is responsive
and is the supported mobile experience today. See docs/HANDOFF.md → Known limitations.