Cortex Chat is a standalone application that gives end users a clean "Ask AI" interface on top of any Cortex instance. It runs as its own deployment with its own user accounts and its own SQLite database, and connects to the Cortex backend exclusively through the REST API with scoped keys — the flagship example of a standalone app in the Cortex ecosystem (as opposed to Apps hosted inside an instance).
The pitch: provision an account for each team member, assign them to a group scoped to the collections they should see, and everyone gets a branded chat experience — streaming answers, Deep Research, inline citations, personal cross-device chat history — without touching Cortex itself or ever seeing an API key. Administrators additionally get a built-in library console, so day-to-day document and pipeline upkeep never requires opening the Cortex workbench.
Feature overview
Chat
- Streaming Ask AI — token-by-token rendering over SSE, proxied through the app's server to avoid gzip buffering; users can toggle streaming off in the settings panel.
- Deep Research mode — the backend's agentic multi-step RAG, with live thinking steps, sub-question decomposition, and retrieval progress shown while it runs. Deep Research is the default mode every new conversation starts in; an admin can switch the default to Chat, and users can switch per conversation.
- Inline citations —
[src_N]annotations render as clickable numbered badges; a source modal shows the full chunk with relevance scores. - Message actions — hover any message for copy, regenerate (last answer), edit & resend (forks the conversation at that point), and 👍/👎 feedback that feeds the admin analytics. With voice configured, every answer also gets a read-aloud button.
- Starter prompts — admin-curated suggested questions render as clickable cards on the empty chat screen, combined with the selected personality's own starters.
- Collection scoping — defaults to everything the user's group can read; a settings panel narrows to a single collection. The backend enforces scope via the key, so the UI can never leak other collections.
- Conversation memory — the app round-trips the backend's opaque memory blob each turn for cross-turn recall and citation continuity beyond the history window.
- Server-side chat history — sessions, messages, and auto-generated titles persist per user and follow them across devices, in a slide-in sidebar with title search, pinning, and per-chat Markdown export.
- Deep links — the active chat rides in the URL (
/?chat=<id>), so a refresh stays in place, back/forward walk between conversations, and a chat link can be shared with anyone who has access to it. - Support link — an optional, admin-configured button in the chat header (helpdesk, mailto, wiki — any URL).
- i18n — English and German UI, default language set per deployment. Every user has a profile page (username, avatar, password).
Personalities (SOUL.md)
Assistant personas layered over the same retrieval engine. Each personality is a portable SOUL.md file — a markdown identity document (Identity, Purpose & Expertise, Voice & Style, Behavioral Directives, Boundaries) with optional frontmatter (name, description, starters, collection). The persona is injected server-side on every turn; it never reaches the browser and is never stored in chat history.
- Three tiers — built-in personalities ship with the app (Research Analyst, Support Assistant, Sales Companion; removable and restorable by an admin, and new releases add new ones), admin-curated personalities are visible globally or per group, and every user can keep up to 20 personal ones.
- Picker — pill chips on the empty chat screen ("Cortex" default + available personalities); each brings its own starter questions and optional default collection. The personality is fixed per conversation.
- Generate — describe the assistant you need in one paragraph and Cortex builds the SOUL.md for you: the app first researches the knowledge base (hybrid searches + plain questions, every step visible in a live research log), then writes the file with the instance's own primary model via the backend's
POST /api/llm/completions. The draft streams in, is hand-editable, and supports a refine loop ("make the tone more formal") before saving. Generated personalities speak your organization's actual language — real product names, real document types. - Portability — export any personality as its verbatim
SOUL.md; import by pasting, uploading a file, or from a URL. Files signed by soulweaver are EIP-191 signature-verified on import and show a verified-signer badge.
Projects — shared team workspaces
A project groups conversations and carries defaults every chat inside it inherits: an optional personality, a collection scope, and instructions — an invisible briefing injected with every question in the project.
- Sharing — one modal, one search field over groups and individual people. Members see all chats in the project; a warning asks for confirmation before an existing chat is moved into a shared project.
- Multi-user conversations — any member can continue any project chat. Every message is attributed to its author (name chips on teammates' messages); editing and regenerating are limited to your own turns; renaming/moving/deleting a chat stays with its creator.
- Realtime — teammates' turns appear live: when someone asks in a chat you have open, their question pops in and the answer streams in real time in your view (joining mid-answer replays what you missed). New chats, moves, and shares update everyone's sidebar within a second. No extra infrastructure — SSE over the app's own server.
- Organization — collapsible project folders in the sidebar; drag & drop chats between your personal list and projects.
Voice
Optional speech-to-text dictation and text-to-speech read-aloud, configured with environment variables against any OpenAI-compatible audio API — a self-hosted speaches / LiteLLM router, Venice, or OpenAI. Unset = the buttons don't exist.
- Dictation — a mic button in the composer records (browser
MediaRecorder), transcribes server-side, and inserts the text. Requires a secure origin (HTTPS or localhost) — the UI explains when it isn't. - Read-aloud — a speaker button on every answer synthesizes and plays it (markdown, citations, and tables are stripped for speech).
- Provider keys stay server-side behind
/api/voice/*proxies, like every other credential in the app.
Multi-tenant auth & user management
- Email/password accounts with argon2id hashing and DB-backed sessions (opaque cookie, 30-day sliding TTL). Users never need Cortex credentials.
- Single Sign-On (OIDC) — vendor-agnostic SSO against any OpenID Connect IdP with discovery: Entra ID / Azure AD, ADFS 2016+, Okta, Auth0, Google Workspace, Keycloak, Authentik, Authelia, Zitadel, PocketID. Three env vars enable it (
OIDC_ISSUER_URL,OIDC_CLIENT_ID,OIDC_CLIENT_SECRET); the flow is Authorization Code + PKCE, terminating into the same session model as password login. First-time users are JIT-provisioned into an optional default group (OIDC_DEFAULT_GROUP); an existing account with the same email is linked automatically — but only when the IdP asserts the email as verified (the account-takeover guard).OIDC_ONLY=trueremoves password login and self-registration entirely; the superadmin is excluded from SSO and keeps a break-glass password login (/login?password=1). Legacy LDAP-only or SAML-only stacks: put a thin bridging IdP (Authentik or Dex) in front and point the issuer URL at it. - Three roles — one env-bootstrapped
superadmin, promotableadmins, andusers. Each user belongs to exactly one group; the group's collection scope defines what its members can search. On first boot the app auto-creates a "Full Access" group and assigns the superadmin to it, retrying in the background until the backend is reachable. - Self-registration with approval — a public
/registerpage feeds a pending queue on the admin Users page; approving a registration assigns its group. On by default, disable withENABLE_REGISTRATION=false. Admins can be notified by email of new registrations, and pending users who log in get an "awaiting approval" message (only on a correct password — the flow is enumeration-safe). - Password reset via email — self-service ("Forgot password?", single-use 60-minute links, always-200 responses) and admin-triggered, feature-gated on SMTP configuration. Reset and approval emails reuse the deployment's branding (accent, logo, title). The superadmin's password is env-managed and excluded from email reset.
- Bulk user import —
scripts/import-users.tscreates users from an.xlsx(columnsemail,benutzername) against a running deployment via the same admin API the UI uses: dry-run by default, idempotent (existing accounts are never modified), one target group and shared initial password per run. Seescripts/README.mdin the repo.
Content management
The /upload page is a tabbed workspace — tabs appear by permission:
| Tab | Visible to | What it does |
|---|---|---|
| Upload | Content-role users + admins | Multi-file document upload (up to 200 MB/file) into permitted collections, per-file status, batch summary. Upload is confirmed on receipt; extraction runs in the background and is deliberately not surfaced here. A Web Import mode appears automatically when the backend has Web Import enabled — paste URLs or discover same-site links, pick a content filter, watch crawl progress. |
| Documents | Admins | Browse/filter the backend's documents by collection, reprocess one, process all pending, delete. Degraded documents (0 entities or unembedded chunks) and injection-flagged documents get warning badges with reasons. |
| Processing | Admins | A compact mirror of the Cortex Knowledge Graph pipeline: KPI tiles (documents, chunks, entities, relationships, communities, pending tasks, disk free, monthly usage meter) and the 3 pipeline steps — process pending extraction, relationship analysis (run/rebuild), community detection + summarization — plus orphaned-entity cleanup. Running tasks block conflicting actions with an explanation. |
| Collections | Admins | Create, rename, and delete backend collections, with document counts. |
- Content roles — an admin grants upload rights per user, choosing which collections the contributor may write into; this mints the user a
manage-scoped backend key.
Admin area
Six pages, for admins and the superadmin:
- Overview — analytics dashboard: KPIs (active users, logins, messages, uploads, answer feedback +/−) over a selectable window, a time-series chart, and a paginated login-history table.
- Users — create/edit/delete accounts, group assignment, role management (superadmin promotes/demotes admins), send reset emails, and the Registrations approval tab.
- Groups — per-group collection scope ("all" or a chosen set); creating a group mints its scoped read key.
- Content Roles — grant/revoke upload rights per user.
- Personalities — manage the shared persona library: enable/disable or remove/restore built-ins, create global or per-group personalities (paste, upload, URL import, or Generate), export any as
SOUL.md. - Settings — runtime branding and behavior, applied instantly with no rebuild: app title & description, accent color, logo (upload/remove; reused in emails), default language, default chat mode, starter prompts, support URL & label, registration-notification recipients, and the chat analytics template.
Chat analytics injection — an admin-defined <cortexchatanalytics> template (with $userEmail / $userName substitution) is injected server-side into every backend request. It never reaches the browser and is never stored in chat history, but backend Agent Skills can read it — e.g. to route chat summaries to a CRM with user identity attached. Empty template = no injection.
Resilience & observability
No configuration required, but useful to know as an operator:
- SSE reconnect — if the backend restarts mid-answer, the stream resubmits transparently (the backend's
event: shutdownframe) and the answer regenerates clean. - Rate limits & quotas — burst 429s and monthly-quota 429s are told apart by their
Retry-Afterhorizon; the quota case shows the reset date instead of a retry timer. Storage-full (507) and oversized-body (413) conditions get friendly messages. - Request correlation — every user action carries an
X-Request-IDthat all proxy routes forward and the backend echoes, so one ID traces a request across both apps' logs. - Error tracking — production builds report errors (never traces/replays) to a GlitchTip project via
@sentry/nextjs.SENTRY_DISABLED=1opts out,SENTRY_DSNpoints at your own instance,SENTRY_ENVIRONMENTtags the deployment, and a build-timeSENTRY_AUTH_TOKENenables source-map upload for readable stack traces.
The key model
There is exactly one privileged credential: an admin-tier Cortex key in the app's environment. It never leaves the server and is used as a factory to mint narrower keys against the backend:
| Key | Permission | Stored | Used for |
|---|---|---|---|
BACKEND_ADMIN_API_KEY (env) | admin | env only — never in SQLite, never in the browser | Minting the keys below; the admin library console; listing collections; admin uploads; personality generation via the backend's POST /api/llm/completions |
| Group chat key | read, scoped to the group's collections | AES-256-GCM encrypted in the app's SQLite | Every chat/search request by the group's members |
| User content key | manage, scoped to chosen collections | AES-256-GCM encrypted in the app's SQLite | That user's uploads and web imports |
The browser holds only a session cookie; server routes decrypt the right key and attach it as X-API-Key per request. The client bundle contains zero secrets and zero deploy-specific values.
Keys are bound to one backend
Minted keys live in the key store of the backend they were minted against. Re-pointing CORTEX_API_URL at a different Cortex instance invalidates all of them (chat returns 401). Recover by recreating groups — that mints fresh keys — and reassigning users; chat history survives because it is keyed by user, not group.
Setup
Prerequisites
- A running Cortex instance (personality Generate additionally needs a Cortex release that ships
POST /api/llm/completions— older backends show a clear message) - An admin-tier API key (
moca_admin_...) from that instance (Settings → API Keys) - Docker, or Node.js 18+ to run from source
Environment reference
All configuration is server-side and runtime — the same image serves any tenant, and boot fails fast with one aggregated error listing every missing or malformed value.
Required:
| Variable | Description | Default |
|---|---|---|
CORTEX_API_URL | Cortex backend URL. Must be reachable from inside the app's container — all traffic flows through the server-side proxy. | http://localhost:8000 |
BACKEND_ADMIN_API_KEY | Admin-tier Cortex key used to mint per-group / per-user keys. | — |
SUPERADMIN_EMAIL | Bootstraps the superadmin on every server start. | — |
SUPERADMIN_PASSWORD | Re-hashed (argon2id) each boot; rotate by editing env + restart. | — |
APP_ENCRYPTION_KEY | 32 random bytes, base64 (openssl rand -base64 32). Encrypts minted keys at rest. Validated to decode to exactly 32 bytes. | — |
Optional:
| Variable | Description | Default |
|---|---|---|
DATABASE_PATH | SQLite file path; avatars and branding assets live alongside it. | ./data/cortex-chat.db |
PORT | Published port in the shipped Compose file. | 3000 |
ENABLE_REGISTRATION | Public /register page with admin approval. false/0 disables signups (pending rows stay manageable). | true |
SMTP_HOST | SMTP server; unset = all email features off and hidden. | — |
SMTP_PORT | 465 with SMTP_SECURE=true (implicit TLS), 587 with false (STARTTLS). Mailpit for local dev: host localhost, port 1025, no auth. | 587 |
SMTP_USER, SMTP_PASS | SMTP credentials; optional (omit for Mailpit). | — |
SMTP_SECURE | true ⇒ implicit TLS; false ⇒ STARTTLS. | false |
SMTP_FROM | Sender, e.g. "Cortex Chat <no-reply@example.com>". Required when SMTP_HOST is set. | — |
APP_BASE_URL | Absolute public URL used to build email links and the SSO redirect URI (never derived from the request Host header). Required when SMTP_HOST or OIDC_ISSUER_URL is set. | — |
OIDC_ISSUER_URL | OIDC issuer URL of your IdP; discovery (/.well-known/openid-configuration) is derived from it. Unset = SSO off and invisible. Must be https:// (plain http allowed for localhost dev only). | — |
OIDC_CLIENT_ID, OIDC_CLIENT_SECRET | OAuth client registered at the IdP with redirect URI {APP_BASE_URL}/api/auth/oidc/callback. Both required when the issuer is set. | — |
OIDC_SCOPES | Scopes requested from the IdP. | openid profile email |
OIDC_BUTTON_LABEL | Login-button text, e.g. "Sign in with Entra ID". | localized "Single Sign-On" |
OIDC_DEFAULT_GROUP | Name of the chat group first-time SSO users join (resolved at login time). Unset = new SSO users land group-less and can't chat until an admin assigns a group. | — |
OIDC_ONLY | true hides the password form and disables password login + self-registration. The env-managed superadmin keeps a password backdoor at /login?password=1. | false |
VOICE_STT_BASE_URL | OpenAI-compatible audio API for dictation; unset = mic hidden. Endpoints used: {base}/audio/transcriptions. | — |
VOICE_STT_API_KEY, VOICE_STT_MODEL | Credentials + model for STT (e.g. faster-whisper-large-v3, whisper-large-v3). Model required when the base URL is set. | — |
VOICE_TTS_BASE_URL | OpenAI-compatible audio API for read-aloud; unset = speaker buttons hidden. Endpoints used: {base}/audio/speech. | — |
VOICE_TTS_API_KEY, VOICE_TTS_MODEL, VOICE_TTS_VOICE | Credentials, model (e.g. kokoro-82m, tts-kokoro), and voice (e.g. af_heart); some backends require a voice. | — |
SENTRY_ENVIRONMENT | Tags error events per deployment/tenant. | production |
SENTRY_DSN | Overrides the baked-in GlitchTip DSN (server-side, runtime). | built-in |
SENTRY_DISABLED | 1 = no error reporting at all. | — |
SENTRY_AUTH_TOKEN | Build-time only — enables source-map upload to GlitchTip (Coolify: set as a build variable; the Compose file maps it as a build arg). | — |
Deprecated but still accepted with a boot warning: LIBRARY_API_URL and NEXT_PUBLIC_API_URL (aliases for CORTEX_API_URL), and a legacy ACCENT_COLOR / NEXT_PUBLIC_ACCENT_COLOR, which is migrated into the settings database once on first boot.
Never prefix any of these with NEXT_PUBLIC_ — that would compile secrets into the client bundle. Branding is intentionally not env config; it is DB-backed and edited at Admin → Settings.
Docker
Already self-hosting Cortex?
If this instance came from npx @mocaos/cortex or the selfhost/ Docker Compose stack, Cortex Chat ships with it already, off by default. Enable it with COMPOSE_PROFILES=chat in .env plus npx @mocaos/cortex restart — or answer "yes" when the installer asks — instead of running the container below; see the deployment guide for the domain-mode extras (chat domain, CORS origin, Caddyfile). This opt-in default is specific to the release/self-host stack — Coolify and Dokploy deployments run chat unconditionally.
The docker run below is still the right path when Chat runs separately from the instance — a different host, or a Cortex you don't administer.
Code
The container listens on 3000 and persists everything (users, groups, keys, chat history, avatars, logo) under /app/data — mount a volume there. For Coolify / Dokploy, create a Docker Compose resource pointing at the repository, set the required variables (plus any optional ones — the shipped docker-compose.yml already maps SMTP, registration, GlitchTip, and PORT), and deploy it unchanged. Any platform with Dockerfile builds (Railway, Render, Fly.io) works the same way. From source: npm install, copy .env.example to .env.local, npm run dev — migrations apply automatically on every server start.
Networking gotcha
http://localhost:8000 inside the container points at the container itself, not your host — chat would fail with 502. If the Cortex backend also runs in Docker, attach Cortex Chat to the backend's network and use the service name (http://cortex-backend:8000). The repository README covers all deployment variants — including running the built image locally against a bind-mounted ./data — in detail.
First run
- Log in as the superadmin. On a fresh database the app auto-creates a "Full Access" group (read key over all collections) and assigns the superadmin to it, so chat works immediately.
- Brand the deployment at Admin → Settings — accent, logo, title, description, language, default chat mode, support link.
- Create groups scoped to the collections each audience should see (this mints each group's read key), then add users — directly, via the bulk import script, or by approving self-registrations.
- Optionally grant content roles for uploads / Web Import, configure SMTP for password resets and notifications, and set a chat analytics template for backend skills.
Related
- Ask AI — the retrieval and Deep Research pipeline behind every answer
- Knowledge Graph — the pipeline the Processing tab drives
- Apps — in-instance apps, the complementary hosting model
- Web Import — the backend feature the upload page auto-detects
- Authentication guide — Cortex's API key tiers and how admin keys are issued