Multi-Tenant CRM for a Field Demo Program
A specialty-foods CPG ran hundreds of in-store demos a month on an aging vertical SaaS plus spreadsheets. Built a multi-tenant CRM — admin scheduling portal, ambassador self-service app (PWA + native iOS), payments, and reporting — with tenant isolation enforced in the database and verified by 126 live security probes.
Problem
A specialty-foods CPG runs an in-store demo program: hundreds of events a month across six regions of a national grocery footprint, staffed by a roster of field brand ambassadors. The program lived in an aging industry SaaS plus a constellation of spreadsheets.
The daily pain was concrete. Reassigning one event to a different ambassador wasn’t supported — the workaround was duplicate the event, then decline the original, which polluted reporting and confused the field team. Ambassadors had no self-service way to publish their availability. Post-event reports, photos, and payments each lived somewhere different, and program status lived in the coordinators’ heads.
Solution
A purpose-built CRM with two faces on one database:
Admin portal — event scheduling (single and bulk), campaigns, venue management with address autocomplete, a color-coded month calendar, a live check-in board that updates in real time as ambassadors arrive, click-to-pay payment links, and a wage/expense split with year-end 1099 CSV export.
Ambassador portal — an availability calendar (the feature that unlocked everything else), shift accept/decline, open-shift claiming, geo + selfie check-in, post-event reports with photo upload, and payment notifications. Delivered as an installable PWA and a thin native iOS app on TestFlight — the native shell wraps the live web app, so every web deploy updates the app instantly.
The fix for the original complaint is almost anticlimactic: reassigning an event is now one edit, with automatic notifications to both ambassadors.
Architecture
Next.js app (56 routes) → Vercel (deploy on push)
│
▼
Supabase
├── Postgres + Row-Level Security ──→ tenant isolation in the DB, not the app
├── Realtime ──→ live check-in board
└── Storage (tenant-scoped) ──→ report photos, receipts, training docs
│
▼
Ambassador clients: installable PWA + Capacitor iOS shell (TestFlight, push-ready)
Key Decisions
Tenant isolation in the database, not the application. Every table carries row-level security; the app can have bugs without leaking a tenant’s data. The test suite proves it live: 126 RLS isolation probes run against the production database, attempting cross-tenant reads and writes on every surface.
The recipient-pin pattern. Audits kept finding the same class of hole: a table referencing another tenant-scoped row lets an admin in Company B plant a row pointing at Company A’s data. The fix — RLS that pins both the row’s tenant and the referenced row’s tenant — became a standing pattern applied across five surfaces.
PWA-first, thin native wrapper. One codebase, instant updates, and an App Store presence — without maintaining a separate mobile app.
Soft-archive over hard-delete. Events archive with notifications instead of disappearing, so history and reporting survive schedule churn.
White-label built in. Branding (logo, name) comes from the tenant record. A fictional second tenant runs as a permanent sales demo and as a live guard against single-tenant assumptions creeping in.
Results
- The duplicate-and-decline workaround is gone — reassignment is one edit with auto-notifications, verified by end-to-end tests against production
- Live pilot underway: real venues loaded, real weekend shifts scheduled
- 126/126 live tenant-isolation probes pass; an independent adversarial verification pass confirmed all ten audited items
- Security audit: zero critical findings; hardening applied (framework CVE patch, CSP headers, credential rotation, upload limits)
- Multi-tenant and self-serve onboarding ready — the next customer is a signup, not a rebuild
How This Scales
- Push notifications — device tokens and the APNs sender are shipped; enabling is a credential drop
- Email dispatch — notification fan-out is env-gated behind a provider key
- More tenants — self-serve company creation exists; per-tenant branding is automatic
- Deeper payments — click-to-pay links today; processor integration is an add-on, not a rework
Tech Stack
- App: Next.js (App Router, 56 routes), TypeScript
- Data: Supabase — Postgres with Row-Level Security, Realtime, tenant-scoped Storage
- Mobile: installable PWA + Capacitor iOS shell (TestFlight)
- Verification: Playwright end-to-end suite + live RLS isolation probes against production
- Hosting: Vercel, deploy-on-push