Repository navigation
01. Features Design
| Feature | Description |
|---|---|
| Branching Conversations | Tree-based message storage enabling non-linear dialogue with sibling branches |
| Real-Time Streaming | Server-Sent Events delivering text, reasoning tokens, and tool-call results incrementally |
| Chat Input | Rich composition area with model/tool/knowledge-base selectors, slash commands, and file uploads |
| System Prompt Assembly | Five-layer prompt stack combining user, project, assistant, knowledge-base, and file-access instructions |
| Projects | Organisational workspaces grouping chats under a shared global prompt and optional knowledge base |
| Assistants | Reusable AI persona presets with custom prompts, applicable per-chat or via @ mentions |
| Prompts | Slash-command shortcuts that prepend saved instructions before the user's message |
| Knowledge Bases | Hybrid vector + full-text search retrieval-augmented generation over uploaded documents |
| MCP Integration | Connects external tool servers (stdio and HTTP) for AI tool-calling within chats |
| Artifacts | Side-panel rendering of Markdown, HTML, spreadsheets, and Mermaid diagrams produced by the AI |
| Model Selection | Per-message model choice from any user-configured OpenAI-compatible provider with capability gating |
| File Attachments | Image, document, and spreadsheet uploads via S3-compatible object storage |
| Translation | Standalone AI-powered text and document translation across 20 languages |
| Transform Agents | Multi-step spreadsheet automation pipelines with human review gates |
| Search | Global client-side search across chats, projects, and assistants with type faceting |
| Profile Management | Account settings covering security, sessions, OAuth linking, and account deletion |
| User Settings | App-level configuration including encrypted API key and global system prompt |
| Transactional email delivery via Postmark for authentication lifecycle events | |
| State Management | Zustand-based optimistic state with composable slices for chat and entity data |
| Markdown Rendering | Rich text with syntax highlighting, KaTeX maths, and Mermaid diagram support |
Messages are stored as a tree rather than a flat list. Each message holds a reference to its parent, and the active conversation branch is tracked by a pointer to the deepest leaf node. Edits and regenerations create sibling nodes instead of overwriting existing messages, preserving the full conversation history.
See dedicated wiki page for details: Message Tree and Branching
AI responses are delivered as a Server-Sent Events stream. The client parses incoming events incrementally — text deltas, reasoning tokens, tool-call initiations, tool results, errors, and completion signals — updating the interface in real time as the model generates its response.
See dedicated wiki page for details: AI Responses and Streaming
The chat input is the primary interface for composing messages. It provides a rich set of composition tools accessible from a unified input area.
The main textarea auto-expands as the user types, up to a configurable maximum height. It accepts plain text and supports paste, drag-and-drop, and keyboard shortcuts. Pressing Enter (without Shift) submits the message; Shift+Enter inserts a newline.
Typing / in the input triggers a command palette overlay. The palette searches two sources: user-created prompt shortcuts (filtered by title and shortcut keyword) and MCP prompts from enabled servers. Selecting a command inserts the prompt's content before the user's message text, separated by a double newline. The prompt ID is recorded in the message metadata so the system can trace which shortcut was used.
Typing @ opens a mention palette listing the user's assistants by name and description. Selecting an assistant binds it transiently to that single message — its system prompt is injected into the prompt stack for that request only. The @ mention system is disabled when the chat already has a permanently bound assistant (set at chat creation), since the bound assistant's prompt always applies.
Users can attach files by clicking the attachment button or dragging files onto the input area. Supported types and limits:
- Images: up to 3 per message, maximum 2 MB each
- Documents (PDF, plain text, Markdown): maximum 20 MB each
- Spreadsheets (Excel): maximum 50 MB each
- Total: up to 5 attachments per message
Documents have their text extracted client-side before sending. Spreadsheet files are uploaded to S3 and made available to MCP tools via presigned URLs.
A dropdown in the input toolbar allows the user to choose which AI model processes the current message. Selection is per-message, not per-chat — different messages within the same conversation can use different models. The selector displays model labels grouped by provider and visually indicates capability badges (vision, tool-calling). When editing or regenerating a message, the selector defaults to the model that was originally used.
The tool picker button opens a dialog listing all enabled MCP servers, their discovered tools, and built-in tools (artifact creation, knowledge-base search). Users can toggle individual tools on or off per message. A separate knowledge-base picker allows attaching one or more knowledge bases whose content the AI can query during generation.
The input toolbar sits beneath the textarea and contains (left to right): the attachment menu, model selector, tool picker, knowledge-base picker, and send/stop button. On mobile, the toolbar condenses into a drawer-based layout.
flowchart LR
A[User composes message] --> B[Select model / tools / KB / assistant]
B --> C[Hit Send]
C --> D[Client Streaming Logic]
D --> E[Persist user message to DB]
E --> F[Upload attachments to S3]
F --> G[Reconstruct thread from message tree]
G --> H["POST /api/chat with history"]
H --> I[SSE stream begins]
I --> J["Parse events: text, reasoning, tool-call, tool-result, error, done"]
J --> K[Update Zustand store optimistically]
K --> L[Persist assistant message to DB]
The system prompt sent to the AI model is assembled from up to five layers, each optional. The layers are concatenated in order with double-newline separators. If no layers produce content, a default fallback instruction is used.
| Layer | Source | Purpose |
|---|---|---|
| 1. Global User Prompt | User settings | Baseline instructions that apply to every conversation across all projects and assistants |
| 2. Project Prompt | Project record | Context-specific instructions shared by all chats within a project |
| 3. Assistant Prompt | Assistant record | Persona and behavioural instructions for the selected or mentioned assistant |
| 4. Knowledge Base Instruction | Automatic (if KB active) | Instructs the AI to use the knowledge-base search tool for document-related questions |
| 5. Spreadsheet Access Instructions | Automatic (if spreadsheets attached) | Lists the presigned URLs of attached spreadsheet files and instructs the AI to use MCP tools to read them |
flowchart TB
subgraph System Prompt Assembly
direction TB
L1["Layer 1: Global User Prompt\n(from User Settings)"]
L2["Layer 2: Project Prompt\n(from Project record)"]
L3["Layer 3: Assistant Prompt\n(from Assistant record)"]
L4["Layer 4: Knowledge Base Instruction\n(auto-injected if KB active)"]
L5["Layer 5: Spreadsheet File Access\n(auto-injected if spreadsheets attached)"]
L1 --> L2 --> L3 --> L4 --> L5
end
L5 --> FINAL["Final System Message\n(all layers joined with \\n\\n)"]
The knowledge-base instruction layer tells the AI to call a dedicated search tool when the user's question relates to their uploaded documents. The spreadsheet instruction layer provides the exact file paths or URLs so the AI can pass them directly to Excel MCP tools without asking the user.
The chat endpoint processes each request through seven sequential phases before returning a streaming response.
Phase 1 — Authentication & Validation. The request is authenticated via a Better Auth session check. If the session is missing or expired, a 401 response is returned immediately. The request body is validated against a strict Zod schema covering the chat ID, message history, model selection, and MCP tool/server selections.
Phase 2 — Context Resolution. The pipeline fetches the chat record and verifies ownership. If the chat belongs to a project, the project's global prompt and knowledge-base association are loaded. If an assistant is bound to the chat or mentioned via @, the assistant's system prompt is fetched. The active knowledge-base ID is resolved by checking (in priority order): the per-message selection, the chat-level setting, and the project-level setting.
Phase 3 — AI Provider Initialisation. The system resolves which OpenAI-compatible provider owns the requested model, retrieves the user's encrypted API key for that provider, decrypts it, and creates a configured AI provider instance. If no key is found or decryption fails, the request is rejected with a descriptive error (see wiki/18.-Provider-Registry-&-Model-Management.md).
Phase 4 — MCP Tool Registration. Enabled MCP servers — both user-owned and public community servers — are fetched from the database, filtered by the user's per-message selection, and connected in parallel. Tool definitions are merged into a single registry. Built-in tools (artifact management, knowledge-base search) are added if selected. A cleanup function is prepared for post-stream teardown.
Phase 5 — Message Preparation. The message history is filtered based on model capabilities — image attachments are stripped for non-vision models. Messages are assembled into the format expected by the AI SDK. The system prompt is built from the five-layer stack. Spreadsheet attachments have presigned S3 URLs generated for tool-based access.
Phase 6 — Streaming. The AI SDK's streaming function is invoked with the assembled messages, registered tools (if the model supports tool-calling), a maximum of 10 tool-use steps, and a 60-second timeout. The response is wrapped in a ReadableStream that emits SSE-encoded events for text deltas, reasoning tokens, tool calls, and tool results. Each event is sent to the client as it occurs.
Phase 7 — Post-Stream Cleanup. After the stream completes, the assistant's response is persisted to the database with metadata (model, tool calls, tool results). The chat's current leaf pointer is updated to the new assistant message. MCP server connections are torn down. Any errors during the stream are caught and sent as SSE error events before closing.
sequenceDiagram
participant Client
participant API as POST /api/chat
participant Auth as Better Auth
participant DB as PostgreSQL
participant Provider as OpenRouter
participant MCP as MCP Servers
Client->>API: POST request (chatId, messages, model, tools)
API->>Auth: Validate session
Auth-->>API: Session (userId)
API->>API: Zod schema validation
API->>DB: Fetch chat, project, assistant, KB
DB-->>API: Context records
API->>DB: Fetch user's encrypted API key
DB-->>API: Encrypted key
API->>API: Decrypt key → init provider
API->>MCP: Connect servers in parallel
MCP-->>API: Tool registries
API->>API: Merge tools + add built-ins
API->>API: Assemble system prompt (5 layers)
API->>API: Filter history by model capabilities
API->>Provider: streamText(model, messages, tools)
Provider-->>API: Stream chunks
loop For each chunk
API-->>Client: SSE event (text / reasoning / tool-call / tool-result)
end
API->>DB: Persist assistant message + metadata
API->>DB: Update chat.currentLeafId
API->>MCP: Disconnect servers
API-->>Client: SSE done event
Projects are organisational workspaces that group related chats under a shared context. They provide a way to maintain a consistent AI instruction set across multiple conversations on the same topic — for example, a software project, a research area, or a client engagement.
The projects listing page displays all user-created projects as a searchable card grid. Each card shows the project name, description, pin status, and timestamps. Clicking a card navigates to the project detail page.
The detail page uses a tabbed layout with the following sections:
| Tab | Content |
|---|---|
| Chats | Lists chats scoped to this project with a "New Chat" button |
| Knowledge | Selector to associate a knowledge base with the project |
| Prompt | Auto-expanding textarea for the project's global system prompt (up to 10,000 characters) |
| Settings | Editable name and description fields |
| Tools | MCP tool picker for pre-selecting which tools are available in this project's chats |
| Danger Zone | Project deletion with confirmation dialog |
A create dialog accessible from the listing page collects the project name and optional description.
When a chat is created within a project, the project ID is stored on the chat record. Project chats are routed under the project's URL namespace and are excluded from the sidebar's "Recent Chats" list — they are accessed exclusively through the project detail page. This two-tier organisation keeps the main sidebar focused on standalone chats while project-related conversations remain grouped.
Chats can be moved between projects (or removed from a project) via the "Move to Project" dialog, which updates the project reference on the chat record.
Each project has an optional global prompt field (up to 10,000 characters). This prompt becomes the second layer in the system prompt assembly, injected after the user's global prompt and before any assistant prompt. It applies uniformly to every chat within the project, ensuring a consistent behavioural baseline.
A project can be linked to a single knowledge base. When set, this association acts as the fallback if no knowledge base is selected at the individual chat or message level. The resolution chain is: per-message selection → chat-level setting → project-level setting. This means a project knowledge base provides default context for all chats without requiring explicit selection each time.
Projects can be pinned to appear at the top of the listing page. Pinned projects are sorted before unpinned ones, with both groups ordered by last-updated timestamp. Pinning is a simple boolean toggle with no limit on the number of pinned projects.
Deleting a project unbinds all associated chats by clearing their project reference — the chats themselves are preserved as standalone conversations. The project record, its settings, and its tool selections are permanently removed.
- Projects do not have their own dedicated chat interface; they are purely organisational containers.
- The global prompt is the primary mechanism for project-level context, rather than configuration files or structured settings.
- Knowledge-base association at the project level avoids repetitive per-chat setup for document-heavy workflows.
| Aspect | Projects | Assistants |
|---|---|---|
| Purpose | Organise chats by topic or domain | Define an AI persona with specific behaviour |
| Prompt layer | 2nd (project prompt) | 3rd (assistant prompt) |
| Card click | Navigates to detail page | Creates a new chat bound to the assistant |
| Knowledge base link | Yes (project-level default) | No |
| Pinning | Yes | No |
| In-chat mention | No | Yes (@ mention for transient use) |
| Sidebar visibility | Project chats hidden from main list | Chats appear in "Recent Chats" normally |
| Deletion effect | Chats unbound but preserved | Chats unbound but preserved |
Assistants are reusable AI persona presets. Each assistant defines a name, optional description, a system prompt, and an optional avatar. They allow users to create consistent AI personalities — such as a code reviewer, a creative writer, or a domain expert — that can be applied across multiple conversations.
The assistants listing page displays all user-created assistants as a searchable card grid. Each card shows the assistant's name, description, avatar, and timestamps.
Clicking an assistant card creates a new chat bound to that assistant and immediately navigates to the chat interface. This design prioritises the most common action — starting a conversation — over configuration browsing.
The assistant detail page is accessible via a dedicated route and uses a tabbed layout:
| Tab | Content |
|---|---|
| Chats | Lists chats using this assistant |
| Prompt | Editable system prompt for the assistant's persona |
| Settings | Editable name, description, and avatar |
| Tools | MCP tool picker for pre-selecting tools |
| Danger Zone | Assistant deletion with confirmation dialog |
Assistants relate to chats in two modes:
- Permanent binding: When a chat is created via an assistant card click, the assistant ID is stored on the chat record. The assistant's prompt always applies as the third system-prompt layer for every message in that chat. This binding persists for the lifetime of the chat.
-
Transient mention (
@): In a chat without a permanently bound assistant, typing@opens the mention palette only for the very first message. Once the first message has been sent, the mention palette is disabled even in unbound chats. Selecting an assistant applies its prompt for that single message only — it is not stored on the chat record. The mentioned assistant ID is passed in the request metadata.
The mention system activates when the user types @ in the chat input. It searches across all assistants by name and description. Selecting an assistant from the palette records the selection in the message metadata.
The @ mention is disabled when the chat already has a permanently bound assistant. This prevents conflicting persona instructions — the bound assistant always takes precedence.
The assistant's system prompt occupies the third layer in the five-layer prompt stack. It is injected after the global user prompt and project prompt, but before the knowledge-base and spreadsheet instructions. Both permanently bound and transiently mentioned assistants are resolved identically at the API layer — the assistant prompt is fetched from the database in either case.
Deleting an assistant unbinds all associated chats by clearing their assistant reference. The chats themselves are preserved. Deletion is confirmed via a destructive-action dialog.
- The card-click-to-create-chat pattern reflects the assumption that users create assistants to use them, not to configure them repeatedly.
- The mutual exclusivity of permanent binding and
@mentions avoids ambiguity about which persona applies. - Assistants deliberately do not support knowledge-base associations — that responsibility belongs to projects and per-message selections.
Prompts are user-defined shortcuts invoked by typing / followed by the shortcut keyword in the chat input. They prepend saved instruction text before the user's message, enabling reusable prompt templates for common tasks such as summarisation, code review, or translation.
See dedicated wiki page for details: Prompts and System Prompts
Knowledge bases provide retrieval-augmented generation over uploaded documents. Documents are chunked, embedded into 2,048-dimensional vectors, and stored alongside a full-text search index. At query time, hybrid search (vector cosine similarity combined with PostgreSQL FTS) uses Reciprocal Rank Fusion to return the most relevant chunks, which are injected into the AI's context.
See dedicated wiki page for details: RAG Knowledge Base
The application connects to external Model Context Protocol servers (supporting both stdio and Streamable HTTP transports) to provide AI tool-calling capabilities. Connections are per-request, run in parallel, and enforce a 10-second timeout. Tool IDs follow a namespaced format and are filtered by the user's per-message selection. SSRF protection blocks requests to private network addresses.
See dedicated wiki page for details: MCP Integration
The artifact panel renders structured outputs produced by the AI in a dedicated side panel. Supported formats include Markdown (rendered via BlockNote), HTML (sandboxed iframe), spreadsheets (interactive grid with styling), and Mermaid diagrams. Artifacts are created via a built-in tool that the AI invokes when the user requests a document, table, diagram, or interactive layout.
See dedicated wiki page for details: Artifact Panel
Model selection in AI Client operates at the message level, not the chat level. Each message independently records which model was used for generation in its metadata. This allows users to switch models mid-conversation — for example, using a vision model for an image-related question and a tool-calling model for data analysis within the same chat.
All models are served through OpenRouter's free tier. The application defines a central model registry with capability metadata:
| Model | Provider | Context Window | Vision | Tool Calling |
|---|---|---|---|---|
| GPT OSS 120B | OpenAI | 128,000 | No | Yes |
| Gemma 4 31B | 32,768 | No | Yes | |
| Nemotron Nano VL | NVIDIA | 128,000 | Yes | No |
| Nemotron 3 Super | NVIDIA | 128,000 | No | Yes |
The application uses capability metadata to gate feature availability per model:
- Vision: Image attachments are only included in the request payload when the selected model supports vision. For non-vision models, image attachments are silently stripped from the message history before sending.
- Tool calling: MCP tools and built-in tools are only passed to the AI when the selected model supports tool-calling. If a user selects tools but chooses a non-tool-calling model, the tools are omitted with a logged warning.
A fallback heuristic handles unknown models by inferring capabilities from their identifier prefix (e.g., model IDs containing "vl" or "vision" are assumed to support vision).
AI Client requires each user to provide credentials for one or more OpenAI-compatible AI providers (OpenRouter, Ollama, Groq, Azure, etc.). Credentials are entered in the app settings page and are encrypted at rest using AES-256 before being stored in the database. At request time, the credentials are decrypted server-side and used to initialise the AI provider. Keys are never exposed to the client — when fetched for display, they are masked (e.g., sk-or-****...****).
If no provider credentials are configured, all AI operations fail with a descriptive error message and a direct link to the settings page.
See dedicated wiki pages for details: Provider Registry & Model Management · AI Provider Key Encryption
When a message is generated, the model ID is stored in the message's metadata JSON field. This serves two purposes:
- Display: The UI can show which model produced each response.
- Inheritance: When editing or regenerating a message, the input defaults to the model that was originally used, so the user does not need to reselect it.
File attachments are uploaded to S3-compatible object storage (MinIO in development) with presigned URLs for secure access. The client-side pipeline handles validation, text extraction (for documents), and metadata preparation before upload. Attachments are linked to messages or transform runs in the database.
See dedicated wiki page for details: Client-Side Attachment Pipeline and Object Storage
Workflows are standalone, purpose-built tools that operate outside the chat interface. They are accessible via a dedicated sidebar section with their own navigation.
The translation workflow provides AI-powered text translation as a focused, single-purpose tool. Unlike the general chat interface, it is optimised for quick translation tasks without the overhead of conversation management, message persistence, or streaming.
The page uses a split-pane layout:
- Left pane (Source): A text area for entering or pasting source text (up to 5,000 characters), with a file attachment button and a character counter.
- Right pane (Target): A read-only text area displaying the translated output, with a copy-to-clipboard button.
- Control bar: Sits between the header and the panes, containing a source language selector, a swap button, and a target language selector. All selectors are searchable comboboxes.
- Header row: Displays the workflow title and a model selector.
The workflow supports 19 languages plus an auto-detect option for the source language:
| Languages |
|---|
| English, Spanish, French, German, Italian, Portuguese, Russian, Chinese (Simplified), Chinese (Traditional), Japanese, Korean, Arabic, Hindi, Bengali, Turkish, Dutch, Polish, Vietnamese, Thai |
The auto-detect option sends the text without a specified source language, allowing the AI to identify it automatically. The swap button exchanges source and target languages (disabled when auto-detect is active) and also swaps the current text content between panes.
Users can attach a single file to the source pane:
- Documents (PDF, plain text, Markdown): Text is extracted client-side and placed into the source text area, truncated to 5,000 characters.
- Images: Sent directly to the AI with an OCR instruction. A warning badge appears if the selected model does not support vision, indicating that OCR may fail.
The translation workflow uses a non-streaming server action rather than the SSE-based chat pipeline. The entire translation completes server-side before the result is returned to the client. This design was chosen because:
- Translations are typically short, making incremental display unnecessary.
- The simpler request/response pattern avoids the complexity of stream parsing, message persistence, and abort handling.
- The result is not persisted to the database — it exists only in the client's local state.
The client collects the source text, language selections, model, and optional attachment, then calls a server action. The server action authenticates the user, validates the input against a Zod schema, initialises the AI provider with the user's API key, constructs a translation-specific prompt, and invokes the AI SDK's non-streaming text generation. The translated text is returned directly to the client.
Transform agents are multi-step document transformation pipelines that process spreadsheet files through a series of AI-driven steps with optional human review gates. They support dry-run mode, step-level configuration, and input/output attachment tracking.
See dedicated wiki page for details: Step-by-Step Automations
The global search page provides a unified interface for finding entities across the application. It aggregates chats, projects, and assistants into a single searchable view, allowing users to quickly locate any resource without navigating to individual listing pages.
| Entity | Searched Fields |
|---|---|
| Chats | Title |
| Projects | Name |
| Assistants | Name |
The search page is a server component that fetches all user-owned chats, projects, and assistants concurrently on page load. These are passed to a client component that performs client-side filtering — there is no server-side search query. As the user types in the search box, the displayed results are filtered in real time by matching the query against the relevant text fields (case-insensitive substring match).
A dropdown filter allows the user to narrow results by entity type:
- All Types (default): Shows chats, projects, and assistants together.
- Chats: Shows only chat results.
- Projects: Shows only project results.
- Assistants: Shows only assistant results.
Results are displayed in sectioned groups (Projects → Assistants → Chats), each with a count badge. Within each section, items are rendered using the same card components used on their respective listing pages, providing visual consistency.
- Client-side filtering was chosen because the total number of user-owned entities is expected to remain manageable (hundreds, not thousands). This avoids additional API endpoints and database queries for search, keeping the implementation simple.
- No full-text search is used for this feature — it relies on simple substring matching. Full-text search is reserved for the knowledge-base RAG system, where document retrieval demands it.
- The search page is accessible from the sidebar header, making it a quick-access feature.
The profile section provides a comprehensive suite of account management tools, accessible from a dedicated sidebar with five sections.
| Section | Features |
|---|---|
| General | Display name update; email shown as read-only |
| Security | Password management, TOTP 2FA (QR code + backup codes), passkey management (WebAuthn) |
| Sessions | Active session list with device metadata, current session identification, bulk revocation |
| Linked Accounts | OAuth provider linking and unlinking (GitHub, Discord) |
| Danger Zone | Permanent account deletion with email confirmation |
The general settings page displays a profile update form pre-populated with the user's current name and email. The name field is editable; the email field is read-only (email changes are not supported through this interface). Updates are submitted via a server action that validates the input and persists the change.
The security page aggregates three credential management areas:
- Password: If the user has a credential-based account (email + password), a change-password form is shown. If the user signed up via OAuth and has no password, a "Set Password" button triggers a password-reset email to establish one.
- Two-Factor Authentication (TOTP): Users can enable 2FA by entering their current password. The system generates a TOTP secret and displays a QR code for scanning with an authenticator app. After verification with a 6-digit code, 2FA is activated and backup codes are displayed for recovery. Disabling 2FA requires the current password.
- Passkeys (WebAuthn): Users can register hardware security keys or platform authenticators as passwordless login credentials. Each passkey has a user-assigned name. Existing passkeys are listed with their creation date and can be individually deleted.
The sessions page lists all active sessions for the user's account, showing device metadata (IP address, user agent). The current session is visually identified. Users can revoke all other sessions in bulk, forcing re-authentication on those devices.
The linked accounts page shows the status of each supported OAuth provider (GitHub, Discord). For each provider, the interface displays either a "Link" button (to initiate the OAuth flow) or the linked account details with an "Unlink" button. Linking redirects the user through the provider's OAuth consent screen and returns to the profile page. Unlinking removes the provider association after confirmation.
The danger zone page contains a single destructive action: permanent account deletion. Clicking the delete button triggers an email confirmation flow — a verification link is sent to the user's email address. The account is only deleted after the user clicks the confirmation link (which expires after 24 hours). An "Are you sure?" confirmation dialog is shown before initiating the flow.
The settings section manages app-level configuration that applies globally across all chats and workflows.
The app settings page contains two primary fields:
-
OpenRouter API Key: The user's personal API key for accessing AI models through OpenRouter. Keys with the standard OpenRouter prefix (
sk-or-) are encrypted at rest using AES-256 before database storage. The field uses a password-type input with a show/hide toggle. When retrieved for display, the key is decrypted and masked (only the first and last few characters are shown). Submitting a masked value is a no-op — only raw keys trigger re-encryption. - Global System Prompt: A free-text field (up to 5,000 characters) that defines baseline instructions prepended to every AI request. This is the first layer in the system prompt assembly, appearing before project and assistant prompts. The textarea auto-expands as content grows. An empty global prompt is valid and simply omits the layer.
Changes are saved via a server action that validates the input, handles encryption for the API key, and upserts the settings record (one row per user).
The settings sidebar also provides navigation to:
- Prompt Management: CRUD interface for slash-command prompt shortcuts. See the Prompts and System Prompts wiki page for details.
- MCP Server Management: CRUD interface for configuring MCP tool servers. See the MCP Integration wiki page for details.
| Setting Level | Scope | Examples |
|---|---|---|
| App-level (User Settings) | Applies to all conversations | API key, global system prompt |
| Project-level | Applies to all chats within a project | Project prompt, project KB, project tools |
| Assistant-level | Applies when assistant is active | Assistant prompt, assistant tools |
| Message-level | Applies to a single message | Model selection, per-message tool selection, per-message KB |
Settings are resolved hierarchically — message-level overrides take precedence, falling back through assistant, project, and finally app-level defaults.
All transactional emails are sent through Postmark, a dedicated transactional email service. The integration uses Postmark's server client, initialised with a server token from environment variables. A single sender address (configured via environment) is used for all outbound messages.
| Type | Trigger | Purpose | Link Expiry |
|---|---|---|---|
| Welcome | Successful user registration | Greets the user and confirms account creation | N/A (no action link) |
| Verification | Sign-up (automatic) | Activates the user's account via a verification link | 24 hours |
| Password Reset | Forgot-password request | Provides a link to set a new password | 24 hours |
| Account Deletion | Delete-account request | Confirms permanent account deletion via a confirmation link | 24 hours |
All email dispatch is hooked into Better Auth's lifecycle events:
- The welcome email fires in an
afterhook on user creation. - The verification email fires via the
sendVerificationEmailhook, which is triggered automatically on sign-up (withsendOnSignUp: true). - The password reset email fires via the
sendResetPasswordhook when a user requests a password reset. - The account deletion email fires via the
sendDeleteAccountVerificationhook when a user initiates account deletion.
Each email is sent in both HTML and plain-text formats for client compatibility. The emails use inline CSS styling with a maximum width of 600 pixels for consistent rendering across email clients.
The application uses a context-sensitive sidebar that adapts based on the current route group. Four sidebar variants exist: the main app sidebar, the profile sidebar, the settings sidebar, and the workflow sidebar.
The main sidebar serves authenticated routes and contains three zones:
Header Zone:
- New Chat button — creates a standalone chat and navigates to it
- Search link — navigates to the global search page
Content Zone:
- Navigation links — Projects, Assistants, Knowledgebases, Workflows
- Recent Chats — a collapsible list showing the 20 most recently updated standalone chats (excluding project-bound chats), sorted by last update time. Each entry has a context menu for rename, move-to-project, and delete actions.
Footer Zone:
- User dropdown — shows avatar, name, and email. Contains links to Profile, Settings, and a Sign Out action.
flowchart TB
subgraph Sidebar
direction TB
H["Header"]
H1["New Chat"]
H2["Search"]
H --> H1
H --> H2
N["Navigation"]
N1["Projects"]
N2["Assistants"]
N3["Knowledgebases"]
N4["Workflows"]
N --> N1
N --> N2
N --> N3
N --> N4
R["Recent Chats (max 20)"]
R --> RC1["Chat 1 — context menu"]
R --> RC2["Chat 2 — context menu"]
R --> RCn["..."]
F["Footer — User Dropdown"]
F --> F1["Profile"]
F --> F2["Settings"]
F --> F3["Sign Out"]
end
Chats are organised into two tiers:
- Standalone chats appear in the sidebar's "Recent Chats" section and on the main chats listing page.
- Project chats are excluded from the sidebar and the main chats listing. They are accessed exclusively through their parent project's detail page, under the "Chats" tab.
This separation keeps the sidebar focused and prevents project-related conversations from cluttering the general chat list.
The sidebar reads chat data from the Zustand store, which is hydrated on mount by fetching from the server. All sidebar actions (rename, delete, move) update the store optimistically — the UI reflects changes immediately while server actions execute in the background. If a server action fails, a toast notification alerts the user.
Each chat entry in the sidebar and on listing pages has a context menu (triggered by a "more" icon button) offering:
- Rename — opens an inline rename dialog
- Move to Project — opens a searchable project picker dialog
- Delete — opens a destructive-action confirmation dialog
The profile sidebar replaces the main sidebar on profile routes. It displays the user's avatar, name, and email, and provides navigation to the five profile sections (General, Security, Sessions, Linked Accounts, Danger Zone). A "Back to Home" button returns to the main interface.
The settings sidebar replaces the main sidebar on settings routes. It navigates between General (app settings), Tools (MCP servers), and Prompts (slash-command shortcuts), with a "Back to Home" button.
The workflow sidebar replaces the main sidebar on workflow routes. It lists the available workflows (All Workflows, Translation, Spreadsheets Automation) and includes a "Back to Home" button and the same user dropdown footer as the main sidebar.
The application uses a single Zustand store composed of two slices — a chat slice managing messages and conversation state, and an entity slice managing projects, assistants, prompts, knowledge bases, and MCP servers. All mutations follow an optimistic update pattern for responsive UI.
See dedicated wiki page for details: State Management
AI responses are rendered as rich Markdown with support for GitHub Flavoured Markdown tables, fenced code blocks with syntax highlighting, KaTeX mathematical notation (inline and block), and Mermaid diagram rendering. Code blocks include a copy-to-clipboard button and language labels.
See dedicated wiki page for details: Markdown Rendering
- Next.js App Router Documentation
- Vercel AI SDK Documentation
- Better Auth Documentation
- Drizzle ORM Documentation
- Model Context Protocol Specification
- OpenRouter API Documentation
- Zustand Documentation
- Postmark Documentation
- WebAuthn / FIDO2 Specification
- Server-Sent Events Specification
- MinIO Documentation
- pgvector Documentation
- Tailwind CSS Documentation
- Base UI Documentation
- BlockNote Documentation