Botly:可嵌入的多租户 AI 客服助手,基于 Next.js + Supabase + Claude
vijayaprathap1/botly
Botly 是一个可嵌入的 AI 客服助手,每个客户企业一个实例,通过一个 script 标签安装,采用 Next.js(App Router)+ Supabase(Postgres、Auth、RLS)+ Claude(默认 Haiku 4.5)构建,目前 Phase 1、2、3 已完成。
An embeddable AI support assistant, one per client business, installed with one script tag. Multi-tenant: Next.js (App Router) + Supabase (Postgres, Auth, RLS) + Claude (Haiku 4.5 by default).
Status: Phases 1, 2 and 3 built. See PLAN.md for the architecture and every place this build departs from the spec, and Status at the bottom.
1. Run it locally
You need Node 20+ and a Supabase project (a free hosted project is easiest; the Supabase CLI with Docker also works).
npm install
cp .env.example .env.local # fill in the values, see comments in the fileDatabase. Apply the migrations in order, then (optionally) the demo seed:
- Supabase CLI:
supabase link --project-ref <ref>thensupabase db push - or paste
supabase/migrations/0001_schema.sql…0005_phase3.sqlinto the SQL editor, in order (already ran 0001–0003? just run 0004 and 0005) - demo client: run
supabase/seed.sqlthe same way. It creates Ananya Handlooms with approved knowledge, public keypk_ananya_demo_0001and test tokentt_ananya_demo_private_0001_change_me_in_production. Rotate that test link in Settings if you keep the demo in production.
Auth. In Supabase → Authentication → URL configuration, set Site URL to http://localhost:3000 and add http://localhost:3000/auth/callback (and later your production URL + /auth/callback) to Redirect URLs. Supabase's built-in email sender only allows a few emails per hour; for real use, point Supabase Auth at Resend's SMTP (Authentication → SMTP settings).
npm run check # setup doctor: tests .env.local, Supabase, migrations, RLS, Anthropic, Resend npm run dev # builds public/widget.js, then starts Next.js on :3000
If something is missing, npm run check says exactly what and where to get it, and the dashboard shows a Finish setting up page instead of crashing. After editing .env.local, always stop and restart npm run dev. GET /api/health returns 200 when config, database and AI are all working (use it for uptime monitoring).
Open http://localhost:3000, sign in with an address listed in ADMIN_EMAILS. The first sign-in makes you the platform admin.
Free LLM for testing
Botly can run on a free OpenAI-compatible API instead of Claude while you test. Put two lines in
.env.local (and in Vercel → Settings → Environment Variables, then redeploy):
| Provider | LLM_PROVIDER= |
Get the key | Free tier (check current limits) |
|---|---|---|---|
| Google Gemini (recommended) | gemini |
https://aistudio.google.com/apikey | Best Tamil/Hindi, large token allowance, no card. Free tier may train on data |
| Cerebras (fastest) | cerebras |
https://cloud.cerebras.ai | Trial credit (30 days), ~1M tokens/day, 5 req/min |
| Mistral | mistral |
https://console.mistral.ai/api-keys | "Experiment" plan, generous monthly tokens, needs phone verification, may train on data |
| Groq | groq |
https://console.groq.com/keys | Very fast, ~1,000 req/day but ~8k tokens/min: small knowledge bases only |
| GitHub Models | github |
https://github.com/settings/personal-access-tokens (fine-grained, "Models: read") | ~150 req/day, ~8k input tokens per request |
| OpenRouter | openrouter + LLM_MODEL=<id>:free |
https://openrouter.ai/keys | ~50 req/day (1,000 after a $10 top-up) |
| NVIDIA | nvidia |
https://build.nvidia.com | Shared queue; big models (Llama 3.3 70B) often time out |
Put the key in LLM_API_KEY. NVIDIA models that respond quickly (try in this order): qwen/qwen3-next-80b-a3b-instruct (default),
openai/gpt-oss-120b (add LLM_REASONING_EFFORT=low), openai/gpt-oss-20b, nvidia/nemotron-3.5-lightning-30b-a3b.
Avoid 70B+ dense models and "thinking" variants on the free queue.
Optional: LLM_MODEL to pick another model (confirm the exact id in the provider's console),
LLM_REASONING_EFFORT=off|low|medium for thinking models (Gemini and Groq presets use low),
LLM_TIMEOUT_MS (default 25000) before a slow reply gives up and shows the contact card.
Then run npm run llm:check. It tests streaming, tool calling ("I don't know" reporting) and a Tamil reply, with timings.
What changes with a free tier:
- Replies stream the same way, and every tool works (leads, handoff, unanswered, follow-ups). Cost shows $0.
- No Claude prompt caching, and free tiers are rate-limited, so replies can be slower than Claude's ~2 seconds.
- Other models may follow the grounding and language rules less reliably, and the go-live safety check may fail more often.
- Free tiers are for testing. Gemini's free tier may use prompts to improve Google's products, so don't send real customer
conversations through it. Switch back with
LLM_PROVIDER=anthropic(or a paid key) before you sell.
2. Deploy to Vercel
- Push this repo to GitHub and import it in Vercel (framework: Next.js; the default
npm run buildalso builds the widget). - Add every variable from
.env.examplein Project → Settings → Environment Variables. SetNEXT_PUBLIC_APP_URLto your production URL (e.g.https://app.botly.in). - Add
https://YOUR_DOMAIN/auth/callbackto Supabase Auth redirect URLs. - Deploy. The widget is served from
https://YOUR_DOMAIN/widget.js.
Notes: notifications run with after() so they never slow the chat stream. Onboarding (crawl + drafting) can take 1–3 minutes; its route asks for 300 s. If your Vercel plan caps functions lower, reduce CRAWL_MAX_PAGES.
3. Migrations
| File | What it does |
|---|---|
0001_schema.sql |
All tables (§3 of the spec) plus rate_limits, notifications (delivery log), eval_runs (go-live gate). RLS enabled everywhere. |
0002_functions.sql |
Atomic rate limiter, quota-checked begin_conversation, usage/cost accounting, 80% warning claim, unanswered-question merging (trigram similarity). Callable only by the service role. |
0003_rls.sql |
admin sees everything; owner reads their own org (and may change a lead's status); anon sees nothing. |
0004_phase2.sql |
Client invites, owner answer suggestions (a trigger limits owners to suggesting), token/cost columns hidden from owners, knowledge_chunks with pgvector + full-text for retrieval mode, bot_report() for monthly reports, delete_visitor_data() and purge_expired_data(). Enables the vector extension. |
0005_phase3.sql |
bot_integrations for Shopify/WooCommerce order lookup. Credentials are encrypted in the app; browser roles can't read even the ciphertext. |
seed.sql |
Generated from evals/ananya-handlooms.yaml by npm run seed:gen. Don't edit by hand. |
Add new migrations as 0004_…sql and so on.
4. Onboard a new client (about 15 minutes)
- Clients → New client. Business name, type (used in the assistant's intro), plan, monthly conversations, timezone, the assistant's display name and their website. The website's domain is added to the allowed domains automatically.
- Onboarding. Paste the website URL and press Start. Botly reads
robots.txt, thensitemap.xml(or follows links), up to 40 pages, and pulls the product catalogue from/products.jsonon Shopify stores. Claude then drafts 20–40 FAQs, a policy summary and a tone-of-voice line. Everything lands as draft. - Knowledge. Filter to Drafts, read through, untick anything wrong, Approve. Fix gaps by editing a source or adding one (FAQ, policy, product, note). Import a product CSV (
name, price, sizes, stock note, url) or upload PDF/DOCX price lists. Click Use this tone if the suggested tone fits. Only approved knowledge reaches the assistant, and edits apply to the next message. - Settings. Colour, avatar, greeting, suggested questions, business hours, lead emails and WhatsApp numbers, fallback contact (shown when chat is unavailable), privacy link. Add
http://localhost:3000to allowed domains if you want to test the script tag on your machine. - Playground. Ask what customers ask. Use the English / Tamil / Hindi / Hinglish buttons. Click a reply to see tokens, cost, latency, tool calls and which knowledge was in the prompt.
- Private test link. Overview → Copy test link, and send it to the client ("ask it anything"). It works while the bot is still a draft and isn't indexed.
- Eval, then go live.
npm run eval -- --bot <bot-id>. Every prompt-injection case must pass before Go live is allowed. For a client's own in-knowledge cases, copyevals/ananya-handlooms.yamltoevals/<bot-id>.yamland change the questions; otherwiseevals/generic.yamlis used. - Install. Send the client the one-line snippet (below). Check Conversations and Leads after the first day.
Give the client their own login
Bot → Overview → Client access → enter their email → Give access. They get an email with the sign-in link (or send them /login). They see only their business: Conversations, Leads, Unanswered (they can suggest answers, which you approve with one click), Reports and the install snippet. No cost, model settings or other clients.
Growth plan: order lookup and callbacks
- Settings → Plan → Growth.
- Settings → Order lookup → Shopify (store URL + Admin API token from a custom app with
read_orders, plusread_all_ordersfor orders older than 60 days) or WooCommerce (site URL + a Read REST API key/secret). Botly tests the connection before saving and stores the credentials encrypted (ENCRYPTION_KEY). - Visitors can then ask "where is my order #1042?". The assistant asks for the phone or email used on the order and shows status, courier, tracking link and expected date only if they match. Wrong details get the same "couldn't find it" answer as a wrong number, and each visitor gets 5 lookups per 10 minutes.
- "Can someone call me tomorrow evening?" opens a booking form (date + slot). It's saved as a
callbacklead and the owner is notified. - Growth clients get a summary email every Monday at 09:00 IST.
Selling it as self-serve SaaS
Anyone can now sign up at your site, build an assistant from their public profiles, try it free, and pay monthly.
Roles
- Super admin (you): every address in
ADMIN_EMAILS. Sees/app/admin: MRR, active trials, trial→paid conversion, AI cost and estimated margin, and can extend a trial, set a plan for offline/done-for-you clients, or suspend an account. Also sees cost, model and eval details on every bot. - Owner (a customer): signs up with Google or email, or is invited by you. Manages only their own business: knowledge, settings, preview, conversations, leads, unanswered questions, reports and billing. Never sees model, cost, quotas or other customers.
Customer journey
- Landing page
/→ Start free →/login(Google or email link). - First sign-in goes to
/start: business name and type, website, social profile links and pasted bios, contact details and hours. - Botly reads up to 15 public pages (plus Shopify products), Claude writes a business profile document, FAQs and a policy summary (all approved automatically for self-serve), picks a greeting, suggested questions and tone, then runs the 4 prompt-injection safety checks. If they pass, the bot goes live.
- The customer lands on their Overview: trial meter, live preview, their business profile (edit/download), the install snippet and a Go live / Pause switch.
- Free trial:
TRIAL_DAYSdays andTRIAL_REPLIESAI replies (preview replies count), once per email. When it ends, visitors see the business's phone/WhatsApp/email instead of AI replies, and the owner sees "Choose a plan". /app/billing→ Razorpay Checkout (UPI, cards, net banking) → subscription active → plan quota applies. Renewals, failures and cancellations arrive by webhook.
Why social profiles aren't scraped: Instagram, Facebook, LinkedIn and Google Maps forbid automated reading of their pages, and Google's Places API terms don't allow storing its data. Customers paste their bios instead; the links are kept in their profile.
Setup for payments and sign-in
- Run
supabase/migrations/0006_saas.sqlin the Supabase SQL Editor. - Google sign-in: Google Cloud Console → APIs & Services → Credentials → OAuth client ID (Web). Authorised redirect URI:
https://<your-project>.supabase.co/auth/v1/callback. Paste the client ID and secret in Supabase → Authentication → Sign In / Providers → Google, and enable it. Other providers (Microsoft, GitHub, Facebook, LinkedIn) work the same way; list them inNEXT_PUBLIC_AUTH_PROVIDERS. - Razorpay (full steps below in "Payments (Razorpay)").
- Fill
BUSINESS_LEGAL_NAME,SUPPORT_EMAIL,GRIEVANCE_OFFICERfor the Terms and Privacy Policy pages (templates: have a lawyer review them).
Pitching with a demo
Super admin → Demos. Paste a business's website and, two to four minutes later, you get a private link (/demo/<long-random-id>) showing a copy of their homepage with a Botly assistant trained on their site. Send it to the owner or open it on a call.
- What the page is. A static copy of their homepage: their images, styles and fonts load from their own servers, none of their scripts run (the page's Content-Security-Policy only allows Botly's widget and bar), forms and cart/login links do nothing, and other links open the real site in a new tab. A bar says "Preview made by Botly… Not the official website". The page isn't indexed.
- JavaScript-built sites (React, some Wix and Shopify themes) arrive empty when downloaded. Those are opened in a headless browser instead (
@sparticuz/chromiumon Vercel, your own Chrome locally), which also reads a few of their pages for the assistant.DEMO_RENDER=offswitches this off. - Limits. Each demo gets
DEMO_REPLY_LIMITAI replies (150) and expires afterDEMO_DEFAULT_DAYS(14); both are enforced by the same gate as free trials. When either runs out the chat shows your contact details (DEMO_CONTACT_*). Leads and "talk to a person" requests from a demo are emailed to you, marked[DEMO]. - You're told when it's opened. The first view by someone who isn't a signed-in admin sends you an email.
- Housekeeping. The daily job expires demos past their date and deletes them, with their conversations, 30 days later. Delete removes one immediately.
- Use. Only to pitch that business's owner. Not for advertising or showing to the public.
Setup: run supabase/migrations/0009_demos.sql in the Supabase SQL editor and set DEMO_CONTACT_PHONE / DEMO_CONTACT_WHATSAPP on Vercel. Demos are separate organizations flagged is_demo, left out of the client list, revenue numbers and trial emails.
Payments (Razorpay)
- Account: sign up at razorpay.com and complete KYC (PAN, bank account, business details). Ask support to enable Subscriptions if it isn't in the menu.
- Business details on your site (Razorpay reviews these pages before approving live payments): set
BUSINESS_LEGAL_NAME,SUPPORT_EMAIL,SUPPORT_PHONE,BUSINESS_ADDRESS,BUSINESS_CITY,GRIEVANCE_OFFICER. They appear on/contact,/terms,/privacy-policyand/refund-policy(all linked in the footer). - Test mode first: Account & Settings → API Keys → generate test keys →
RAZORPAY_KEY_ID(rzp_test_…) andRAZORPAY_KEY_SECRET. - Plans: Subscriptions → Plans → create "Botly Starter" (monthly, every 1 month, ₹2,999) and "Botly Growth" (monthly, ₹9,999). Copy the
plan_…ids intoRAZORPAY_PLAN_STARTER/RAZORPAY_PLAN_GROWTH. The amounts must matchPRICE_STARTER_INR/PRICE_GROWTH_INR(the site shows prices "+ GST"; if you want GST included in the charge, put the GST-inclusive amount in the plan and on the site). - Webhook: Account & Settings → Webhooks → Add. URL
https://YOUR_DOMAIN/api/billing/webhook, a secret you make up (put the same value inRAZORPAY_WEBHOOK_SECRET), events: allsubscription.*. - Check:
npm run checkconfirms the keys work, both plans exist in the same mode, they're monthly INR, and their prices match the website. - Try it: sign in as a customer → Plan & billing → Choose Starter → pay with a Razorpay test card or test UPI id → the plan turns Starter within seconds, and the payment appears in Super admin → Latest billing events.
- Go live: after KYC approval, switch the dashboard to Live mode and repeat steps 3–5 there (live keys, live plans, live webhook — ids differ between modes). Update the environment and redeploy.
How it behaves:
- Nothing changes until Razorpay confirms payment. Closing the checkout never touches the customer's current plan.
- Switching plans: the new plan starts once paid, and the old subscription is cancelled at that moment, so nobody is billed twice.
- Renewal fails → status "past due", service stays on while Razorpay retries. Retries exhausted ("halted"), cancelled or ended → the widget shows contact details instead of AI replies.
- Every webhook is stored in
billing_events(visible in Super admin). A webhook that can't be applied returns 500 so Razorpay retries it.
Alerts, trial emails and updates
- Error alerts: every server error and every AI-provider failure emails
ALERT_EMAIL(or the firstADMIN_EMAILSaddress), at most once per 15 minutes per kind of error. NeedsRESEND_API_KEY. For uptime, point a free monitor (UptimeRobot, Better Stack) athttps://YOUR_DOMAIN/api/health?deep=1. - Trial emails: the daily job (9 am IST) sends each trial one welcome, one "80% of replies used", one "2 days left" and one "trial ended" email.
- Migration 0007 (billing name, GSTIN, trial email log): run
supabase/migrations/0007_billing_details.sqlin the Supabase SQL editor after pulling this update. - Migration 0010 (only if you ran an earlier draft of 0009 and see "cannot change return type of existing function"): run
supabase/migrations/0010_demos_upgrade.sql. It is safe to run on any database. - Migration 0008 (business hours "not set" instead of a made-up Mon–Sat 10–7 default): run
supabase/migrations/0008_hours_not_set.sqlthe same way. It also resets existing bots that still carry the untouched old default; a business that really keeps those hours re-enters them in Settings.
5. Install the widget
The whole install is one line. Put it just before </body>:
<script src="https://YOUR_DOMAIN/widget.js" data-key="BOT_PUBLIC_KEY" async></script>
The client's domain must be in the bot's allowed domains (Settings). On any other domain the widget stays silent: no bubble and no console errors.
Shopify. Online Store → Themes → ⋯ → Edit code → layout/theme.liquid. Paste the line just above </body> and save. Add both the store domain and your-store.myshopify.com (or *.myshopify.com) to allowed domains.
WordPress. Easiest: install the free "WPCode" (Insert Headers and Footers) plugin → Code Snippets → Header & Footer → paste into Footer → Save. Or, in a child theme, paste it before </body> in footer.php.
Wix. Settings → Custom code (under Advanced; needs a paid plan with a connected domain) → Add custom code → paste → Place code in Body – end → Apply to All pages → Load code once. Add both your custom domain and *.wixsite.com if you use the free subdomain for testing.
Any other site. Paste before </body> in the shared layout/footer template.
Power users can drive it from the page:
Botly.open(); Botly.close(); Botly.sendMessage("Do you ship to Dubai?"); Botly.identify({ name: "Priya", phone: "9876543210", email: "priya@example.com" }); // prefills the lead form
To switch a client off (cancellation), untick Active in Settings: the bubble disappears from their site on the next page load. Nothing to remove on their side.
6. Tests
npm test # unit tests (Vitest): prompt builder, knowledge cap, quota, rate limiter, origin # check, markdown sanitiser, phone validation, chat engine, crawler, CSV, eval checks npm run widget:size # builds the widget and prints raw/gzipped size (fails over 35 KB gzipped) npm run eval -- --fixture evals/ananya-handlooms.yaml # 34 eval cases against real Claude, no database needed npm run eval -- --bot <bot-id> # same, against a bot in Supabase; records the result
The eval needs the active provider's key (ANTHROPIC_API_KEY, or LLM_API_KEY with LLM_PROVIDER=nvidia). --scripted runs the harness with a fake model to check the plumbing; its scores mean nothing.
Database and end-to-end tests without Supabase. A local test harness runs the real app against plain Postgres + PostgREST, with a minimal auth stand-in that writes magic links to a file and a fake Resend that records emails. It is for automated tests only; use the Supabase CLI for day-to-day local development.
DB=botly_test bash scripts/local-db.sh # migrations + seed into local Postgres DATABASE_URL=postgres://postgres:postgres@localhost:5432/botly_test npm run test:db # RLS + SQL function tests # End-to-end (needs a PostgREST binary and a build made with the harness env; see scripts/local-stack/start.sh): POSTGREST_BIN=/path/to/postgrest bash scripts/local-stack/e2e.sh
Playwright embeds the widget on hostile pages served from a different origin: aggressive global CSS (* { all: unset }, button { background: red !important }), a sticky header with a huge z-index, a plain page, a disallowed origin, and a 375 px phone. It also signs in to the dashboard and walks through knowledge, settings, playground, conversations, leads and an onboarding crawl.
Measured in this build: widget 10.4 KB gzipped (budget 35 KB). Lighthouse performance on a plain host page: 100 without the widget, 100 with it (3 runs each, CLS 0).
Scheduled jobs
vercel.json schedules two Vercel Cron jobs; set CRON_SECRET so only Vercel can call them:
/api/cron/retentionnightly at 03:00 IST: deletes conversations and leads older than each client's retention setting (default 12 months)./api/cron/weeklyMondays at 09:00 IST: weekly summary email for Growth clients.
7. Credentials you need to create
| What | Where | Used for |
|---|---|---|
| Supabase project | supabase.com | Database, auth. NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY, SUPABASE_SERVICE_ROLE_KEY |
| Anthropic API key | console.anthropic.com | Chat replies, onboarding drafts, evals. ANTHROPIC_API_KEY |
| Resend API key + verified domain | resend.com | Lead emails. Until a domain is verified you can only send to your own Resend address. RESEND_API_KEY, EMAIL_FROM |
| WhatsApp Cloud API (Phase 2) | developers.facebook.com → WhatsApp | A permanent System User token, phone number ID, and an approved utility template named new_lead with 4 body variables, e.g. "New lead for {{1}}: {{2}}, {{3}}. Need: {{4}}". Until then each lead logs "WhatsApp pending" and email still goes out. |
CRON_SECRET |
any long random string | Protects the scheduled jobs |
ENCRYPTION_KEY (Growth) |
openssl rand -base64 32 |
Encrypts store credentials. Never change it after connecting stores. |
| Voyage AI key (optional) | voyageai.com | Semantic search when a client's knowledge exceeds 25k tokens. Without it, retrieval uses keyword search. |
| Shopify custom app / WooCommerce API key (Growth, per client) | the client's store admin | Order lookup |
| Vercel project + domain | vercel.com | Hosting the app and widget.js |
Status
Phase 1: migrations + RLS; clients and bots; knowledge editor with approve flow, CSV/PDF/DOCX import; website crawler with Claude-drafted FAQ/policy/tone; the widget; streaming /api/chat with prompt caching and the four core tools; email notifications; private test page; playground; conversations; leads; usage and cost logging; quotas; eval runner and the Ananya Handlooms eval set; go-live gate.
Phase 2: unanswered inbox with one-step answering and owner suggestions; client (owner) logins and views, with cost hidden at the database level; WhatsApp Cloud API notifications (active once WHATSAPP_* is set); monthly reports with printable view and CSV; retrieval mode for large knowledge bases (pinned policies + hybrid vector/keyword search, English rewrite for Tamil/Hindi questions); conversation export; visitor data deletion; nightly retention clean-up; 80% quota warning.
Phase 3: Shopify and WooCommerce order lookup with customer verification and encrypted per-bot credentials; callback/appointment booking with a date/slot card; weekly report email.
Needs your accounts to run for real: Anthropic (replies, evals), Resend (emails), WhatsApp Cloud API (template approval), Voyage (optional), a client's Shopify/WooCommerce store. Everything else is tested end to end on a local harness.
来源:GitHub · github.com