Skip to content

Frontdesk

Free, self-hosted AI receptionist for small business websites

Role
Creator & Sole Developer (Open Source)
Dates
Nov 2026
Status
Live
  • NestJS
  • pgvector
  • Socket.IO
  • Claude API
  • TypeScript
  • Next.js
  • PostgreSQL
  • Prisma
  • Redis
  • BullMQ
  • OpenAI API
  • Transformers.js
  • Web Components
  • Docker
  • Vitest
  • Turborepo
Try the live demo
Frontdesk

Summary

A working self-hosted AI receptionist that answers only from a business's own information, takes enquiries, books through Slotwise and hands chats to staff live. It needs no AI key of its own, and 156 API tests pass in CI.

The problem

Small businesses such as clinics, solicitors, salons and tutors answer the same questions all day: prices, opening hours, whether they can book. Questions asked out of hours often become lost customers. Paid AI chat tools cost a monthly fee, keep the data on someone else's servers and often make things up. Frontdesk is a free alternative that the business hosts itself. It only answers from what it has been given, and says "I don't know" instead of guessing.

My role

Solo project. I own the whole project. I wrote the PRD, plan and decision records, and built the API, worker, dashboard, widget, Docker setup, CI and evaluation set.

What I built

  • Wrote the PRD, a 7-phase build plan and four decision records, then built the whole system myself.
  • Built the retrieval pipeline: chunking, background embedding, and pgvector search filtered by workspace.
  • Designed the prompt and guardrails, with tool-call signals for gaps and sources, and reference data wrapped as untrusted.
  • Built live handover over Socket.IO with a Redis adapter, plus the staff dashboard.
  • Built bring-your-own-key: encrypted, write-only workspace keys, and a demo that costs the host nothing.
  • Built the Shadow DOM chat widget, Docker setup, demo mode, CI and an evaluation set.

Architecture

A pnpm and Turborepo monorepo: - apps/api: NestJS with Prisma on PostgreSQL with pgvector. It holds the HTTP API and the WebSocket gateways. A separate worker runs BullMQ jobs: embedding, website import, file parsing, email and nightly jobs (retention and demo reset). - apps/web: Next.js dashboard, landing page and demo page. It proxies /api, including WebSockets, to the API. - packages/widget: the chat widget, a plain Web Component bundled with esbuild into one widget.js. - packages/shared: Zod schemas shared by the API, web app and widget. - evals: the evaluation set and its runner. Redis holds the queues, rate limits, per-conversation reply locks and the Socket.IO adapter. Embeddings come from a local all-MiniLM-L6-v2 model by default, or from OpenAI. Chat replies use the workspace's own key, else the server's key, else none, with Anthropic, OpenAI, Gemini or any OpenAI-compatible server such as Ollama. docker compose runs the web app, API, worker, PostgreSQL and Redis.

Key decisions and trade-offs

Chose: one HNSW index for all workspaces, with every search filtered by workspace and pgvector iterative scans (an exact scan on older versions), re-sorted by distance in the app. Because: one index per workspace, or partitioning, is too many moving parts for small installs. Chose: Socket.IO with the Redis adapter rather than Server-Sent Events, with replies streamed word by word over the same socket. Because: handover needs two-way messages and presence. "Online" means a staff socket is in the workspace room, so there is no presence store to go stale. Chose: bring your own key. A workspace's own key, else the server's key, else none. Keys are AES-256-GCM encrypted, never returned, and only the last 4 characters are shown. Because: a free, self-hosted project can't depend on one person's AI bill. With no key the app still starts, and everything except AI answers works. Chose: a monthly AI limit per workspace, counted in tokens, plus rate limits per visitor, IP and workspace. At the limit the chat takes messages only. Because: AI costs must stay predictable for a small business, and visitors can still leave a message.

Hardest problem and how I solved it

Filtered vector search can silently return nothing. All workspaces share one HNSW index. A plain index scan fetches the nearest chunks from the whole table and only then applies the workspace filter. If other workspaces hold closer chunks, the filter removes them all and the assistant wrongly says "I don't know". In a local test, with 300 near chunks in one workspace and 12 far chunks in another, the HNSW scan returned 0 of 5 results and the exact scan returned 5. The fix reads the pgvector version once. On 0.8 and later it turns on iterative scans, so the scan keeps going until enough rows pass the filter, and results are re-sorted in the app. On older pgvector it turns index scans off, so PostgreSQL reads only that workspace's rows. That fallback is what the live demo uses, because its server has pgvector 0.6. A test seeds three workspaces and checks the third still gets exactly k of its own chunks.

Testing and quality

156 API tests in 12 files (Vitest and Supertest) pass in CI against real PostgreSQL with pgvector and Redis, with a fake AI provider, so no test can call a paid API. They cover sign-in codes, refresh-token rotation and reuse detection, every role, workspace isolation (including vector search), rate limits, the monthly limit, live handover (including two takeovers at once), SSRF protection, uploads and zip bombs, retention, demo mode, and bring your own key: the app boots with no key, keys are never returned or logged, and no workspace can use another's key. A test also fails if any endpoint forgets to say who may call it. CI runs typecheck, lint, tests, build, pnpm audit and a dry run of the 26-case evaluation set. The cases cover grounded answers, honest "I don't know", refusing medical and legal advice, and direct and indirect prompt injection. A real-model score hasn't been measured yet.

Results

  • 156 API tests in CI against real PostgreSQL and Redis
  • 26 evaluation cases, including prompt injection
  • 0 of 5 → 5 own-workspace results from a shared vector index, before and after the fix

Version 1 is built and open source under the AGPL-3.0 licence, with CI passing on main. The public demo replays labelled sample answers, so it costs nothing to host, and visitors can paste their own key for live answers. Try the live demo: open the chat on the made-up Riverside Physio site, or press "Open the demo dashboard" to see the staff side. No password is needed.

Key features

  • Knowledge from FAQs, text pages, PDF and Word uploads, and website import (up to 25 pages).
  • Grounded answers with sources, streamed word by word, and an honest "I don't know".
  • Plain-English rules plus presets for law firms, clinics and general businesses.
  • Enquiry capture in the chat (with consent), emailed to the business.
  • Booking via a booking link, or real free times from Slotwise booked in the chat.
  • Live handover to staff, with a take-a-message fallback when nobody is online.
  • Bring your own key: Anthropic, OpenAI, Gemini or any OpenAI-compatible server, including Ollama.
  • Embeddable Web Component widget with Shadow DOM, keyboard and screen-reader support.
  • Owner, Admin and Staff roles, emailed sign-in codes, and data retention set per workspace (default 90 days).

Out of scope

Version 1 leaves out voice and phone calls, WhatsApp and Messenger, payments, multiple languages and a hosted version. The evaluation set hasn't been scored against a real model yet, and the demo's sample answers were written by hand, not recorded from a model.