Lalabase
Tickets, Doku, Zeiterfassung und Kundengespräche an einem Ort, durchsuchbar per KI. Für Softwarefirmen mit Kundenprojekten. Open Source: auf deinem eigenen Server oder in unserer Cloud in Deutschland.
翻译准备中
尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证WazaBolt is your AI business assistant on WhatsApp. It answers customer questions in English, French and Pidgin, shares products and prices, captures orders and hands conversations to your team when needed.
翻译准备中
取自仓库 README
正在加载完整 README。 以下为已保存的摘录。
WazaBolt — Your AI Business Assistant on WhatsApp. Power your business on WhatsApp. Never miss
a customer. WazaBolt is a WhatsApp business automation platform for African businesses, starting in
Cameroon. Businesses connect their business WhatsApp account to
WazaBolt to automate replies, share their catalog, capture orders and manage customers — with
their team always able to take over.
WazaBolt is an independent product and is not affiliated with WhatsApp or Meta. WhatsApp is an
integration channel, connected through Meta's official WhatsApp Business Platform.查看完整 README ↗WazaBolt — Your AI Business Assistant on WhatsApp. Power your business on WhatsApp. Never miss a customer. WazaBolt is a WhatsApp business automation platform for African businesses, starting in Cameroon. Businesses connect their business WhatsApp account to WazaBolt to automate replies, share their catalog, capture orders and manage customers — with their team always able to take over.
WazaBolt is an independent product and is not affiliated with WhatsApp or Meta. WhatsApp is an integration channel, connected through Meta's official WhatsApp Business Platform.
npm install
cp .env.example .env.local # nothing is required yet for the marketing site
npm run dev # http://localhost:3000
Checks: npm run lint, npm test (unit tests) and npm run build.
Next.js 16 (App Router, TypeScript) · Tailwind CSS v4 · shadcn/ui-style components on Radix
(components/ui) · Lucide icons · Zod / React Hook Form (installed for Phase 2+).
app/
[lang]/ root layout (html lang), every page lives under /en or /fr
(marketing)/ /, /features, /how-it-works, /pricing, /solutions, /faq, /resources, /about, ...
(auth)/ /login, /register, /forgot-password, /reset-password
dashboard/ protected SaaS dashboard: home, onboarding, conversations, customers,
products, orders, knowledge, ai (+ ai/languages), automations,
analytics, whatsapp, team, billing, settings
opengraph-image localized link-preview image
auth/confirm/ email-link handler (not localized)
brand-assets/ internal: generated social templates (/brand-assets/*.png, not linked)
global-not-found.tsx, icon.svg, apple-icon.png, manifest.ts, robots.ts, sitemap.ts
messages/ UI dictionaries: en.ts (source of truth), fr.ts
lib/i18n/ locales, language registry, country packs, routing helpers, dictionaries
lib/ai/ WhatsApp AI: language layer (packs, detection, resolution), prompts, reply schema,
claude.ts (Claude tool loop), pipeline.ts (when/how to answer), context.ts,
service.ts (validation), tools/registry.ts (business-scoped tools)
lib/actions/ Server Actions (auth, settings, business, products, knowledge, customers,
conversations, orders, ai) — all re-check the user's role
lib/data/queries.ts dashboard read models (always RLS-scoped to the user's business)
lib/validation/ Zod schemas shared by forms and Server Actions
lib/whatsapp/ Cloud API client (graph.ts), token encryption, connect/disconnect, webhook ingestion
app/api/whatsapp/ webhook endpoint (GET verification, POST messages + statuses)
types/supabase.ts generated database types (`npm run db:types`)
components/
brand/ WazaBoltLogo, WazaBoltIcon, WazaBoltBadge, AIStatus, ChannelBadge
marketing/ sections + FeatureCard, IndustryCard, PricingCard, SectionHeading, CTASection,
DashboardPreview (dashboard-mock.tsx)
conversations/ WhatsAppChatMockup (customer's view, demo) and dashboard chat primitives
dashboard/ sidebar, mobile nav, user card, language settings form
app/ dashboard building blocks: page header, panels, empty states, stat cards,
badges, tables, action forms (useActionState), product/customer/order forms
motion/ Reveal (scroll reveal), CountUp
ui/ Button (the WazaBolt button: default/outline/secondary/ghost/dark/gold variants),
badge, card, accordion, sheet, input, label
config/ site (name, taglines, nav), plans, solutions, FAQ, dashboard nav
lib/brand/ mark SVG + brand hex (icons), icon tones, social image generator
public/logo/ exported logo files (icon, horizontal, compact, dark, mono, PNG sizes)
assets/ Plus Jakarta Sans subset + JPEG photos used only by the image generator
/product and /industries redirect permanently to /how-it-works and /solutions.
Tokens live in app/globals.css (@theme scales plus --brand-* variables). The brand
styleboard is an internal design reference: it is applied through these tokens and the logo
components, and is not shown as a page on the public site.
| Colour | Hex | Token | Use |
|---|---|---|---|
| Waza Green | #16B878 | waza-500 / --brand-primary | Primary buttons, links, active states, AI indicators |
| Deep Teal | #102A2A | deep / --brand-secondary | Headings, navigation, dark sections, footer, logo text |
| Golden | #FFC83D | gold / --brand-accent | The bolt, badges, small accents |
| Coral | #FF6B4A | coral-500 / --brand-alert | Alerts and rare highlights only |
| Soft Mint | #E9FAF3 | mint / waza-50 | Section backgrounds, subtle cards |
| Warm White | #FFFDF8 | cream / --brand-background | Page background |
| Slate | #526262 | slate / --brand-text-muted | Secondary text |
Accessibility rules: Waza Green buttons carry Deep Teal text (5.9:1 — white would be 2.6:1).
Green headline text uses waza-600 (#12A06A, large text); green body text and links use
waza-700 (#0C8354). Gold always carries Deep Teal text.
Typography: Plus Jakarta Sans (headings, 600–800) + Inter (body, 400–600) via next/font.
Type utilities: type-display, type-h1, type-h2, type-h3, type-lead, type-body,
type-small, type-label.
Logo: a rounded conversation bubble with a bold white "W" and a Golden lightning bolt breaking through its top-right corner. Variants: horizontal (with tagline), compact (stacked), icon only, dark background, monochrome. Favicon and the WhatsApp profile image use the icon only.
Messages: descriptor "Your AI Business Assistant on WhatsApp." · primary "Power your business
on WhatsApp." · supporting "Never miss a customer." (all in config/site.ts).
Design rules: moderately rounded cards (rounded-2xl), soft shadows, subtle borders, generous
spacing, green actions, gold accents, one Deep Teal contrast section per page, Lucide icons only,
restrained motion (disabled under reduced motion). Unbuilt features are always labelled
Coming Soon / Planned.
The chat-* colours exist only to depict a customer's WhatsApp chat in demo mockups. WazaBolt is
not affiliated with WhatsApp or Meta, and the WhatsApp logo itself is never used.
| Area | Status |
|---|---|
| Marketing site, brand system, logo, SEO/OG/manifest, responsive layout | Functional (static pages) |
| Hero chat, live demo, dashboard preview | UI mockup — demo conversation / example data |
| Pricing | Functional display from config/plans.ts (proposed prices) |
| Mobile Money, voice notes | Shown as Planned — not in the first release |
| Auth: register, email confirmation, login, logout, password reset, protected dashboard | Functional (Phase 2) |
| Database: users, businesses, business_members, audit_logs with RLS | Functional (Phase 2) |
| Dashboard (Stage 1): home with real metrics, 6-step onboarding, products (variants, images, stock), knowledge (FAQs + documents), customers, conversations (Take Over / Return to AI), orders, AI settings, analytics, WhatsApp status, team, billing, settings | Functional — real data only; empty states say "No data yet" |
| Roles owner / admin / agent / viewer | Enforced in Server Actions and by RLS |
| Website + dashboard in English and French (URLs, metadata, hreflang, sitemap, emails) | Functional |
| Languages & AI style settings (reply languages, default, mode, tone, formality, emoji, length, notes) | Functional — saved to the database |
| Language detection / resolution / prompt builder (en, fr, Cameroonian Pidgin) | Functional code + unit tests; not yet called by a live AI (no WhatsApp webhook yet) |
| WhatsApp (Stage 2): connect a number (verified with Meta, token encrypted), webhook (signature-checked, idempotent), customers + conversations created automatically, team replies from the inbox, delivery ticks, read receipts, 24-hour window, media stored privately | Functional — see "WhatsApp setup" below |
| AI replies (Stage 3): Claude answers WhatsApp customers from the catalog, FAQs, policies and hours; looks up products/stock, records orders, hands over to the team; respects Human Mode, after-hours settings and the plan's monthly allowance; usage logged | Functional — needs ANTHROPIC_API_KEY; see "AI replies" below |
WazaBolt is multilingual from the start, on three layers:
1. UI (website, auth, dashboard) — English and French. Every page lives under /en or /fr
(app/[lang]). proxy.ts sends unprefixed URLs to the visitor's saved choice (NEXT_LOCALE
cookie), else their browser language, else English. Strings live in messages/en.ts (source of
truth) and messages/fr.ts; TypeScript fails the build if a key is missing. Server Components use
getMessages(); Client Components use useI18n(). Validation and auth errors are keys, translated
by the form. Pages have localized titles, canonical + hreflang alternates, and the sitemap lists
both languages. Signed-in users' dashboard language is stored in users.ui_locale and in their auth
metadata, which the bilingual email templates read (.Data.locale).
2. Data — languages are rows, not columns. languages (BCP-47 codes; Cameroonian Pidgin is
wes), country_packs + country_pack_languages (Cameroon: XAF, Africa/Douala, en/fr/wes),
business_languages + businesses.default_language, ai_settings (language mode and response
style), and per-customer preferred_language with its source (explicit_request,
set_by_business, inferred). Messages record detected language, confidence, secondary language
and whether the message was mixed. Sign-up applies the country pack automatically.
3. WhatsApp AI (lib/ai/). For every inbound message:
detect.ts scores each supported language (handles mixing, missing accents, SMS spelling, Pidgin's
English vocabulary) → explicit-request.ts catches "reply in English" / "en français svp" /
"you fit tok Pidgin?" (negations ignored) → resolve.ts picks the reply language
(business fixed mode > explicit request > saved preference > confident detection > conversation
language > default; disabled languages fall back, Pidgin → English) → prompts/system-prompt.ts
builds a cache-friendly prompt (platform rules + one guide per language, then the business's
languages and style, then a per-turn language decision) → reply-schema.ts defines the structured
reply the model must return, including the language it used.
Adding a language or country: add it to lib/i18n/languages.ts and the languages table, write
an AI pack in lib/ai/language/packs/ (detection words, request names, prompt guidance, formality
rules, fixed messages), and list it in a country pack. For a new dashboard language, add a
dictionary in messages/ and register it in lib/i18n/config.ts and lib/i18n/load.ts.
Before launch: the French UI copy and the French/Pidgin fixed messages and prompt guidance
(lib/ai/language/packs/fr.ts, wes.ts, marked needs-review) should be reviewed by fluent
speakers. Pidgin is labelled Beta in the dashboard.
Every WhatsApp message is stored with a message_type: text, image, audio, document,
video or location (migration 20260930120000_multimodal_messages.sql).
| Type | MVP status | What happens |
|---|---|---|
| Text | Processed | Language pipeline → AI reply |
| Voice note (audio) | Architecture ready | Stored privately; customer gets a polite "please type" reply in their language; team notified. Switch on once a speech-to-text provider is chosen (Transcriber in lib/messaging/ports.ts). |
| Image | Processed (Stage 5) | Stored privately, then shown to the assistant with its caption; it searches the catalog and compares with product photos (viewProductPhotos). If the photo can't be opened, the customer gets a short notice and the team is flagged. |
| Document, video, location | Stored, not processed | Team notified; shown in the dashboard. |
lib/messaging/whatsapp/webhook.ts (verify X-Hub-Signature-256, parse all types) →
plan.ts (what to do per type, from capabilities.ts) → whatsapp/media-downloader.ts (Cloud API,
server token, type/size/checksum checks) → media-store.ts (private bucket) → transcription /
vision → ai-input.ts (the customer turn the model sees).message_transcriptions, separate from the audio file, with their
own detected language (English, French, Pidgin). The dashboard shows the player and the
transcript side by side (components/conversations/message-content.tsx).search_catalog; decideCatalogMatch() only lets a single clear, confident match be
quoted, otherwise the assistant asks which product or hands over. Results are recorded in
message_image_analyses.whatsapp-media (created by the migration, no public access, no browser
policies); the dashboard gets 5-minute signed URLs after a Row Level Security membership check.
Storage paths are built from ids, never filenames. Logs record ids and error codes, never media
or message content. Media is marked for deletion after 90 days by default (delete_after);
transcripts and analyses remain..env.local (and your host's environment): NEXT_PUBLIC_SUPABASE_URL,
NEXT_PUBLIC_SUPABASE_ANON_KEY, and NEXT_PUBLIC_SITE_URL (your site's public URL).
The service-role key is only needed for webhooks/media later — never expose it to the browser.supabase/migrations/, in filename order — paste each into the SQL
editor, or run npx supabase link then npx supabase db push.NEXT_PUBLIC_SITE_URL; add
<site-url>/auth/confirm (or <site-url>/**) to Redirect URLs.whatsapp-media (private) and product-images (public read, owner/admin
upload into their own business folder) are created by the migrations — check they exist under
Storage after applying them.supabase/templates/confirmation.html into
"Confirm signup" (subject: Confirm your WazaBolt account / Confirmez votre compte WazaBolt) and
recovery.html into "Reset password" (subject: Reset your WazaBolt password / Réinitialisez
votre mot de passe WazaBolt). The body switches between English and French from the user's
locale. These links use token_hash, so they work even if the email is opened on a different
device.Supabase Auth applies its own rate limits to sign-ups, logins and emails (configurable under Auth → Rate limits). Consider enabling CAPTCHA protection before public launch.
proxy.ts (Next.js 16's replacement for middleware) refreshes the session cookie on each
request and redirects signed-out visitors away from /dashboard.lib/auth/dal.ts re-checks the user on the server for every protected page and action;
the database enforces tenant isolation with Row Level Security.httpOnly, SameSite=Lax and Secure in production. Only server code
talks to Supabase.# Unit tests: language detection, explicit requests, reply-language resolution, prompts
npm test
# Database: tenant isolation, RLS, multilingual, multimodal, SaaS foundation, WhatsApp, AI and
# business-operations schema
# (local or staging DB — never production)
DATABASE_URL=postgres://... npm run test:db
# End-to-end: needs Supabase Auth + DB + Mailpit, e.g. `npx supabase start`,
# then `npm run build && npm start` with the local Supabase URL/key in .env.local
NEXT_PUBLIC_SUPABASE_ANON_KEY=... npm run test:e2e
# Stage 1 dashboard: onboarding, CRUD, takeover, orders, tenant isolation, mobile layout
NEXT_PUBLIC_SUPABASE_ANON_KEY=... npm run test:e2e:dashboard
# Stage 2 WhatsApp, against a local fake Graph API (tests/e2e/fake-graph.mjs, port 4010).
# Start the app with WHATSAPP_GRAPH_API_BASE_URL=http://localhost:4010 and test values for
# WHATSAPP_APP_SECRET / WHATSAPP_VERIFY_TOKEN / WHATSAPP_TOKEN_ENCRYPTION_KEY / SUPABASE_SERVICE_ROLE_KEY.
NEXT_PUBLIC_SUPABASE_ANON_KEY=... WHATSAPP_APP_SECRET=... WHATSAPP_VERIFY_TOKEN=... [DATABASE_URL=...] npm run test:e2e:whatsapp
# Stage 3 AI replies, against a scripted fake Anthropic API (tests/e2e/fake-anthropic.mjs, port 4020).
# Start the app with ANTHROPIC_API_KEY=test-key ANTHROPIC_BASE_URL=http://localhost:4020 AI_DEBOUNCE_MS=1500 too.
# The photo checks need Supabase Storage (included in `npx supabase start`).
NEXT_PUBLIC_SUPABASE_ANON_KEY=... WHATSAPP_APP_SECRET=... npm run test:e2e:ai
# WazaBolt Docs: guest generator and business documents.
NEXT_PUBLIC_SUPABASE_ANON_KEY=... DATABASE_URL=postgres://... npm run test:e2e:documents
# Colours and sizes: product page, till, order form, assistant (fake Graph + fake Anthropic).
NEXT_PUBLIC_SUPABASE_ANON_KEY=... DATABASE_URL=postgres://... WHATSAPP_APP_SECRET=... npm run test:e2e:product-options
# Stage 6 appointments (fake Graph + fake Anthropic, like the AI suite)
NEXT_PUBLIC_SUPABASE_ANON_KEY=... DATABASE_URL=postgres://... WHATSAPP_APP_SECRET=... npm run test:e2e:appointments
# Stage 7 notifications (fake Graph with templates); start the app with CRON_SECRET=test-cron too
NEXT_PUBLIC_SUPABASE_ANON_KEY=... DATABASE_URL=postgres://... WHATSAPP_APP_SECRET=... npm run test:e2e:notifications
# Stage 8 broadcasts (fake Graph with templates; CRON_SECRET=test-cron)
NEXT_PUBLIC_SUPABASE_ANON_KEY=... DATABASE_URL=postgres://... WHATSAPP_APP_SECRET=... npm run test:e2e:broadcasts
# Stage 4 business operations (stock, usage banner, plan requests, invitations, roles).
# Uses DATABASE_URL directly to play the WazaBolt operator (approving a plan) and simulate AI usage.
NEXT_PUBLIC_SUPABASE_ANON_KEY=... DATABASE_URL=postgres://... npm run test:e2e:operations
# V2 sales, stock, customers, expenses, dashboard, reports, CSV, isolation, cashier permissions,
# phone layout and French (DATABASE_URL is used to check the ledger directly)
NEXT_PUBLIC_SUPABASE_ANON_KEY=... DATABASE_URL=postgres://... npm run test:e2e:pos
# Regenerate database types after a migration
DATABASE_URL=postgres://... npm run db:types
WazaBolt uses Meta's official WhatsApp Business Platform (Cloud API) with one Meta app for the platform; each business connects its own number.
supabase/migrations/20261002120000_whatsapp_integration.sql (after the earlier ones).NEXT_PUBLIC_):
SUPABASE_SERVICE_ROLE_KEY, WHATSAPP_APP_SECRET, WHATSAPP_VERIFY_TOKEN,
WHATSAPP_TOKEN_ENCRYPTION_KEY (openssl rand -base64 32). Redeploy.https://<your-site>/api/whatsapp/webhook, verify token = WHATSAPP_VERIFY_TOKEN,
then subscribe to the messages webhook field. The app must be in Live mode for real customers.whatsapp_business_messaging and
whatsapp_business_management, issued for the platform's Meta app. WazaBolt checks the number
belongs to the account, reads its display name, subscribes the app to the account's webhooks,
and only then shows Connected. The token is encrypted (AES-256-GCM) before it is stored in
whatsapp_credentials, a table no browser role can read.How it behaves:
whatsapp-media bucket (signed URLs, 90-day retention).processing_status = received
for the AI stage to pick up.When a customer writes, the webhook stores the message and answers after responding to Meta
(lib/ai/pipeline.ts):
REPLY_DEBOUNCE_MS in config/economics.ts, 6 s; AI_DEBOUNCE_MS overrides in tests) so a burst of messages gets one answer, to the newest.lib/ai/images.ts: decoded and re-encoded as JPEG,
at most 1024 px, before anything is sent). It searches the catalog with words describing the photo
and can look at up to 2 catalog photos (viewProductPhotos, 512 px thumbnails, only images from
WazaBolt's own product-images storage) before saying "we have it". Image tokens grow with
pixels, so a photo message costs about as much as one or two text messages. Businesses can switch
photo understanding off (AI Assistant → "Understand customer photos", migration
20261006120000_photo_setting.sql): photos are then passed to the team with a short notice. It never confirms a payment from a
screenshot. Voice notes and files get a short notice in the customer's language and are flagged
for the team.lib/ai/claude.ts). The model uses tools scoped to the business —
searchProducts, viewProductPhotos, checkProductStock, getBusinessHours, getDeliveryFee, getOrderStatus, createOrder,
createCustomer, requestHumanAgent — and finishes with send_reply (structured reply).needs_human switches the
conversation to Human Mode and flags it.ai_usage (model, tokens, tool calls, outcome — never content), shown on
the AI Assistant page.Test chat (AI Assistant → Test chat, agents and above): talk to the assistant as a customer
without WhatsApp, including sending a product photo. Same prompt, knowledge, tools and language rules; catalog lookups are real, but
orders, customer details and handovers are simulated (dryRun), nothing is sent and the transcript
stays in the browser. Runs on Claude Haiku 4.5 and is limited to 10 messages per business per hour (config/economics.ts); runs are logged in ai_usage
with reason test_chat and left out of the monthly figures.
Prices, rates, limits and budgets (Claude and Meta rates, FCFA rate, Mobile Money fee, plan prices,
margin target, model choice) all live in config/economics.ts (server-only).
Model: Claude Haiku 4.5 (REPLY_MODEL in config/economics.ts; Sonnet 5.5 only if
USE_SONNET_FOR_REPLIES is switched on; env AI_MODEL overrides for a quick rollback). Output is
capped by the business's reply length (MAX_OUTPUT_TOKENS: 300 / 450 / 700). The tools, platform
rules and business knowledge are a cached prefix (kept above Haiku's 4,096-token caching minimum —
checked in tests/unit/reply-budget.test.ts), cached for 5 minutes, or 1 hour for a business with 3+
AI replies in the past hour. Before each reply, the up-to-8 products matching the conversation
(PREFETCH_PRODUCTS; the latest message first, then the recent turns, with English / French / Pidgin
everyday words ignored) go into the turn context, so most price and stock questions take one Claude
request instead of search → reply. Only the last 6 messages are sent (HISTORY_MESSAGES); once
that window is full the assistant keeps a short running summary of the conversation
(conversations.ai_summary, apply supabase/migrations/20261011120000_conversation_summary.sql).
FAQs and policies up to KNOWLEDGE_CACHED_CHARS (12,000 characters, by priority) stay in the
cached prompt; beyond that, at most 3 items matching the customer's message are sent with it.
No Claude call for stickers and reactions (ignored on arrival), emoji-only messages, the same text
repeated within 10 minutes, pastes over 2,000 characters or 3+ links (the team is flagged for those),
or a customer past 15 AI replies in an hour (their messages wait for the team; nothing is dropped).
Rules layer (lib/ai/rules): short single questions are answered from the business's own data
without Claude — greetings and thanks, opening hours, the address, a delivery fee the business wrote
for the named place, one clearly matching product's price and stock. Keywords in English, French and
Pidgin, with typo tolerance; anything that looks like an order, a complaint, bargaining, a request for
a person, a photo or more than one question goes to Claude. Logged in ai_usage as model rules
(reason rules_<intent>); the test chat shows "Answered instantly … (no AI used)". Set
ANTHROPIC_API_KEY in Vercel (server-side only) and redeploy; the AI Assistant page shows Live
once the key is set, WhatsApp is connected and AI is switched on.
Apply supabase/migrations/20261005120000_business_operations.sql (after the earlier ones).
Stock. Orders created from now on take their items out of stock — the variant's stock when the variant tracks it, otherwise the product's (empty stock = not tracked, never touched). An order for more than is in stock is refused with the item's name, from the dashboard and from the AI assistant (which then tells the customer what's available). Cancelling an order gives the stock back; deleting one that still holds stock too; un-cancelling takes it again (refused if it's been sold meanwhile). Each product has a low-stock alert level (default 5): the dashboard home lists products and variants at or below it, and Products → Stock "Low" filters by each product's own level. Orders created before this migration never took stock, so cancelling them doesn't add any.
Team. Owners and admins invite people from Team: enter their email and role, then share the link (copy, or "Share on WhatsApp") — WazaBolt doesn't email it. The link works once, for that email only, for 7 days; only its hash is stored, so it's shown once ("New link" makes a fresh one and the old one stops working). A new person creates their account from the link (no business of their own); someone who already has a WazaBolt account logs in and clicks Join, then switches between businesses from the name at the top of the dashboard. Admins manage agents and viewers; only the owner invites or manages admins; the owner can't be removed. Anyone else can leave a business.
AI allowance. ai_usage_status() returns the plan's allowance and this usage month's use; the
AI pipeline, the billing page and the dashboard banner all use it. An AI conversation is one
customer's 24-hour window in which Claude replied (ai_conversation_windows); answers from the
rules layer alone don't count. Paid plans count per month of their billing period (yearly plans:
each month of the year), Free per calendar month. Businesses that signed up before Step 4 keep the
old count (conversations answered this calendar month; Free 50) until subscriptions.rules_from:
their next billing period, or 1 November 2026 for Free; the dashboard shows a notice until then.
Owners and admins are warned from 80 %; once it's used up, conversations already going continue,
the rules still answer simple questions and other messages go to the team, with an Upgrade link.
Protecting the margin (Step 4). Apply supabase/migrations/20261012120000_margin_protection.sql. Per business and usage month, the pipeline compares our Claude
spend with the plan's hidden budget (claudeBudgetFcfa() in config/economics.ts, never shown to
businesses) — lib/billing/guard.ts:
[email protected]) is emailed when a business passes 80 % and 100 % of its budget,
and when yesterday's total Claude spend is more than 30 % above the day before (each alert once).Prepaid billing. Plans are paid in advance, monthly or yearly (10 months for 12). On Billing
an owner or admin requests a plan, or a renewal of their paid plan, with a phone number to reach
them. Each request emails the team and shows up in WazaBolt admin → Plan requests
(/en/admin/plan-requests, platform admins only, with a badge while requests wait). Take the
payment (Mobile Money, outside WazaBolt for now), then Approve with the amount received, the
method and the Mobile Money reference: it's recorded (subscription_payments, shown to the owner on
Billing) and the plan starts now — or, for a renewal paid before the end or in the grace days, the
next period follows on from the current one. Decline emails the customer too.
A daily job (/api/cron/billing, 05:00 UTC, vercel.json; needs CRON_SECRET) emails the owner a
renewal reminder 5 and 1 days before a paid period ends; at the end the plan is payment due for 3
days (banner, email), then the business moves to Free (nothing is deleted; paying restores the
plan). A Mobile Money provider (Campay or Notch Pay) will plug in through lib/billing/payments.ts.
One Free account per WhatsApp number. A business on Free can't connect a number another
business has used before (whatsapp_number_history); on a paid plan it can.
Emails go through Resend from WazaBolt <[email protected]> (replies go to contact@). Set
RESEND_API_KEY in Vercel (Production, marked Sensitive) and redeploy; without it nothing is
sent (the server logs [email] skipped) and the Plan requests page says emails are off.
Apply supabase/migrations/20261007120000_appointments.sql. Then, in Appointments → Services &
settings, switch on Take bookings, set the start-time step, how many appointments can happen
at once (chairs/staff), the minimum notice and how far ahead, and add services (duration, price or
"price on request"). Bookable hours are the business's opening hours, in its timezone.
available_slots() lists free start times; book_appointment() books under a per-business lock
and re-checks the slot, so the team and the assistant can never take the same last place.YYYY-MM-DDTHH:mm and are matched against real slots.Apply supabase/migrations/20261008120000_notifications.sql.
notifications.event_key).message_template_status_update so approvals show up without refreshing.vercel.json runs /api/cron/reminders daily at 17:00 UTC (18:00 in Douala) for
appointments starting in the next 2–30 hours. Set CRON_SECRET in Vercel (any long random
string); Vercel sends it to the job, and calls without it are refused. On Vercel's Hobby plan cron
jobs run once a day, which is what this needs.Apply supabase/migrations/20261009120000_broadcasts.sql.
/api/cron/reminders). Each customer gets it
once (broadcast_recipients).WazaBolt works as a shop's daily tool even without WhatsApp: the till, stock, customers' credit, expenses and profit. Amounts are shown as "25,000 FCFA" (XAF and XOF display as FCFA).
| Where | What |
|---|---|
Sales → New sale (/dashboard/sales/new) | Till: search products, quantities, variants, customer (walk-in, existing or new), discount (owners/admins), payment method and reference, part payment — the rest goes on the customer's credit. One sale is one database transaction (create_sale): order, items with their cost at the time of sale, stock movements, payment. Each screen carries a key so a double tap records one sale. |
Sales (/dashboard/sales, /dashboard/sales/[id]) | History with filters (dates, method, status, customer); printable receipt (logo and footer from Settings → Receipts); payments; estimated profit for owners/admins. |
| Products | Selling price, cost price, unit, minimum stock; stock only changes through sales or Update stock (purchase, return, damaged, lost, count) — every change is in the product's stock history with who did it and why. Products with sales are archived, never deleted. |
| Customers | Purchases, total spent, what they owe, last purchase; "Customers who owe" filter; record a payment (settles the oldest sales first, never more than owed); full payment history. |
| Expenses (owners/admins) | Rent, electricity, internet, transport, salaries, marketing, supplier payments, packaging, delivery, other — by month, with totals per category. |
| Dashboard | Today's sales, money received, estimated profit, credit owed; last 7 days and this month (expenses, estimated net profit); 30-day chart; best sellers; stock and customer figures; alerts; recent sales. |
| Reports (owners/admins) | Any period, by day/week/month: sales, cost of goods, gross and net profit (estimated), products, top customers, who owes, low stock, expenses — each section as CSV. |
Profit is an estimate: sales amount minus each item's cost price recorded when it was sold, minus expenses. Items sold without a cost price count as zero cost, and the dashboard and reports say so.
Payments are recorded by hand. WazaBolt doesn't check MTN MoMo or Orange Money yet: the
method and reference are what the cashier typed. A provider integration would call the same
database functions (order_payments.provider is ready for it).
Roles: owner and admin (manager) see everything; agent (cashier) sells, takes payments and
manages customers but sees no cost prices, profit, expenses or reports; viewer (staff) reads only.
The database enforces these rules for expenses, reports, discounts, stock changes and settings (RLS, column grants and checks inside the functions). Cost prices (products.cost_price, order_items.unit_cost) are not readable by cashiers or staff even through the database API: owners/admins read them via product_costs() / order_item_costs() (migration 20261014120000_cost_privacy.sql). A new column on those two tables must be granted to authenticated explicitly.
supabase/seed/demo_mj_fashion.sql creates a separate, fictional MJ Fashion Cameroon (8
products, 6 made-up customers with invalid phone numbers, ~60 sales over 30 days, credit,
payments, stock events and expenses) for an existing account. Set the owner's email at the top
and run it in the Supabase SQL Editor; then switch to the demo shop from the top bar. Running it
twice does nothing. To reset it after a demo, set v_reset to true and run again (it deletes only
that account's demo shop, recognised by its demo marker, then rebuilds it with fresh dates); with
v_rebuild set to false it only deletes it.
Sales and payments emit business events (lib/core/events.ts: sale completed, payment recorded,
credit outstanding) for future WhatsApp / SMS / email channels — none is registered yet. The audit
and phase-by-phase plan is in docs/v2-implementation-plan.md.
CUS-000001): given by the database when a customer is created (by hand, at the till or by a first WhatsApp message), per business, from a locked counter, so two customers created at the same moment never share one. They are never edited or reused (a deleted customer's number is skipped). The internal id stays the key every order points to; the ID is for people (shown, copied, searched).ORD-00001): unchanged format, from the same kind of counter; past 99,999 they grow (ORD-100000) instead of being cut.INV-00067, RCT-00067). No tax is recorded on orders.Definitions (same everywhere — customer_stats, the Customers and Orders pages):
| Term | Meaning |
|---|---|
| Purchases | Orders delivered to the customer (till sales are delivered at once). Cancelled and returned orders don't count. |
| Total spent | Value of those purchases (totals after discounts, with delivery fees). |
| Amount paid / payments received | Money actually collected: valid payments minus refunds (voided ones excluded). |
| Owes | What delivered orders still owe (total − paid). Deposits on orders not delivered yet reduce what they will owe. |
| Balance due (per order) | Total − paid; 0 for cancelled and returned orders. A customer's "owes" equals the balances of their delivered orders. |
| Order value (Orders page) | Totals of the orders shown, cancelled and returned excluded. |
| Last contact | Latest message (either way), order or payment. |
lib/documents/): one model, calculations (calc.ts), EN/FR labels, three PDF templates (Classic, Modern, Minimal) rendered with @react-pdf/renderer, and an HTML preview with the same totals. Used by the guest tool and the dashboard (and later WhatsApp)./[lang]/tools/documents — no account; POST /api/documents/guest returns the PDF and saves nothing (30 PDFs/hour per visitor, hashed IP kept one day).INV-2026-000001); issued documents can't be edited (void + duplicate). Receipts: owners and admins. PDFs are rendered on demand from the saved snapshot. Numbering and defaults per business.docs/wazabolt-docs-phase1.md; architecture and next phases: WAZABOLT_EXPANSION_PLAN.md.products.options holds the options in order ([{name, colour, values: [{value, hex, image_url}]}]); each combination is a product_variants row with options = {"Colour": "Red", "Size": "M"} (its name/value stay filled, "Colour / Size", "Red / M"). Stock, the stock history, create_order and create_sale treat a combination like any variant.checkProductOptions answers "which colours in M?" from real stock and gives the exact combination for createOrder; send_photo sends the catalog photo of the colour asked for (only photos in WazaBolt's own product-images storage), right after the reply.WazaBolt earns money without WhatsApp too: the business tools are limited on Free, and the Boutique
plan sells them without the AI assistant. Prices and limits live in the plans table (the database
enforces them) and are mirrored in config/economics.ts for the website.
| Plan | FCFA / month | Products | Sales / month | Users | Reports | Expenses & profit | AI conversations |
|---|---|---|---|---|---|---|---|
| Free | 0 | 30 | 100 | 1 | last 7 days | – | 30 |
| Boutique | 5,000 | unlimited | unlimited | 1 | full + CSV | ✓ | – |
| Starter | 10,000 | unlimited | unlimited | 2 | full + CSV | ✓ | 500 |
| Business | 25,000 | unlimited | unlimited | 5 | full + CSV | ✓ | 2,000 |
| Pro | 50,000 | unlimited | unlimited | 10 | full + CSV | ✓ | 5,000 |
WB411–WB414).businesses.free_limits_from); they see a notice in the dashboard. New businesses get the Free
limits at once. The demo shop is exempt (free_limits_from = 2099-12-31).Apply supabase/migrations/20261010120000_cost_tracking.sql.
All prices, rates, limits and budgets are in config/economics.ts (server-only — nothing in it
reaches the browser). Meta rates there are marked unverified until checked against Meta's rate
card; each business pays Meta directly, so they are only used for estimates.
Claude calls (claude_calls): one row per Messages API request — customer replies and test
chat, including attempts that then failed — with the model, input / output / cache-read /
cache-write (5-minute and 1-hour) tokens and the cost in USD and FCFA at the rates of the moment.
Internal: no business user can read it, owners included.
WhatsApp sends: every outbound message records the category we sent it as
(messages.wa_category: service for free-form text in the 24-hour window — assistant, team,
in-window notifications, STOP/START confirmations; utility for notification templates; marketing
for broadcasts) and is counted per WhatsApp number, month (UTC) and category in
whatsapp_usage. When Meta's status webhook includes pricing, Meta's own category, billable flag
and pricing type are stored next to ours (messages.meta_*) so the estimates can be checked.
Owners see "Free WhatsApp messages this month — N of 1,000 left" on the dashboard home and the WhatsApp page (service messages from their number, resetting on the 1st, UTC). Counts only — no prices anywhere.
WazaBolt admin (/en/admin/businesses, /en/admin/plan-requests and /en/admin/margins; a "WazaBolt admin" link in the
dashboard sidebar appears only for platform admins, everyone else gets a 404). Businesses lists
every sign-up, newest first: business, owner name and email, plan, whether setup is finished and
WhatsApp connected, team size — account facts only, never a business's customers or conversations.
Margin report (/en/admin/margins): per plan
and per business for a month — revenue (price of active / past-due subscriptions; no payment
history yet), Claude cost (test chat shown separately), Mobile Money fee, margin, the hidden Claude
budget, Claude cost per AI conversation, WhatsApp messages by category and an estimate of what
Meta bills the business. Paid businesses and plans below 60%, and businesses costing money without
paying, are flagged; Free is shown as acquisition cost. Give yourself access in the SQL editor:
insert into public.platform_admins (user_id) select id from auth.users where email = '[email protected]';
Pricing (/en/admin/pricing, Step 5): 1,000 simulated conversations (retail, restaurant,
salon, real estate; English, French, Pidgin) run through the real reply pipeline — batching,
skips, rules, catalog lookup, prompt and cache, tool calls, summary — with Claude stood in for
(lib/simulation/; tokens counted from the real requests, on the expensive side). It reports
messages per conversation, the share answered by rules, tokens per request, cache hit rate,
Claude cost per conversation and how many conversations fit in Meta's free 1,000 messages. Each
plan is simulated at its full allowance (busier businesses reuse the cache more) and checked on
the monthly and the annual price; a calculator tries any price and allowance. Nothing there
changes a price. prints the report; fails if a paid plan's
simulated margin drops below 60%.
public/images/hero-owner.webp and product-robe-wax.webp (low-resolution crops from
the design mockup) with licensed, high-resolution photos — and the JPEG copies in assets/
used by the social image generator (the Instagram story shows the low resolution most).config/site.ts (siteConfig.contact).NEXT_PUBLIC_SITE_URL in production so canonical URLs, the sitemap and OG links are correct.| Business operations (Stage 4): orders take stock automatically (back on cancel/delete, never oversold — the AI included), low-stock alerts; team invitation links, roles, removing/leaving, switching businesses; AI-allowance warnings at 80 % / 100 %; plan-change requests approved by the WazaBolt team |
| Functional — see "Business operations" below |
| Product photos (Stage 5): customers' photos on WhatsApp (and in the test chat) are looked at by the assistant and matched to the catalog, comparing with the catalog's own product photos | Functional — see "AI replies" below |
| Appointments (Stage 6): services with duration and price, bookable in opening hours (slot step, capacity, notice, horizon); the assistant finds free times and books on WhatsApp; Calendar with confirm / done / no-show / cancel; booking from the dashboard | Functional — see "Appointments" below |
| Customer notifications (Stage 7): order updates, appointment confirmations / cancellations / reminders, follow-up after 24 h; WazaBolt's message templates submitted to Meta with review status | Functional — see "Customer notifications" below; goes out once WhatsApp is connected |
| Broadcasts (Stage 8): promotions to customers who agreed (consent on the customer page or START by WhatsApp, STOP always unsubscribes), each submitted to Meta as a marketing template, audience by tags and language, progress and per-customer results | Functional — see "Broadcasts" below |
| Sales, stock, customers & money (V2): point of sale with printable receipts (cash, MTN MoMo, Orange Money, bank transfer, card, credit — recorded by hand), stock ledger with reasons and alerts, customer credit and payments, expenses, dashboard with estimated profit, reports with CSV export | Functional — see "Sales, stock and money (V2)" below; works without WhatsApp |
| Voice transcription, payments | Not started — interfaces only (lib/messaging/ports.ts); voice notes get a short notice and are flagged for the team |
npm run simulatenpm test