A modern AI chat application with multi-model support, MCP integration, and per-user data isolation using Cloudflare Workers and Durable Objects.
- Free Models: GPT-4o mini, GPT-4.1 mini/nano, Gemini 2.5 Flash Lite
- BYOK (Bring Your Own Key) Models:
- OpenAI: GPT-4o, GPT-4.1, GPT-5, o3/o4-mini, o3, o3-pro
- Anthropic: Claude Opus 4, Sonnet 4.5/4/3.7, Haiku 3.5
- Google: Gemini 2.5 Flash/Pro
- Advanced AI Capabilities: Streaming responses, extended thinking/reasoning mode, tool calling, vision/image support
- Built-in MCP servers with enable/disable controls
- Custom MCP server support (HTTP/SSE)
- MCP tools exposed to AI models during conversations
- Visual tool call rendering with results
- Chat Management: Create, view, and manage conversations with persistent history
- API Key Management: Securely store encrypted BYOK API keys
- Usage Tracking: Monitor token usage and quotas across models
- Customizable Settings: Configure models, providers, MCP servers, and appearance preferences
- Profile Management: Account settings, session management, and deletion
Better Chat uses a functional, feature-based architecture with clear separation between routes, business logic, and data access. Each feature is self-contained with a consistent structure, making it easy to locate and modify code.
features/[feature]/
├── routes.ts # API routes (thin, validation only)
├── handlers.ts # Business logic orchestration (optional)
├── queries.ts # Database reads
├── mutations.ts # Database writes
├── utils.ts # Pure utility functions
├── types.ts # TypeScript types
└── constants.ts # Constants (optional)
Architecture Principles:
- 100% Functional — No classes, only pure functions
- Thin Routes — Routes validate input, call handlers, return response
- Query/Mutation Split — Clear separation between reads and writes
- Feature Colocation — All related code lives together in one feature directory
- Consistent Naming — Same file names across all features
1. Frontend Request
└─ POST /api/ai/ with messages and conversationId
↓
2. Routes Layer (features/ai/routes.ts)
├─ Validates authentication (requireUserDO)
├─ Parses request body (Zod schema)
└─ Calls streamCompletion(userId, stub, body)
↓
3. Handler Layer (features/ai/handlers.ts)
├─ validateIncomingMessages() — validates messages
├─ getUserSettings() — loads user preferences
├─ requireAvailableQuota() — checks usage limits
├─ resolveModelProvider() — determines provider
├─ createUserProviderRegistry() — sets up AI SDK
├─ getCustomMcpServers() — fetches MCP configs
├─ getMCPTools() — connects to MCP servers
├─ buildSystemPrompt() — constructs prompt
├─ streamText() — streams AI response
├─ onFinish:
│ ├─ userDOStub.appendMessages() — saves to DO
│ ├─ maybeGenerateConversationTitle() — create title
│ ├─ closeMCPClients() — cleanup
│ └─ recordUsage() — tracks tokens
└─ Returns streaming response
↓
4. Data Layer
├─ settings/queries.ts: getUserSettings() → D1
├─ usage/mutations.ts: recordUsage() → D1
├─ tools/mcp/queries.ts: getCustomMcpServers() → D1
└─ DO: appendMessages(), listMessages() → per-user SQLite
↓
5. Infrastructure
├─ D1: Settings, usage tracking, MCP configs
├─ Durable Objects: Per-user conversation storage
├─ AI Providers: OpenAI, Anthropic, Google APIs
├─ MCP Servers: External tool providers
└─ Crypto: API key encryption
Example: Updating User Settings
Most operations use oRPC for type-safe API calls:
1. Frontend: api.settings.update({ theme: "dark" })
2. Routes: settingsRouter.update validates input via Zod schema
3. Handlers: updateUserSettings(userId, input) orchestrates logic
4. Mutations: Writes updated settings to D1 database
5. Response: Typed response back to frontend via oRPC
Benefits of functional feature-based architecture:
- Simplicity: No layers, classes, or abstractions — just functions
- Predictability: Same structure across all features
- Discoverability: Need usage logic? Check
features/usage/
- Maintainability: Changes isolated to one feature directory
- LLM-Friendly: Clear boundaries, consistent patterns, easy to reason about
- Performance: Thin routes and pure functions optimize cold starts
This architecture strikes a balance: structured enough to scale, simple enough to understand quickly. Features are self-documenting through consistent file naming, making it easy for both humans and LLMs to navigate the codebase and make targeted changes without unintended side effects.
- D1 (SQLite): Shared/global data (auth, usage quotas, user settings)
- Durable Objects SQLite: Per-user isolated storage (conversations, messages)
- Per-User Isolation: Each user gets their own Durable Object instance with dedicated SQLite database
- Cloudflare Workers: Edge-native serverless runtime
- Better Auth: Email OTP and social authentication (Google, GitHub)
- KV Sessions: Distributed session storage with rate limiting
- React 19 with React Server Components support
- TanStack Router for file-based routing
- TanStack Query for data fetching and caching
- oRPC for type-safe API calls
- Tailwind CSS 4 for styling
- shadcn/ui for UI components
- Vercel AI SDK for streaming AI responses
- React Markdown with syntax highlighting (Shiki)
- Cloudflare Workers for serverless compute
- Hono for HTTP routing
- oRPC for type-safe RPC
- Drizzle ORM for database operations
- Better Auth for authentication
- Vercel AI SDK for AI provider integration
- Resend for email delivery (production)
- Cloudflare D1: Central SQLite database
- Cloudflare Durable Objects: Per-user stateful storage
- Cloudflare KV: Session and cache storage
- Alchemy: Multi-stage deployment and orchestration
See CLAUDE.md for detailed development instructions, architecture documentation, and code conventions.
# Install dependencies
bun install
# Start development environment (uses .env.dev)
bun a:dev
# Individual apps
bun dev:web # Web app only (port 3001)
bun dev:server # Server only (port 3000)
# D1 (Central Database)
bun db:generate # Generate migrations
bun db:migrate # Run migrations
bun db:push # Push schema changes
bun db:studio # Open Drizzle Studio (dev)
# Durable Objects (Per-User Database)
cd apps/server && bun do:generate # Generate DO migrations
# Deploy to production (uses .env.prod)
bun a:deploy
# Destroy deployment
bun a:destroy
Create stage-specific environment files:
.env.dev
(development).env.prod
(production)
Required variables: BETTER_AUTH_URL
, BETTER_AUTH_SECRET
, CORS_ORIGIN
, API_ENCRYPTION_KEY
, and provider-specific API keys.
See .env.example
files in root and app directories for complete configuration.
MIT