Ask My Business

Elevator pitch
An embeddable AI chat widget for small businesses. Answers customer questions grounded in the business's own content: hours, services, FAQs, delivery windows. Multi-tenant architecture with per-client theming, escalation contacts, and Row-Level Security in Postgres so client data never crosses tenants.
The problem
Small businesses (allied health practices, tiffin services, local trades) have websites but no bandwidth to respond to every "when do you deliver / what are your hours / do you take Medicare" question. Existing chatbot vendors either quote enterprise prices or produce generic bots that hallucinate. The gap is a widget that costs less than a monthly streaming subscription, answers only from the client's actual content, and hands off to a human when it doesn't know.
The approach
Multi-tenant from day one. One backend serves every client, isolation enforced at the database layer (forced RLS on Postgres, per-client ChromaDB collections). Widget is a static JS file served byte-identical from the CDN. Same code on every client's site; config fetched at runtime using a public widget key. Grounded retrieval with a tuneable distance threshold, and every question logged for abuse review.
Technical architecture
- API: FastAPI on Railway (project: "empathetic-expression")
- Database: PostgreSQL with forced Row-Level Security. Every query filtered by
tenant_idat the DB level, so an application bug can't leak across clients - Vector store: ChromaDB, one collection per client, embedded content from onboarding
- Background jobs: Redis + RQ for content ingestion, re-embedding, scheduled re-indexing
- Widget: Static TypeScript build, served byte-identical from Railway. Runtime config via
GET /widget/configwith public widget key. Quick-reply chips route through the same guardedPOST /api/v1/widget/queryas free-form questions - Deployment discipline: Migrations reviewed personally before applying to production (migrations 0003 to 0005 currently merged to main on the
aryan-osGitHub repo)
Key decisions and why
- Static widget over embedded iframe. Cleaner integration on client sites (no CSP issues), and the whole widget is auditable as one file
- RLS at the DB layer, not the app layer. App-layer isolation is only as safe as the least-careful query. RLS makes isolation structural
- ChromaDB per client, not shared. Prevents accidental cross-tenant retrieval and makes "delete this client's data" a simple collection drop
- Public widget key + config endpoint. No auth cookies means the widget works anywhere the client embeds it. Rate limiting handles abuse
Hardening list before scaling paying clients
Real production requires more than a working demo. Before onboarding second and third clients:
- Delimit user questions in the retrieval prompt to reduce prompt-injection surface
- Move from a hardcoded system prompt to the client's real versioned system prompt (stored per-client, versioned migrations)
- Log question text and the
groundedflag on every response for abuse and quality review - Tune the retrieval distance threshold now that real client content exists to sample from
Current status
First live deployment: Maruvaan (South Indian tiffin delivery, Sydney). Live example at maruvaan.net.
Domain: askmybiz.com.