Receptro is a single Next.js 16 (App Router) application — one deployable unit that serves both the UI and the API. There is no separate backend service.
Browser
│
▼
Next.js App Router (src/app)
├─ Pages: (app)/ authenticated shell, login/register/verify-otp
├─ src/proxy.ts — route protection (redirects unauthenticated users
│ away from protected pages, and authenticated users
│ away from auth pages)
└─ API routes (src/app/api/**/route.ts) — the real backend
│
▼
src/lib/auth — session/JWT/OTP/password helpers, requireUser/requireRole
src/lib/validations — Zod schema per resource
│
▼
src/db (Drizzle ORM) ──▶ PostgreSQL
│
▼
External services (optional, with dev fallbacks):
- SendGrid (email) → falls back to console logging
- Cloudinary (file storage) → falls back to /public/uploads on disk
businesses is the tenant boundary. Every dealer, invoice, payment,
and audit_log row belongs to a business_id, not directly to a user.
This is what allows a business to have multiple logins (OWNER/ADMIN/STAFF)
sharing the same data. Every API route filters on
eq(table.businessId, user.businessId) — see src/lib/auth/requireUser.ts
and the query patterns in src/app/api/**/route.ts.
Defined in src/db/schema.ts (Drizzle ORM, PostgreSQL):
businesses— tenant root (name, GST number, address)users— belongs to a business; role (OWNER/ADMIN/STAFF), status, OTP verification fields, theme/locale preferencesdealers— customers being tracked, with a credit limitinvoices— belong to a dealer; status is computed (UNPAID/PARTIAL/PAID) from payments against the invoice totalpayments— recorded against an invoice, wrapped in a database transaction with row locking so concurrent payment writes can't corrupt an invoice's balanceaudit_logs— append-only log of create/update/delete actions across dealers, invoices, payments, team, and settings
Migrations are managed with drizzle-kit (drizzle/ holds generated SQL;
npm run db:generate / db:push in package.json).
POST /api/auth/registercreates a business + OWNER user, sends an OTP.POST /api/auth/verify-otpverifies the OTP and issues a JWT session cookie (jose, httpOnly,securein production,sameSite=lax).src/proxy.tsreads the session cookie on every request and redirects based on route + auth state (PROTECTED_PREFIXES/AUTH_PAGES).- Every API route calls
requireUser()(andrequireRole()for admin-only actions) fromsrc/lib/auth/requireUser.tsto re-verify the session server-side — the proxy redirect is a UX convenience, not the security boundary.
PaymentForm.tsxsubmits toPOST /api/payments.requireUser()resolves the session and business.paymentSchema(Zod) validates the body.- A Postgres transaction locks the invoice row, validates the payment
against the outstanding balance, inserts the payment, and recomputes
invoice status — all inside
src/app/api/payments/route.ts. logAudit()(src/lib/audit.ts) writes an audit log entry.- The dashboard's aggregate queries (
src/app/api/dashboard/summary) reflect the change on next load — these are real SQL aggregations, not client-side math.
Two deployment paths exist in the repo, described in full in
DEPLOYMENT.md:
- Docker Compose (
compose.yaml) — app + Postgres, for local/single- server use.Dockerfileis a 3-stage build producing a Next.jsoutput: standaloneimage. - Kubernetes (
k8s/*.yaml) — namespace, Postgres deployment/PVC, app deployment/service/ingress, and a migration Job, for testing the app in a cluster.
src/lib/i18n/dictionaries/{en,hi,es,fr}.ts hold UI-chrome translations
(LocaleContext); data fields (dealer names, invoice numbers, etc.) are
never translated. ThemeContext persists a per-user theme preference (5
themes) both server-side (users.theme) and to localStorage for a
flash-free load.