Your AI. Every provider. One memory.
Lucy runs OpenAI, Claude, Gemini, and local models like Ollama in one interface that remembers your work, connects to your tools, and keeps your keys on your machine.
Lucy is a closed environment, with a brain. Most AI platforms are someone else's cloud — Lucy is a sealed room inside your company. She remembers your decisions and context (the brain), keeps every byte on your deployment under your keys (the walls), and embeds into any app you own with one line of code (the doors). Your company, your data, and nobody else's business.
Switch AI models mid-conversation | Memory that compounds | MCP connector marketplace | Visual workflow builder | Run local LLMs for full privacy | Voice in, voice out | Embed with one line of code | Open source. Self-hostable. Free.
Built for teams that want to test, compare, and ship AI — self-hosted, on infrastructure you control.
📚 Documentation: in-app at /docs when Lucy is running, or browse the markdown in docs/kb/ — guides for users (chat, memory, personas, connectors, workflows, voice, themes) and developers (architecture, self-hosting, embedding, HTTP API, MCP server, contributing).
Lucy is a self-hosted AI platform for teams that want to roll out AI tooling without handing their data to someone else's cloud. It connects to OpenAI, Anthropic Claude, Google Gemini, and local models via Ollama and LM Studio through a unified provider interface, streams responses in real time, and lets you switch models mid-conversation. All chat history and settings are stored either locally in the browser (zero-infrastructure standalone mode) or persisted to a Supabase PostgreSQL database when you need multi-user, cross-device access.
Beyond chat, Lucy ships a visual workflow builder — a drag-and-drop canvas powered by React Flow — where you can compose multi-step AI pipelines from a rich node library (AI Agent, Knowledge Base, Condition, Filter, Transform, Code, HTTP, Send Email, Integration, …). In connected mode workflows run server-side and durably (a run survives closing the tab, with saved history and per-node logs), fire automatically on triggers (schedule, webhook, or record-change events), retry failed runs with backoff, support run cancellation, and keep published versions you can restore. On the desktop app they run locally in the browser.
The integration layer lets other applications register themselves with Lucy's project registry. Once registered, Lucy can read live data from the app's Supabase tables, inject it into the AI system prompt as context, and trigger actions on the app's behalf. An embeddable widget ships via a single <script> tag for easy integration into any web page.
Replace the placeholders below with actual screenshots once the application is running.
| Screen | Description |
|---|---|
docs/screenshots/chat.png |
Main chat interface — conversation sidebar, model selector, streaming message output |
docs/screenshots/workflow-builder.png |
Visual workflow canvas with node panel, configuration sidebar, and run panel |
docs/screenshots/personas.png |
Personas management page — built-in and custom AI personas |
docs/screenshots/onboarding.png |
4-step company onboarding wizard |
docs/screenshots/settings.png |
Settings page — API key management, local models detection, theme toggle |
docs/screenshots/integrations.png |
Integrations page listing registered connected projects |
docs/screenshots/embed-widget.png |
Embeddable chat widget mounted in an external application |
- Connect to OpenAI (GPT-4o, GPT-4o Mini, GPT-3.5 Turbo), Anthropic (Claude Sonnet 4, Claude Haiku 4), Google (Gemini 2.0 Flash, Gemini 1.5 Pro), DeepSeek (V3 Chat, R1 Reasoner), Groq (Llama 3.3 70B, Llama 3.1 8B, Gemma 2), Mistral (Large, Small), xAI (Grok 2, Grok Beta), OpenRouter (gateway), and local models (via Ollama or LM Studio) from a single interface
- Real-time streaming responses via Server-Sent Events — tokens render as they arrive
- Per-conversation model switching — change provider and model without starting a new session
- Conversation history with auto-generated titles, searchable sidebar
- Message editing — edit any user message inline; saving re-sends the conversation from that point
- Response regeneration — regenerate any assistant reply with a single click
- Conversation export — download any chat as Markdown or JSON from the chat header menu
- Rate limiting — chat API enforces 30 requests per minute per IP to protect server-level API keys
- Lucy remembers across conversations — facts, preferences, working style, and what happened when (semantic / pragmatic / episodic)
- Always-on profile + searchable collection — a compact identity block is always present; relevant memories are retrieved per message via hybrid search (pgvector + full-text, fused with Reciprocal Rank Fusion)
- Entity salience — recurring terms (clients, products, jargon) auto-promote in importance the more they appear
- Reconciliation-aware capture — one end-of-conversation pass extracts, de-duplicates, and reconciles (ADD / UPDATE / MERGE / SKIP); a privacy guard never stores secrets or PII
/rememberpins a fact instantly;/globalcontributes it to shared knowledge; incognito skips capture for a session- Admin-gated with storage-usage visibility, configurable contradiction policy (supersede / keep-history), and a staged archive → grace-window → delete flow
- Dual-mode by design — full semantic memory in connected (Supabase) mode; a lexical fallback engine for standalone mode
- Pluggable embedder — pick OpenAI
text-embedding-3-smallor a fully-local Ollama model (e.g.embeddinggemma) in Settings → Memory; the pgvector column reshapes to match the chosen dimension automatically
lucy chat— streaming chat in any shell, with the same memory and encrypted provider keys as the web app- Rich REPL — welcome banner, Markdown-rendered replies (code blocks, lists, bold), a thinking spinner,
/-command Tab autocomplete, and boxed output; defaults to a model whose provider you actually have a key for - Pipe-friendly one-shots —
cat error.log | lucy chat "explain this"(plain text when piped) lucy models / memories / screenings / admin / whoami— manage your deployment without leaving the terminal- Thin client over Lucy's HTTP API, authenticated with a Lucy API key; configure once with
lucy login(~/.lucy/config.json, orLUCY_URL/LUCY_API_KEYenv vars) - Run via
npm run lucy -- <cmd>in the repo, ornpm linkfor a globallucycommand
- Browse, install, configure — a curated catalog of MCP connectors (GitHub, Slack, Notion, Postgres, Linear, Stripe, Brave Search, Filesystem, Fetch) installable per user with one click
- Tool use in chat — Lucy calls connector tools mid-conversation (OpenAI-compatible + Anthropic providers); tool calls render as live status chips above the streaming reply
- Encrypted secrets — connector credentials are AES-256-GCM encrypted at rest and never returned to the browser
- Approval gating — flag any installation so write-like actions (create/update/delete/send) require your approval instead of executing silently
- Mic input — browser Web Speech (live interim text) or cloud Whisper/Deepgram press-to-record
- Read-aloud — per-message speaker button plus auto-read; browser voices, OpenAI TTS, or any OpenAI-compatible local server
- Configured in Settings → Voice; keys travel via request headers only
- Ships as a downloadable Electron app for Windows (
.exe), macOS (.dmg), and Linux (.AppImage) — runs the full Lucy server locally - Local-first — no account required; chats, memory, and provider keys stay on your machine in standalone mode
- First-run wizard — pick a cloud API key or a local Ollama / LM Studio model and start
- Connect to Cloud — optionally push local chats and settings to a justlucy.ai account (one-way, idempotent sync)
- OS-detecting download page at
/download
- Run Lucy as a Telegram bot from Admin → Channels — paste a BotFather token, register the webhook, and chat with the same memory and encrypted keys
- Shared mode (one bot on your keys, optional Telegram user-ID allowlist) or Linked mode (each user binds their own account with a
/linkcode)
- Luminous (default — glassy purple glow), Industrial (sharp, structured), Editorial (bold typography, stark contrast), Minimal dark, and Light
- CSS-variable design tokens restyle the entire app instantly; picker in Settings → General; set in Manrope via
next/font
Type / in the chat box for an autocomplete menu of slash-commands:
| Command | Effect |
|---|---|
/remember <text> |
Save a fact to memory |
/forget <text> |
Forget memories matching the text |
/global <text> |
Save shared knowledge for everyone |
/memories |
Show what Lucy remembers (count + recent) |
/incognito |
Toggle: don't capture memories this session |
/new |
Start a new conversation |
/help |
List all commands |
Arrow keys + Enter/Tab to apply, Esc to dismiss. Arg-commands prime the input; no-arg commands fire immediately. The registry lives in lib/chat/slash-commands.ts.
- 5 built-in personas with purpose-built system prompts:
- General Assistant — helpful and balanced for everyday tasks
- Code Expert — senior developer persona for TypeScript, React, and Node.js
- Creative Writer — storytelling, copywriting, and creative content
- Data Analyst — SQL, pandas, statistics, and data visualisation
- Onboarding Guide — patient guide for helping new employees
- Create custom personas — define your own name, icon, description, and system prompt on the
/personaspage - Persona selector chip in the chat input — pick a persona without leaving the conversation
- Built-in personas cannot be deleted; custom ones can be edited or removed
- Active persona system prompt is injected at the start of each new conversation
- Personas are persisted to
localStorageunder thelucy-personaskey
- Per-message token estimates — shown on hover for each message (heuristic: ~4 chars/token)
- Conversation total displayed in the chat header as
~N tokens this conversation - Implemented in
lib/utils/tokens.tsviaestimateTokens()andestimateConversationTokens()
- Run AI models entirely on your own machine via Ollama or LM Studio
- Both expose an OpenAI-compatible API — Lucy connects without any API key
- Auto-detection — the model selector probes localhost at startup and lists available models automatically
- Detect Models button in Settings discovers what is currently loaded
- Configurable server URLs (defaults:
http://localhost:11434for Ollama,http://localhost:1234for LM Studio) - Gracefully skipped when local servers are not running — cloud providers continue to work
- Login, signup, and forgot-password pages built with Supabase Auth
- Google OAuth — one-click sign-in via Google
- Auth middleware — protects
/chat,/workflows, and/settingsroutes when Supabase is configured; redirects unauthenticated users to/auth/loginwith aredirectToparam - In standalone mode (no Supabase configured), auth is fully bypassed and all routes are public
- User avatar and sign-out in the header when signed in
- Two-factor authentication — email-OTP and TOTP authenticator-app 2FA, with a login challenge gate
- Account security — password reset via emailed code, active-device tracking, and a profile / account page
- Transactional email — reset and 2FA codes sent over SMTP (configure the
SMTP_*env vars) /personasroute is always public (not protected by auth middleware)
- Full Web App Manifest with name, short name, display mode, theme colour, and icons
- Favicon and app icons render the Lucy brand mark —
app/icon.svg(favicon), 192×192 and 512×512 PWA icons, and an Apple touch icon - Can be installed as a standalone app on desktop and mobile
- Drag-and-drop canvas built on React Flow (@xyflow/react)
- Rich node library — AI Agent (pick provider + model), Knowledge Base, Condition, Filter, Transform, Code (JS), HTTP, Send Email, Integration, Output
- Durable server-side execution (connected mode) via an in-process worker + a Postgres run queue — runs survive closing the tab, with saved history, per-node logs, and cancellation; the desktop app runs workflows in-browser
- Triggers — run automatically on a schedule (cron), from a webhook (secret URL), or on record events (row created/updated/deleted)
- Retry/backoff + idempotency for trigger-fired runs; DRAFT/PUBLISHED versions with restore
- Execution log panel with per-node input/output inspection
- Syntax-highlighted code blocks rendered via rehype-highlight
- Language label shown in the block header
- Copy button on every code block
- Line numbers shown automatically when a block has more than 5 lines
- Horizontally scrollable to handle wide code without breaking layout
- Semantic HTML throughout:
<header role="banner">,<nav role="navigation">,<main>,<form role="form">, message list as<div role="log" aria-live="polite"> - All interactive elements have
aria-labelattributes - Persona selector uses
role="listbox"/role="option"/aria-selected - Conversation list items have
aria-currentfor the active conversation - Export button has
aria-expandedandaria-haspopup - Header user menu has
aria-expandedandaria-haspopup - Mobile hamburger button has
aria-expandedandaria-label
- Hamburger menu in the header on small screens — tap to open a full-width dropdown nav
- Sidebar overlay — on mobile, the conversation sidebar slides in as a fixed overlay with a backdrop
- Swipe-to-close — swipe left on the mobile sidebar to dismiss it (60 px threshold)
- Sidebar close button visible in mobile overlay mode
- Selecting a conversation auto-closes the mobile sidebar
- Chat input bottom bar wraps gracefully on narrow screens
Cmd/Ctrl+K— focus the conversation search inputCmd/Ctrl+Shift+N— start a new chatEscape— close the sidebar on mobileEnter— send message (in chat input)Shift+Enter— insert newline in chat inputCmd/Ctrl+Enter— save an inline message editEscape— cancel an inline message edit
- Standalone mode (default): zero configuration, all data in browser
localStorageunder thelucy-namespace. No backend required. - Connected mode: point
NEXT_PUBLIC_SUPABASE_URLandNEXT_PUBLIC_SUPABASE_ANON_KEYat a Supabase project and Lucy switches automatically to PostgreSQL-backed persistence with Supabase Auth - The entire application is written against a
StorageAdapterinterface — swapping backends requires no component changes
- Light and dark themes — toggle from the header or Settings page
- No flash on load — an inline
<script>in the root layout reads the stored theme before React hydrates, so there is never a white flash on a dark-mode page - ThemeProvider keeps the
<html>class in sync with the Zustand store in real time - Loading skeletons for the chat and workflows pages — shown while JavaScript bundles load
- Global loading page — animated bouncing dots with the Lucy logo
- Error boundary (
app/error.tsx) catches React render errors and shows a friendly "Try again" screen - 404 page (
app/not-found.tsx) with a link back to chat
- Registry — external apps call
registerProject()to describe their tables, columns, and available actions - Context builder — at chat time, Lucy queries the registered app's live Supabase data and injects a concise summary into the LLM system prompt (token-budget aware)
- Action executor — four handler types:
supabase-insert,supabase-update,api-call, andworkflow - Embeddable widget — serve Lucy as an iframe chat panel in any web app via a single
<script>tag
- 4-step guided wizard: Welcome (company name) → API Keys → Test Chat → Invite
- API keys are stored locally or encrypted in Supabase; they never leave the user's own infrastructure
- Profile Verification — automatic one-shot review of a contractor's profile with AI-generated grade (1–5), strengths, and concerns
- Project Screening — client-initiated multi-step screening: Lucy generates 5–8 tailored questions, the contractor answers, then Lucy grades the fit
- Grading scale: Excellent Match (5) → Not Recommended (1)
- Results visible only to the screening creator (RLS-enforced)
- API endpoints:
POST /api/screening/start,GET/POST /api/screening/:id,GET /api/screening
- External applications authenticate to Lucy using per-user API keys
- Key format:
lucy_k_<24-random-base64url-chars>— only the SHA-256 hash is stored in the database - Full key shown once on creation, never again
- Manage keys from the Settings page (Supabase mode only)
- API routes:
POST /api/keys(create),GET /api/keys(list),DELETE /api/keys(revoke/delete)
- Lucy exposes screening tools via the Model Context Protocol for use with Claude Code, Cursor, and other MCP-compatible editors
- 5 tools:
start_screening,get_screening,list_screenings,submit_screening_answers,verify_contractor_profile - Run with
npm run mcp— uses stdio transport
- Node.js 18 or later
- npm 9 or later
- At least one AI provider API key (OpenAI, Anthropic, or Google) — or a running Ollama instance for local-only use
# 1. Clone the repository
git clone https://github.com/JustLucyHQ/lucy.git
cd LucyAI
# 2. Install dependencies
npm install
# 3. Create your local environment file
cp .env.example .env.local
# 4. Open .env.local and add your API keys (see Configuration below)
# 5. Start the development server
npm run dev
# 6. Open the application
# http://localhost:3001On first launch you will be redirected to the onboarding wizard to set your company name and configure API keys.
Lucy ships with a multi-stage Dockerfile and a docker-compose.yml for running the production build in a container.
# Build and start the container
# API keys are picked up from your shell environment or a .env file
OPENAI_API_KEY=sk-... docker-compose up --build
# Or export variables first, then run
export OPENAI_API_KEY=sk-...
export ANTHROPIC_API_KEY=sk-ant-...
docker-compose up --buildThe app is available at http://localhost:3000.
# Build
docker build -t lucy-ai .
# Run
docker run -p 3000:3000 \
-e OPENAI_API_KEY=sk-... \
-e ANTHROPIC_API_KEY=sk-ant-... \
lucy-aiThe build uses a three-stage approach:
- deps — installs production dependencies only (
npm ci --omit=dev) - builder — copies source and runs
next build(telemetry disabled) - runner — minimal
node:20-alpineimage runningserver.jsas a non-root user
The docker-compose.yml maps OLLAMA_URL and LM_STUDIO_URL to host.docker.internal so the containerised app can reach Ollama or LM Studio running on the host machine.
Lucy has 282 tests across 49 test suites written with Jest and React Testing Library. A representative sample:
# Run all tests once
npm test
# Run in watch mode (re-runs on file change)
npm run test:watch
# Type-check without running tests
npx tsc --noEmit| Suite | File | What is covered |
|---|---|---|
| Provider registry | __tests__/lib/providers/index.test.ts |
getProvider, getProviderForModel, getAllModels, getModelsByProvider |
| Local storage adapter | __tests__/lib/storage/local.test.ts |
LocalStorageAdapter CRUD, conversation/message/preference/provider config |
| Workflow engine | __tests__/lib/workflow/engine.test.ts |
Start→Output execution, edge following, condition branching, transform operations |
| Integration registry | __tests__/lib/integrations/registry.test.ts |
registerProject, getProject, getAllProjects, getProjectTables, getProjectActions |
| Markdown utilities | __tests__/lib/utils/markdown.test.ts |
hasMarkdown, generateConversationTitle, truncate |
| ModelSelector component | __tests__/components/chat/ModelSelector.test.tsx |
Renders provider groups, onChange callback fires with correct model id |
| Button component | __tests__/components/ui/Button.test.tsx |
Renders children, variant classes, loading spinner, disabled state, click behaviour |
- Framework: Jest 29 with
ts-jestfor TypeScript transpilation - Environment:
jsdom(browser-like, suitable for React component tests) - Setup file:
jest.setup.tsloads@testing-library/jest-dommatchers - Module alias:
@/maps to the project root (matchestsconfig.jsonpaths) - Test location: all test files live under
__tests__/mirroring the source tree
All configuration is done through environment variables. Copy .env.example to .env.local and fill in the values relevant to your setup.
| Variable | Required | Description |
|---|---|---|
OPENAI_API_KEY |
Optional* | OpenAI API key. Required to use GPT-4o, GPT-4o Mini, or GPT-3.5 Turbo. Format: sk-... |
ANTHROPIC_API_KEY |
Optional* | Anthropic API key. Required to use Claude Sonnet 4 or Claude Haiku 4. Format: sk-ant-... |
GOOGLE_API_KEY |
Optional* | Google AI API key. Required to use Gemini 2.0 Flash or Gemini 1.5 Pro. Format: AIza... |
DEEPSEEK_API_KEY |
Optional* | DeepSeek API key. Required to use DeepSeek V3 (Chat) or DeepSeek R1 (Reasoner). Format: sk-... |
GROQ_API_KEY |
Optional* | Groq API key (very fast inference). Format: gsk_... |
MISTRAL_API_KEY |
Optional* | Mistral API key (Mistral Large / Small). |
XAI_API_KEY |
Optional* | xAI API key for Grok models. Format: xai-... |
OPENROUTER_API_KEY |
Optional* | OpenRouter API key — one key, many models. Format: sk-or-... |
NEXT_PUBLIC_SITE_URL |
Optional | Canonical public URL of the deployment (e.g. https://justlucy.ai). Drives OG / canonical / manifest URLs; inlined at build time. |
SMTP_HOST / SMTP_PORT / SMTP_USER / SMTP_PASS / SMTP_FROM |
Optional | SMTP credentials for password-reset and 2FA emails (connected mode). |
NEXT_PUBLIC_SUPABASE_URL |
Optional | Your Supabase project URL (e.g. https://xyz.supabase.co). When omitted, Lucy runs in standalone localStorage mode. |
NEXT_PUBLIC_SUPABASE_ANON_KEY |
Optional | Your Supabase project's anon public key. Required when NEXT_PUBLIC_SUPABASE_URL is set. |
SUPABASE_SERVICE_ROLE_KEY |
Optional | Service-role key for screening + memory APIs (bypasses RLS). Required when using the screening or connected-memory endpoints. |
LUCY_ADMIN_EMAIL |
Optional | When set, only these signed-in users may change global memory settings (Settings → Memory). Accepts a single email or a comma-separated list. Unset means any authenticated user may toggle. |
LUCY_URL |
Optional | Lucy's own URL for MCP callbacks. Default: http://localhost:3001. |
LUCY_API_KEY |
Optional | API key for MCP server and external integrations (e.g., Contractors Room). |
OLLAMA_URL |
Optional | Base URL for the Ollama server. Default: http://localhost:11434. |
LM_STUDIO_URL |
Optional | Base URL for the LM Studio local server. Default: http://localhost:1234. |
*At least one AI provider key is required to send messages to cloud models. Local models via Ollama or LM Studio require no API key. Keys can also be entered per-user through the Settings page; server-level keys act as a shared fallback.
Authentication is built on Supabase Auth and is completely optional. When NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY are set, auth is automatically enabled. When they are not set, Lucy runs in anonymous standalone mode and all routes are public.
| Route | Description |
|---|---|
/auth/login |
Email + password sign-in and Google OAuth |
/auth/signup |
New account creation with email confirmation |
/auth/forgot-password |
Send a password-reset email |
/auth/callback |
Supabase OAuth redirect handler (server route) |
middleware.ts runs on every request. When auth is enabled it:
- Lets all requests to
/auth/*,/api/*,/onboarding,/embed,/personas, and static assets through unconditionally. - Checks the Supabase session cookie for requests to
/chat,/workflows, and/settings. - Redirects unauthenticated requests to
/auth/login?redirectTo=<original-path>.
If anything goes wrong during session verification the middleware fails open (allows the request) to avoid blocking the app.
In your Supabase project → Authentication → Providers → Google, enable Google and add your OAuth client ID and secret. Add https://your-lucy-instance.com/auth/callback to the authorised redirect URIs in your Google Cloud Console project.
Personas configure the AI's personality and expertise by injecting a system prompt at the start of each conversation.
| Persona | Icon | Description |
|---|---|---|
| General Assistant | 🤖 | Helpful and balanced for everyday tasks |
| Code Expert | 💻 | Senior developer — TypeScript, React, Node.js |
| Creative Writer | ✍️ | Storytelling, copywriting, and creative content |
| Data Analyst | 📊 | SQL, data analysis, charts, and statistics |
| Onboarding Guide | 🎓 | Helps new employees learn company tools and processes |
- Click Personas in the header navigation, or click Create Custom in the persona selector dropdown in the chat input
- On the
/personaspage, click Create Custom - Fill in: icon (emoji), name, short description, and system prompt
- Click Save Persona — the persona appears immediately in the selector
Custom personas can be edited or deleted at any time. Built-in personas are read-only. The active persona is highlighted with a purple border and "Active" badge.
The persona selector chip sits in the chat input bar next to the model selector. Click it to open a dropdown listing all personas. The selected persona's system prompt is used for new conversations. Switching personas mid-conversation does not affect already-sent messages.
lucy-ai/
├── app/ # Next.js 16 App Router
│ ├── layout.tsx # Root layout — inline theme script, AuthProvider > StorageProvider > StoreSync > ThemeProvider
│ ├── page.tsx # Landing / redirect
│ ├── icon.tsx # Dynamic favicon 32×32 (purple "L", edge runtime)
│ ├── icon-192.tsx # PWA icon 192×192 (edge runtime)
│ ├── icon-512.tsx # PWA icon 512×512 (edge runtime)
│ ├── manifest.ts # Web App Manifest (PWA)
│ ├── error.tsx # React error boundary page — "Something went wrong" + Try again
│ ├── not-found.tsx # 404 page with link back to chat
│ ├── loading.tsx # Global loading fallback (animated dots)
│ ├── globals.css # Global styles + Tailwind imports
│ ├── chat/
│ │ ├── page.tsx # Main chat page (sidebar + window + input + keyboard shortcuts)
│ │ └── loading.tsx # Chat page skeleton (header + sidebar + message area)
│ ├── workflows/
│ │ ├── page.tsx # Workflow list (grid of cards)
│ │ ├── loading.tsx # Workflows page skeleton
│ │ └── [id]/page.tsx # Workflow editor (canvas + toolbar + panels)
│ ├── personas/
│ │ └── page.tsx # AI persona manager (create, edit, delete, set active)
│ ├── settings/
│ │ ├── page.tsx # API keys, local models, theme, data
│ │ └── integrations/ # Integration management page
│ ├── onboarding/ # 4-step onboarding wizard
│ ├── embed/ # Embeddable widget page (loaded in iframe)
│ ├── auth/
│ │ ├── login/page.tsx # Sign-in page (email + Google OAuth)
│ │ ├── signup/page.tsx # Account creation page
│ │ ├── forgot-password/page.tsx # Password reset request page
│ │ └── callback/route.ts # Supabase OAuth callback handler
│ └── api/
│ ├── chat/route.ts # POST — streaming AI chat via SSE (rate-limited: 30 req/min/IP)
│ ├── models/route.ts # GET — list models; ?includeLocal=true probes Ollama/LM Studio
│ ├── embed/route.ts # GET — serve embeddable widget loader script
│ ├── keys/route.ts # GET/POST/DELETE — API key management
│ └── screening/ # AI screening endpoints (start, get, submit, list)
│
├── components/
│ ├── ThemeProvider.tsx # Keeps <html> class in sync with Zustand theme state
│ ├── chat/
│ │ ├── ChatWindow.tsx # Message list, token counter, export menu
│ │ ├── ChatInput.tsx # Textarea + persona selector + model selector + send/stop
│ │ ├── ChatMessage.tsx # Single message — markdown, code blocks, edit, regenerate, token count
│ │ ├── ChatSidebar.tsx # Conversation list, search, mobile overlay, swipe-to-close
│ │ ├── ModelSelector.tsx # Provider/model dropdown with local model detection
│ │ └── PersonaSelector.tsx # Persona chip + dropdown in the chat input bar
│ ├── workflow/ # WorkflowCanvas, NodePanel, NodeConfigPanel, RunPanel, WorkflowToolbar
│ │ └── nodes/ # Individual node components (one per node type)
│ ├── onboarding/ # OnboardingWizard
│ ├── embed/ # LucyWidget (iframe chat widget)
│ ├── layout/
│ │ ├── Header.tsx # Top nav — theme toggle, user avatar, sign-out, mobile hamburger
│ │ └── Sidebar.tsx # Generic sidebar shell
│ └── ui/ # Primitive UI components — Button, Input, Card, Select, Badge, Avatar, Spinner
│
├── lib/
│ ├── providers/ # AI provider implementations
│ │ ├── types.ts # AIProvider, AIModel, ChatMessage, ProviderName (openai|anthropic|google|local)
│ │ ├── openai.ts # OpenAI streaming adapter
│ │ ├── anthropic.ts # Anthropic streaming adapter
│ │ ├── gemini.ts # Google Gemini streaming adapter
│ │ ├── local.ts # LocalProvider — Ollama + LM Studio via OpenAI-compatible API
│ │ └── index.ts # Provider registry — getProvider(), getAllModels(), getModelsByProvider()
│ │
│ ├── storage/ # Storage abstraction layer
│ │ ├── index.ts # StorageAdapter interface + data types
│ │ ├── local.ts # LocalStorageAdapter (standalone mode)
│ │ ├── supabase.ts # SupabaseStorageAdapter (connected mode)
│ │ └── provider.tsx # React context — detects mode and supplies the active adapter
│ │
│ ├── store/ # Zustand client stores
│ │ ├── conversations.ts # Conversation list state
│ │ ├── chat.ts # Active chat state
│ │ ├── settings.ts # User preferences + provider config + ollamaUrl + lmStudioUrl
│ │ ├── personas.ts # AI personas state — 5 built-ins + custom, persisted to localStorage
│ │ └── StoreSync.tsx # Bootstrap component: loads stores from adapter on mount
│ │
│ ├── workflow/ # Workflow engine and storage
│ │ ├── types.ts # All workflow types — NodeType, WorkflowNode, ExecutionResult, etc.
│ │ ├── engine.ts # Execution engine — walks the graph, runs nodes in order
│ │ ├── registry.ts # Node type registry — maps NodeType to React component + config defaults
│ │ ├── storage.ts # Workflow CRUD over the active StorageAdapter
│ │ └── store.ts # Zustand store for workflow editor state
│ │
│ ├── integrations/ # External app integration layer
│ │ ├── registry.ts # registerProject(), getProject(), getAllProjects()
│ │ ├── context.ts # buildProjectContext() — injects live data into system prompt
│ │ ├── actions.ts # executeAction() — runs integration actions
│ │ └── contractors-room.ts # Built-in Contractors Room integration definition
│ │
│ ├── auth/ # API key authentication
│ │ └── api-keys.ts # generateKey, hashKey, validateApiKey, createApiKey, revokeApiKey
│ │
│ ├── screening/ # AI screening engine
│ │ ├── index.ts # startScreening, submitAnswers, getScreening, listScreenings
│ │ ├── types.ts # Screening, ScreeningGrade, ContractorProfile, GRADE_LABELS
│ │ └── grading.ts # LLM prompt builders + response parsers
│ │
│ ├── mcp/ # Model Context Protocol server
│ │ └── server.ts # Standalone MCP server (5 screening tools, stdio transport)
│ │
│ ├── supabase/ # Supabase client setup
│ │ ├── client.ts # Browser-side Supabase client (db: { schema: 'lucy' })
│ │ ├── server.ts # Server-side Supabase client (db: { schema: 'lucy' })
│ │ ├── auth.tsx # AuthProvider context — signIn, signUp, signOut, signInWithGoogle
│ │ ├── schema.sql # Database migration — lucy schema tables + RLS policies
│ │ ├── api_keys.sql # API key table migration
│ │ └── screening_rls_fix.sql # Multi-tenancy RLS fix
│ │
│ ├── scripts/ # CLI utilities
│ │ └── seed-admin-key.ts # Generates API key for admin@contractorsroom.com
│ │
│ └── utils/
│ ├── markdown.ts # Markdown rendering helpers + generateConversationTitle
│ ├── stream.ts # SSE encoder utilities for streaming API responses
│ └── tokens.ts # Token estimation — estimateTokens(), estimateConversationTokens()
│
├── __tests__/ # Jest test suites (282 tests, 49 suites)
│ ├── components/
│ │ ├── chat/ModelSelector.test.tsx
│ │ └── ui/Button.test.tsx
│ └── lib/
│ ├── integrations/registry.test.ts
│ ├── providers/index.test.ts
│ ├── storage/local.test.ts
│ ├── utils/markdown.test.ts
│ └── workflow/engine.test.ts
│
├── middleware.ts # Route protection — redirects unauthenticated users when Supabase is enabled
├── jest.config.ts # Jest configuration (ts-jest, jsdom, @/ alias)
├── jest.setup.ts # Jest setup — loads @testing-library/jest-dom matchers
├── Dockerfile # Multi-stage production Docker build
├── docker-compose.yml # docker-compose for running Lucy with env var passthrough
├── .dockerignore # Excludes node_modules, .next, tests from Docker build
├── .env.example # Environment variable template
├── tailwind.config.ts # Tailwind configuration
├── next.config.js # Next.js configuration
└── tsconfig.json # TypeScript configuration
Supabase is optional. Without it, Lucy runs fully in standalone mode using browser localStorage. When you want persistent, cross-device, multi-user storage with authentication, follow these steps.
Go to supabase.com, create a new project, and note your Project URL and anon public key from Project Settings → API.
NEXT_PUBLIC_SUPABASE_URL=https://your-project-ref.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGci...Open the SQL Editor in your Supabase dashboard and execute the full contents of lib/supabase/schema.sql. This creates all required tables and indexes.
To enable the memory system (connected mode), also run lib/supabase/memory.sql followed by lib/supabase/memory_search.sql. These add the pgvector extension, the memory/entity/profile tables (HNSW + FTS indexes), RLS policies, the single-row memory_settings config, and the hybrid-search RPCs. Memory stays off until an admin enables it in Settings → Memory.
All Lucy tables live in the lucy schema (not public). The schema provides namespacing so tables have clean names without prefixes. Supabase clients must be configured with db: { schema: 'lucy' }.
| Table | Description |
|---|---|
conversations |
One row per conversation. References auth.users. |
messages |
Chat messages. Cascades on conversation delete. Includes model, provider, tokens_used. |
provider_configs |
Per-user API key storage (encrypted at rest). Unique per (user_id, provider). |
user_preferences |
Per-user settings — theme, default model/provider, company name. |
memory_profiles |
Always-on per-user (and per-project) profile, merge-updated JSONB. |
memories |
Durable semantic/pragmatic/episodic memories with halfvec(1536) embeddings + FTS, visibility, source, and bi-temporal valid_at/invalid_at. |
entities |
Salient terms (clients, products, jargon) with occurrence/recency-driven importance. |
memory_entities |
Join table wiring memories ↔ entities. |
memory_settings |
Single-row deployment config — memory gate, embedder, contradiction policy, deletion grace window. |
workflows |
Workflow definitions. Stores nodes and edges as JSONB. |
workflow_runs |
Execution history — inputs, outputs, logs, status, and timing. |
screenings |
AI screening records (grade, questions, transcript). |
screening_answers |
Individual Q&A answers per screening. |
api_keys |
Per-user API keys (SHA-256 hashed, prefix for display). |
RLS is enabled on all tables. The policies ensure:
- Users can only read and write their own conversations, messages, API configs, preferences, and workflows.
- Messages are indirectly scoped through their parent conversation's
user_id. - Screenings use
created_by = auth.uid()— only the creator sees their screenings.
No service-role key is required for normal operation — the anon key with the user's JWT is sufficient. The screening API uses the service-role key to bypass RLS.
Lucy supports running AI models locally with zero API cost. Both Ollama and LM Studio expose an OpenAI-compatible REST API that Lucy connects to automatically.
# 1. Install Ollama from https://ollama.com
# 2. Pull a model
ollama pull llama3.1
ollama pull mistral
# 3. Start the server (runs on http://localhost:11434 by default)
ollama serve- Download from lmstudio.ai
- Download a model from the Discover tab
- Go to Local Server → Start Server (runs on
http://localhost:1234by default)
Once a local server is running:
- Open Lucy → Settings → scroll to Local Models
- Click Detect Models to discover what is available
- The model selector in the chat input will now show a Local group with your running models
- Select any local model and chat normally — no API key required
The integration layer lets any external application make its data and actions available to Lucy's AI. The pattern is intentionally simple: register a descriptor object once at startup and Lucy handles the rest.
import { registerProject } from '@/lib/integrations/registry';
registerProject({
id: 'my-app',
name: 'My Application',
description: 'Short description for the AI system prompt',
supabaseSchema: 'my_app', // Supabase schema where your tables live
tables: [
{
name: 'projects',
description: 'Active client projects with budgets and status',
columns: [
{ name: 'name', type: 'text', description: 'Project name' },
{ name: 'budget', type: 'numeric', description: 'Approved budget' },
{ name: 'status', type: 'text', description: 'Current status' },
],
accessPolicy: 'user', // 'user' | 'public' | 'admin'
},
],
actions: [
{
id: 'create-project',
name: 'Create Project',
description: 'Insert a new project row',
parameters: [
{ name: 'name', type: 'string', required: true, description: 'Project name' },
{ name: 'budget', type: 'number', required: false, description: 'Budget amount' },
{ name: 'company_id',type: 'number', required: true, description: 'Client company ID' },
],
handler: 'supabase-insert',
config: { table: 'projects', schema: 'my_app' },
},
],
});Call registerProject() once during server startup (e.g., inside an API route module or a Next.js instrumentation file). On the client side you can call it inside a useEffect or a provider component.
| Handler | Description |
|---|---|
supabase-insert |
INSERT a row into a Supabase table. config needs table and optional schema. sender_id / user_id are injected automatically. |
supabase-update |
UPDATE a row matching matchColumn. config needs table, schema, and matchColumn. |
api-call |
POST params as JSON to an external HTTP endpoint. |
workflow |
Trigger a Lucy workflow by workflowId via /api/workflows/[id]/run. |
lib/integrations/contractors-room.ts ships as the first reference integration. It registers 6 tables (user_profiles, companies, projects, project_tasks, messages, member_cvs) and 3 actions (create-project, send-message, update-contract-status) against the contractors_room Supabase schema.
It is automatically registered in app/api/chat/route.ts:
import { registerContractorsRoom } from '@/lib/integrations/contractors-room';
registerContractorsRoom();Add Lucy as a floating chat panel to any web page with a single script tag:
<script
src="https://your-lucy-instance.com/api/embed?project=contractors-room&model=gpt-4o&theme=dark&position=bottom-right"
async
></script>Query parameters:
| Parameter | Default | Options |
|---|---|---|
project |
(none) | Any registered integration id |
model |
gpt-4o |
Any model id (e.g. claude-sonnet-4-6) |
theme |
dark |
dark, light, auto |
position |
bottom-right |
bottom-right, bottom-left, inline |
The script creates a fixed-position iframe (340 × 520 px) pointing at Lucy's /embed page. Using an iframe avoids React version conflicts with the host application.
The workflow builder gives you a visual canvas to compose multi-step AI pipelines. Each node in the graph represents a single processing step. Edges connect nodes to define execution order.
| Node | Description |
|---|---|
| Start | Entry point for every workflow. Defines named input variables (e.g. user_query) that are passed through the pipeline and can be referenced by downstream nodes. |
| LLM | Calls an AI model from any registered provider. Configure system prompt, temperature, max tokens, and which workflow variable to use as input. |
| Condition | Branches the graph based on a field evaluation. Supports operators: contains, not_contains, equals, not_equals, greater_than, less_than, regex, starts_with, ends_with. |
| Knowledge Base | Queries a named knowledge collection for semantically similar content. Returns the top-K results into the workflow context. |
| Transform | Mutates a value without calling an AI. Operations: template, uppercase, lowercase, trim, extract_json, combine, replace. |
| HTTP Request | Makes an outbound HTTP request (GET, POST, PUT, PATCH, DELETE) with configurable headers, body, and timeout. Response is injected into the workflow context. |
| Integration | Runs an action on a registered Lucy integration (see Integrations). Maps workflow variables to action parameters. |
| Output | Terminal node. Marks the final value to surface as the workflow result. Supports text, markdown, and json display formats. |
When you click Run, the execution engine:
- Finds the
startnode and resolves initial variable values from the run panel inputs - Traverses the graph in topological order, executing one node at a time
- Each node receives the current variable context, produces an output, and the engine merges it back into the context
- Condition nodes evaluate their expression and activate only the matching outgoing edge
- Execution state (
idle,running,success,error) is broadcast back to the canvas in real time so each node shows its live status - A full
ExecutionResultwith per-node logs (input, output, duration, error) is available in the Run panel after completion
| Technology | Version | Role |
|---|---|---|
| Next.js | 16 | Full-stack React framework, App Router, API routes |
| React | 19 | UI library |
| TypeScript | 5 | Static typing across the entire codebase |
| Tailwind CSS | 3.4 | Utility-first styling |
| React Flow (@xyflow/react) | 12 | Workflow canvas — nodes, edges, drag-and-drop |
| Zustand | 5 | Lightweight client-side state management |
| Supabase JS | 2 | PostgreSQL persistence + Auth (optional) |
| OpenAI Node SDK | 6 | OpenAI client (also drives Ollama / LM Studio + other OpenAI-compatible providers) |
| Anthropic SDK | 0.10x | Anthropic Claude client |
| Google Generative AI | 0.24 | Google Gemini client |
| @modelcontextprotocol/sdk | 1 | MCP — connector client + screening-tools server |
| grammY | 1 | Telegram bot framework |
| cron-parser | 4 | Workflow schedule (cron) parsing |
| Nodemailer | 8 | SMTP email (password reset, 2FA codes) |
| Electron | 42 | Desktop app shell |
| electron-builder | 26 | Desktop installers (.exe / .dmg / .AppImage) |
| react-markdown | 10 | Markdown rendering for AI responses |
| rehype-highlight | 7 | Syntax highlighting in code blocks |
| remark-gfm | 4 | GitHub Flavored Markdown support |
| lucide-react | 1 | Core icon set |
| @lobehub/icons | 5 | AI-provider brand icons |
| simple-icons | 16 | Company / connector brand icons |
| zod | 3 | Schema validation |
| Jest | 30 | Test runner |
| React Testing Library | 16 | Component testing utilities |
| Method | Path | Description |
|---|---|---|
POST |
/api/chat |
Streams an AI response as Server-Sent Events. Body: { messages, model, provider, projectId?, userId? }. API keys passed via x-openai-key, x-anthropic-key, or x-google-key headers (falls back to server env vars). Local provider passes x-ollama-url / x-lmstudio-url. Rate limited to 30 requests/minute/IP. |
GET |
/api/models |
Returns all available models grouped by provider. Add ?includeLocal=true to also probe Ollama and LM Studio for currently-loaded models. |
GET |
/api/embed |
Serves the embeddable widget JavaScript loader. Query params: project, model, theme, position. Response is application/javascript with Access-Control-Allow-Origin: *. |
POST |
/api/screening/start |
Start a new AI screening. Requires API key auth (Authorization: Bearer lucy_k_...). |
GET |
/api/screening/:id |
Get screening status and results. |
POST |
/api/screening/:id |
Submit contractor answers to a screening. |
GET |
/api/screening |
List screenings with optional filters. |
POST |
/api/keys |
Create a new API key (requires Supabase session). |
GET |
/api/keys |
List API keys for the current user (prefix only). |
DELETE |
/api/keys |
Revoke or delete an API key. |
GET |
/auth/callback |
Supabase OAuth redirect handler. Exchanges the auth code for a session and redirects to the app. |
# Run the development server with hot reload
npm run dev
# Run the test suite
npm test
# Run tests in watch mode
npm run test:watch
# Type-check the project
npx tsc --noEmit
# Lint
npm run lint
# Production build
npm run build
# Serve production build locally
npm run startLucy exposes its screening tools via the Model Context Protocol. To use them with Claude Code, Cursor, or other MCP-compatible editors:
cd C:\RepositoryAI\LucyAI
npm run mcpAdd to your claude_desktop_config.json:
{
"mcpServers": {
"lucy": {
"command": "npx",
"args": ["tsx", "C:\\RepositoryAI\\LucyAI\\lib\\mcp\\server.ts"],
"env": {
"NEXT_PUBLIC_SUPABASE_URL": "http://localhost:8000",
"SUPABASE_SERVICE_ROLE_KEY": "your-service-role-key",
"LUCY_API_KEY": "lucy_k_your-key-here"
}
}
}
}| Tool | Description |
|---|---|
start_screening |
Start a new contractor screening |
get_screening |
Get screening status and results |
list_screenings |
List screenings with filters |
submit_screening_answers |
Submit contractor answers |
verify_contractor_profile |
One-shot profile verification |
Lucy and Contractors Room share the same local Docker Supabase instance:
- Lucy uses the
lucyschema - Contractors Room uses the
contractors_roomschema - PostgREST exposes both:
PGRST_DB_SCHEMAS=public,storage,graphql_public,cj,contractors_room,lucy - Shared authentication — same
auth.userstable, one login for both apps
| Metric | Count |
|---|---|
| App Router pages | 34 |
| API routes | 40 |
| React components | 67 |
| Lib modules | 102 |
| Test suites | 49 |
| Tests | 282 |
| AI providers | 9 (OpenAI, Anthropic, Google, DeepSeek, Groq, Mistral, xAI, OpenRouter, Local) |
| Built-in personas | 5 |
| Workflow node types | 11 |
| MCP tools | 5 (screening) |
| Source files (TS/TSX) | 302 |
| Lines of code | ~28,000 |
Recently shipped (previously on this list): Electron desktop app (.exe / .dmg / .AppImage) · cross-conversation memory with vector search · durable server-side workflows with schedule / webhook / record-event triggers · MCP connector marketplace · Telegram channel · voice in / voice out · two-factor authentication.
Still planned:
- WhatsApp integration — Connect via WhatsApp Cloud API or Twilio
- Smart model routing — Automatically pick the cheapest model that can handle each prompt's complexity
- Workflow engine v2 — Iterator / loop and human-input (Form) nodes with run suspend & resume
- Expanded connector catalog — More one-click MCP connectors in the marketplace
- Hosted cloud — Managed Lucy at justlucy.ai for teams who prefer not to self-host
GNU Affero General Public License v3.0 (AGPL-3.0) — see LICENSE for details.
