NextChat is a cross-platform AI chat interface that connects to 16 LLM providers through a unified proxy architecture. Built on Next.js 14 with React 18, TypeScript, SCSS Modules, and Zustand state management, it ships as both a web application and a Tauri-based desktop application with a Rust backend. This guide covers every aspect an administrator, operator, or contributing developer needs to deploy, configure, secure, and maintain a NextChat instance.
1. Architecture Overview
1.1 Technology Stack
| Layer | Technology | Version |
|---|---|---|
| Framework | Next.js (App Router) | 14 |
| UI Library | React | 18 |
| Styling | SCSS Modules | — |
| State Management | Zustand (persisted) | — |
| Client Storage | IndexedDB (idb-keyval), localStorage | — |
| Desktop Runtime | Tauri | 1.5 |
| Desktop Backend | Rust | — |
| Bundler | Webpack | 5 |
| Language | TypeScript | 5.2 |
| Testing | Jest, Testing Library | 29 |
| Linting | ESLint, Prettier, Husky, lint-staged | — |
| Package Manager | Yarn | 1.22 |
1.2 High-Level Component Map
Browser / Tauri Shell
│
├── Next.js App Router (app/)
│ ├── layout.tsx Root layout, metadata, analytics injection
│ ├── page.tsx Entry point → Home component
│ └── API Routes (app/api/)
│ ├── [provider]/[...path]/route.ts Unified provider proxy
│ ├── config/route.ts Server-side config endpoint
│ ├── artifacts/route.ts HTML artifact sharing
│ ├── webdav/[...path]/route.ts WebDAV sync proxy
│ ├── upstash/[action]/[...key]/route.ts Upstash sync proxy
│ └── tencent/route.ts Tencent-specific signing
│
├── Client Layer (app/client/)
│ ├── api.ts ClientApi — provider routing dispatcher
│ ├── controller.ts ChatControllerPool — abort/lifecycle
│ └── platforms/ 14 provider implementations
│
├── State Stores (app/store/) 9 Zustand persisted stores
├── MCP Layer (app/mcp/) Model Context Protocol integration
├── Components (app/components/)
├── Config (app/config/) Server + client + build config
├── Locales (app/locales/) 20 language files
├── Masks (app/masks/) Built-in persona definitions
├── Utils (app/utils/) 19 utility modules
│
└── Tauri Desktop (src-tauri/)
├── src/main.rs Tauri entry, stream_fetch command
├── src/stream.rs Streaming HTTP bridge
├── Cargo.toml Rust dependencies
└── tauri.conf.json Desktop app configuration
1.3 Request Data Flow
A chat message traverses the following path from user input to stored response:
- Input — User types a message.
useChatStore.onUserInput()is called. - Template expansion — The input template (
DEFAULT_INPUT_TEMPLATEor user-configured) is applied viafillTemplateWith(). Available variables:{{input}},{{time}},{{model}},{{lang}},{{cutoff}},{{ServiceProvider}}. - Context assembly — The system prompt, long-term memory summary, mask (persona) context messages, and recent chat history are concatenated into a messages array. Message count is bounded by the configured context window and
CHAT_PAGE_SIZE(15) /MAX_RENDER_MSG_COUNT(45). - Provider dispatch —
getClientApi(providerName)resolves the correct platform client fromapp/client/platforms/. - Streaming — The provider's
chat()method sends the request through the Next.js API proxy (or directly in Tauri mode) and streams the response via Server-Sent Events (SSE). - Persistence — On stream completion the message is saved to the active session in
useChatStore. If the session's message count exceeds the summarize threshold, an automatic summarization request fires in the background. - MCP execution (optional) — When
ENABLE_MCP=true, assistant responses are scanned for fenced blocks matching the pattern```json:mcp:{clientId}```. Matched blocks are parsed and executed viaexecuteMcpAction()server actions.
1.4 API Proxy Architecture
All provider API calls are routed through Next.js API routes at /api/[provider]/[...path]. Every route follows the same pipeline:
- Authentication —
auth()(defined inapp/api/auth.ts) validates either a hashed access code or a provider-specific API key from the request headers. - Model gating —
isModelNotavailableInServer()checks whether the requested model is blocked by server configuration (e.g.,DISABLE_GPT4orCUSTOM_MODELSexclusions). - Proxying — The request is forwarded to the provider's base URL (default or custom via
*_URLenv var) with appropriate headers. - Streaming — The provider's response is streamed back to the client with CORS headers attached.
Provider-specific handler modules (app/api/openai.ts, app/api/anthropic.ts, app/api/google.ts, etc.) implement the auth-check-proxy pattern with provider-specific header transformations and request body adjustments.
NOTE: The proxy architecture means the server holds all provider API keys. Client-side keys stored in the browser are sent as request headers and used as a fallback when no server-side key is configured for a given provider.
1.5 Zustand Store Architecture
All persistent client-side state lives in nine Zustand stores, each created via createPersistStore() with version-stamped migrations.
| Store | localStorage Key | Schema Version | Responsibility |
|---|---|---|---|
useAccessStore |
access-control |
2 | Auth state, provider API keys, cached server config |
useChatStore |
chat-next-web-store |
3.3 | Chat sessions, messages, MCP tool-call state |
useAppConfig |
app-config |
4.1 | UI preferences, default model config |
useMaskStore |
mask-store |
3.1 | Custom personas (masks) |
usePluginStore |
chat-next-web-plugin |
1 | OpenAPI-based plugins |
usePromptStore |
prompt-store |
3 | User prompt library |
useSdStore |
sd-list |
1.0 | Stable Diffusion image generation state |
useSyncStore |
sync |
1.2 | WebDAV / Upstash sync configuration |
useUpdateStore |
chat-update |
1 | Version-check metadata |
Data is serialized to IndexedDB (via idb-keyval) for large payloads (chat history) and localStorage for smaller config stores. The IndexedDBStorage class in app/utils/indexedDB-storage.ts wraps idb-keyval and provides the Zustand persist adapter.
IMPORTANT: Store schema versions are critical for migration correctness. When modifying store shapes in development, always increment the version and add a migration function — otherwise users upgrading in place will encounter silent data corruption.
2. System Requirements
2.1 Server / Build Host
| Resource | Minimum | Recommended |
|---|---|---|
| Node.js | 18.x | 20.x LTS |
| Yarn | 1.22+ | 1.22+ |
| RAM (build) | 2 GB | 4 GB |
| RAM (runtime) | 512 MB | 1 GB |
| Disk | 500 MB (source + deps) | 1 GB |
For the Tauri desktop build, additionally:
| Resource | Requirement |
|---|---|
| Rust toolchain | stable (rustup) |
| Tauri CLI | @tauri-apps/cli v1.5 |
| OS-specific deps | See Tauri prerequisites for Linux (webkit2gtk, etc.), macOS (Xcode CLT), or Windows (WebView2, VS Build Tools) |
2.2 Runtime Environment
NextChat runs as a standard Next.js application. It requires:
- A Node.js 18+ runtime (or a Docker container providing one).
- Network access to at least one LLM provider API endpoint.
- A reverse proxy (nginx, Traefik, Cloudflare Tunnel, etc.) for TLS termination in production.
2.3 Provider Requirements
At least one provider API key must be configured. No provider is mandatory — the application is provider-agnostic by design.
3. Configuration Reference
All server-side configuration is read from environment variables by getServerSideConfig() in app/config/server.ts. There are no configuration files on the server side; everything is env-var driven. The variables are grouped below by functional area.
3.1 Core Application Settings
| Variable | Type | Default | Description |
|---|---|---|---|
CODE |
string | (none) | Comma-separated access codes. Each code is MD5-hashed (via spark-md5) before comparison. Users must enter one of these codes to access the instance. Leave empty to allow unrestricted access. |
BASE_URL |
string | (none) | Custom base URL for the OpenAI-compatible endpoint. Overrides the default https://api.openai.com. |
PROXY_URL |
string | (none) | HTTP proxy URL for outbound API requests. Only effective inside Docker or server environments where the Node.js process honors HTTP_PROXY-style variables. |
HIDE_USER_API_KEY |
string | (none) | Set to any non-empty value to hide the API key input fields in the client UI. Useful when you want to enforce server-side keys only. |
DISABLE_GPT4 |
string | (none) | Set to any non-empty value to block all GPT-4 class models from selection. Applies server-side — requests for GPT-4 models are rejected at the proxy. |
ENABLE_BALANCE_QUERY |
string | (none) | Set to any non-empty value to expose a balance-check endpoint in the UI (OpenAI billing API). |
DISABLE_FAST_LINK |
string | (none) | Disables URL fragment-based settings import (# links that auto-configure provider keys). Set this to prevent users from being phished via crafted URLs. |
CUSTOM_MODELS |
string | (none) | Comma-separated model list. Prefix a model name with - to remove it from the default list. Example: +my-custom-model,-gpt-4 adds one model and removes another. |
DEFAULT_MODEL |
string | (none) | Model ID to pre-select for new chat sessions. Must match an available model name exactly. |
VISION_MODELS |
string | (none) | Comma-separated list of model names that support image/vision input. Enables the image-upload button for these models. |
DEFAULT_INPUT_TEMPLATE |
string | {{input}} |
Template applied to every user message before sending. Supports variables: {{input}}, {{time}}, {{model}}, {{lang}}, {{cutoff}}, {{ServiceProvider}}. |
ENABLE_MCP |
string | (none) | Set to "true" (string) to enable Model Context Protocol server-side actions. Requires MCP server configuration in app/mcp/mcp_config.json. |
WHITE_WEBDAV_ENDPOINTS |
string | (none) | Comma-separated list of additional allowed WebDAV endpoint domains. The sync proxy validates destination URLs against a built-in whitelist plus these additions. |
CAUTION: Setting
CODEto an empty string or omitting it entirely disables access control. Any visitor can use the instance and consume your API quota. Always set access codes in production.
3.2 Build Settings
| Variable | Type | Default | Description |
|---|---|---|---|
BUILD_MODE |
string | standalone |
standalone for standard Next.js server output. export for static HTML export (used by Tauri desktop builds). |
BUILD_APP |
string | (none) | Set to any value during desktop app builds. Triggers Tauri-specific build paths. |
VERCEL |
string | (none) | Auto-set by the Vercel platform. Detected by the build system to adjust output settings. |
3.3 Provider API Keys
Every provider supports multi-key rotation: supply multiple keys separated by commas, and the client will randomly select one per request to distribute rate-limit pressure.
OpenAI
| Variable | Description |
|---|---|
OPENAI_API_KEY |
OpenAI API key(s). Comma-separated for rotation. |
OPENAI_ORG_ID |
OpenAI organization ID. Sent as OpenAI-Organization header. |
Azure OpenAI
| Variable | Description |
|---|---|
AZURE_URL |
Azure OpenAI resource endpoint (e.g., https://myresource.openai.azure.com). |
AZURE_API_KEY |
Azure API key(s). |
AZURE_API_VERSION |
Azure API version string (e.g., 2024-02-01). |
Google (Gemini)
| Variable | Description |
|---|---|
GOOGLE_API_KEY |
Google AI / Gemini API key(s). |
GOOGLE_URL |
Custom Gemini API endpoint. |
Anthropic (Claude)
| Variable | Description |
|---|---|
ANTHROPIC_API_KEY |
Anthropic API key(s). |
ANTHROPIC_URL |
Custom Anthropic API endpoint. |
ANTHROPIC_API_VERSION |
Anthropic API version string. |
Baidu (Ernie)
| Variable | Description |
|---|---|
BAIDU_API_KEY |
Baidu API key(s). |
BAIDU_URL |
Custom Baidu endpoint. |
BAIDU_SECRET_KEY |
Baidu OAuth secret key for token exchange. |
ByteDance (Doubao)
| Variable | Description |
|---|---|
BYTEDANCE_API_KEY |
ByteDance API key(s). |
BYTEDANCE_URL |
Custom ByteDance endpoint. |
Alibaba (Qwen)
| Variable | Description |
|---|---|
ALIBABA_API_KEY |
Alibaba / DashScope API key(s). |
ALIBABA_URL |
Custom Alibaba endpoint. |
Tencent (Hunyuan)
| Variable | Description |
|---|---|
TENCENT_SECRET_KEY |
Tencent Cloud secret key. |
TENCENT_SECRET_ID |
Tencent Cloud secret ID. |
TENCENT_URL |
Custom Tencent Hunyuan endpoint. |
Moonshot
| Variable | Description |
|---|---|
MOONSHOT_API_KEY |
Moonshot API key(s). |
MOONSHOT_URL |
Custom Moonshot endpoint. |
iFlytek (Spark)
| Variable | Description |
|---|---|
IFLYTEK_API_KEY |
iFlytek API key. |
IFLYTEK_API_SECRET |
iFlytek API secret. |
IFLYTEK_URL |
Custom iFlytek endpoint. |
DeepSeek
| Variable | Description |
|---|---|
DEEPSEEK_API_KEY |
DeepSeek API key(s). |
DEEPSEEK_URL |
Custom DeepSeek endpoint. |
xAI (Grok)
| Variable | Description |
|---|---|
XAI_API_KEY |
xAI API key(s). |
XAI_URL |
Custom xAI endpoint. |
ChatGLM
| Variable | Description |
|---|---|
CHATGLM_API_KEY |
ChatGLM (Zhipu AI) API key(s). |
CHATGLM_URL |
Custom ChatGLM endpoint. |
SiliconFlow
| Variable | Description |
|---|---|
SILICONFLOW_API_KEY |
SiliconFlow API key(s). |
SILICONFLOW_URL |
Custom SiliconFlow endpoint. |
302.AI
| Variable | Description |
|---|---|
AI302_API_KEY |
302.AI API key(s). |
AI302_URL |
Custom 302.AI endpoint. |
Stability AI (Image Generation)
| Variable | Description |
|---|---|
STABILITY_API_KEY |
Stability AI API key for Stable Diffusion image generation. |
STABILITY_URL |
Custom Stability AI endpoint. |
3.4 Cloudflare KV (Optional Caching)
| Variable | Description |
|---|---|
CLOUDFLARE_ACCOUNT_ID |
Cloudflare account identifier. |
CLOUDFLARE_KV_NAMESPACE_ID |
KV namespace to use for caching. |
CLOUDFLARE_KV_API_KEY |
API key with KV read/write permissions. |
CLOUDFLARE_KV_TTL |
Cache time-to-live in seconds. |
3.5 Analytics
| Variable | Default | Description |
|---|---|---|
GTM_ID |
(none) | Google Tag Manager container ID. |
GA_ID |
G-89WN60ZK2E |
Google Analytics measurement ID. The default tracks usage for the upstream project maintainers. Override or unset this for self-hosted instances. |
TIP: To disable all analytics on a self-hosted instance, set
GA_IDto an empty string and leaveGTM_IDunset.
3.6 Complete Variable Count
The extraction identifies 77 environment variables across the categories above. Every variable is read in app/config/server.ts via process.env — there is no .env file parsing library; the runtime environment (Node.js, Docker, Vercel) must inject them.
4. Deployment & Scaling
4.1 Docker Deployment (Recommended)
NextChat ships with a Dockerfile and Docker Compose configuration at the repository root.
# Build and run
docker build -t nextchat .
docker run -d \
-p 3000:3000 \
-e CODE="your-access-code" \
-e OPENAI_API_KEY="sk-..." \
--name nextchat \
nextchat
For multi-provider setups, pass all required *_API_KEY and *_URL variables:
docker run -d \
-p 3000:3000 \
-e CODE="admin123,user456" \
-e OPENAI_API_KEY="sk-..." \
-e ANTHROPIC_API_KEY="sk-ant-..." \
-e GOOGLE_API_KEY="AIza..." \
-e HIDE_USER_API_KEY=1 \
-e DISABLE_GPT4=1 \
-e CUSTOM_MODELS="-gpt-4-32k,-gpt-4o" \
--name nextchat \
nextchat
NOTE: The
PROXY_URLvariable is specifically designed for Docker deployments where outbound traffic must traverse an HTTP proxy. In non-Docker environments, use standardHTTP_PROXY/HTTPS_PROXYenvironment variables instead.
4.2 Docker Compose
version: "3"
services:
nextchat:
build: .
ports:
- "3000:3000"
environment:
- CODE=your-access-code
- OPENAI_API_KEY=sk-...
restart: unless-stopped
4.3 Vercel Deployment
NextChat is designed for one-click Vercel deployment:
- Fork or import the repository into Vercel.
- Set environment variables in the Vercel dashboard (Settings → Environment Variables).
- Deploy. The
VERCELenv var is auto-set and triggers appropriate build adjustments.
Vercel's edge network handles TLS, CDN, and scaling automatically. The BUILD_MODE=standalone default works correctly on Vercel.
4.4 Static Export (Tauri Desktop)
For the desktop application:
yarn export # Builds static HTML export
yarn app:build # Triggers Tauri to package the desktop app
The BUILD_MODE=export and BUILD_APP=1 variables are set automatically by the export script. The Tauri build produces platform-specific installers (.dmg, .msi, .AppImage/.deb).
4.5 Manual Node.js Deployment
yarn install
yarn build
yarn start # Starts on port 3000
Place behind a reverse proxy (nginx, Caddy, Traefik) for TLS termination.
4.6 Scaling Considerations
NextChat is a stateless server — all persistent state lives in the client's browser (IndexedDB/localStorage). This makes horizontal scaling straightforward:
- Multiple instances behind a load balancer require no session affinity.
- No shared database — the server reads only environment variables and proxies requests.
- Bottleneck — the upstream LLM provider APIs, not NextChat itself. Rate limits are per-API-key; use multi-key rotation (comma-separated keys) to distribute load.
For high-traffic deployments, scale by:
- Running multiple container replicas behind a load balancer.
- Using separate API keys per replica (or comma-separated pools).
- Deploying to edge platforms (Vercel, Cloudflare Workers) for geographic distribution.
NOTE: The WebDAV and Upstash sync endpoints are server-proxied. If sync traffic is high, ensure the proxy routes have adequate timeout settings (the default
REQUEST_TIMEOUT_MSis 60 seconds).
5. Operations
5.1 Build Scripts Reference
| Script | Command | Purpose |
|---|---|---|
yarn dev |
concurrently "yarn mask:watch" "next dev" |
Development server with hot reload and mask file watching |
yarn build |
yarn mask && cross-env BUILD_MODE=standalone next build |
Production build (standalone Next.js output) |
yarn start |
next start |
Start the production server |
yarn export |
yarn mask && cross-env BUILD_MODE=export BUILD_APP=1 next build |
Static HTML export for Tauri desktop packaging |
yarn app:dev |
concurrently "yarn mask:watch" "yarn tauri dev" |
Desktop app development with hot reload |
yarn app:build |
yarn mask && yarn tauri build |
Production desktop app build |
yarn test |
jest --watch |
Run tests in watch mode |
yarn test:ci |
jest --ci |
CI test run (single pass, no watch) |
yarn mask |
npx tsx app/masks/build.ts |
Compile mask (persona) definitions from source |
yarn prompts |
node ./scripts/fetch-prompts.mjs |
Fetch community prompt library |
5.2 Health Monitoring
NextChat exposes a /api/config endpoint that returns the server-side configuration (sanitized — no API keys). This can serve as a basic health check endpoint.
Key metrics to monitor in production:
- HTTP 429 responses from upstream providers (rate limiting).
- SSE stream disconnections — usually caused by proxy timeout settings being too low.
- Memory usage — the Next.js server process should remain under 512 MB for typical usage.
- Error rates on
/api/[provider]/[...path]— provider-specific failures.
5.3 Sync Backends
NextChat supports two server-mediated sync backends for cross-device data portability:
WebDAV Sync:
- Client configures a WebDAV server URL, username, and password in Settings → Sync.
- All sync traffic is proxied through
/api/webdav/[...path]to avoid CORS issues. - The
WHITE_WEBDAV_ENDPOINTSenv var controls which destination domains are allowed.
Upstash (Redis) Sync:
- Client configures an Upstash REST endpoint and token.
- Proxied through
/api/upstash/[action]/[...key]. - Provides faster sync for users who prefer a managed Redis backend.
TIP: For enterprise deployments, host your own WebDAV server (e.g., with nginx +
dav_ext_module) and whitelist only that endpoint. This keeps all user data within your infrastructure.
5.4 Logging and Debugging
NextChat does not implement a structured logging framework. Operational visibility comes from:
- Browser console — client-side errors, network failures, store hydration issues.
- Node.js stdout/stderr — server-side API route errors, proxy failures.
- MCP Logger — The
MCPClientLoggerclass (app/mcp/logger.ts) provides color-coded console output for MCP server lifecycle events. - Error boundaries — The
ErrorBoundarycomponent (app/components/error.tsx) catches React rendering errors and displays a fallback UI with error details.
For production Docker deployments, ensure docker logs or a log aggregator captures stdout/stderr from the container.
5.5 Version Update Checking
The useUpdateStore periodically checks for new releases by fetching from the GitHub releases API. The check interval and behavior are controlled client-side. Administrators cannot disable this check server-side — it runs entirely in the browser.
5.6 Localization
NextChat supports 20 languages via locale files in app/locales/. The locale is auto-detected from the browser's navigator.language and can be overridden in Settings. Adding a new locale requires:
- Creating a new file in
app/locales/following the existing pattern. - Registering it in the locale index.
- Rebuilding.
6. Performance
6.1 Key Constants and Timeouts
| Constant | Value | Location | Purpose |
|---|---|---|---|
REQUEST_TIMEOUT_MS |
60,000 ms (60s) | Client constants | Default timeout for API requests |
REQUEST_TIMEOUT_MS_FOR_THINKING |
300,000 ms (5min) | Client constants | Extended timeout for DALL-E, o1, o3, DeepSeek-R, and other "thinking" models |
CHAT_PAGE_SIZE |
15 | Client constants | Messages loaded per pagination page |
MAX_RENDER_MSG_COUNT |
45 | Client constants | Maximum messages rendered simultaneously in the chat view |
DEFAULT_SIDEBAR_WIDTH |
300px | Client constants | Initial sidebar width |
MAX_SIDEBAR_WIDTH |
500px | Client constants | Maximum sidebar width |
MIN_SIDEBAR_WIDTH |
230px | Client constants | Minimum sidebar width |
MOBILE_MAX_WIDTH |
600px | Client constants | Breakpoint for mobile layout |
6.2 Client-Side Performance
- Virtualized rendering — Only
MAX_RENDER_MSG_COUNT(45) messages are rendered in the DOM at once, regardless of conversation length. - Lazy markdown parsing — The
markdown.tsxcomponent defers heavy parsing (syntax highlighting, LaTeX rendering) until the message is visible. - IndexedDB for large data — Chat history is stored in IndexedDB rather than localStorage to avoid the 5-10 MB localStorage limit and reduce main-thread serialization overhead.
- Abort handling —
ChatControllerPool(app/client/controller.ts) tracks active SSE connections per session and provides abort functionality. Navigating away from a chat aborts its active stream.
6.3 Server-Side Performance
The Next.js server is I/O-bound (proxying HTTP requests). CPU usage is minimal. Key performance considerations:
- Streaming — All provider responses are streamed via SSE, not buffered. Memory usage per concurrent request is bounded by the SSE chunk size, not the total response length.
- No database — Zero database queries per request. The server is purely a proxy + config endpoint.
- Cold starts — On serverless platforms (Vercel), cold starts add 1-3 seconds to the first request. Subsequent requests are fast.
TIP: For self-hosted deployments behind nginx, set
proxy_buffering offand increaseproxy_read_timeoutto at least 300 seconds to support long-running streaming responses from thinking models.
7. Security
7.1 Authentication Model
NextChat uses a simple access-code authentication system:
- The administrator sets one or more access codes via the
CODEenvironment variable (comma-separated). - Each code is MD5-hashed using the
spark-md5library before storage/comparison. - Users enter an access code in the Settings panel. The code is hashed client-side and stored in
useAccessStore(localStorage key:access-control). - On every API proxy request,
auth()inapp/api/auth.tschecks either:- The hashed access code in the
Authorizationheader (prefixed withACCESS_CODE_PREFIX="nk-"), OR - A provider-specific API key in the appropriate header.
- The hashed access code in the
- If neither is valid, the request is rejected.
WARNING: The MD5 hashing of access codes provides only obfuscation, not cryptographic security. MD5 is fast to brute-force. For production deployments with sensitive API keys, place NextChat behind an additional authentication layer (OAuth proxy, VPN, IP allowlist) rather than relying solely on access codes.
7.2 Known Security Vulnerabilities
The following security issues have been reported in GitHub issues and should be evaluated before deploying to production:
| Issue | Severity | Description |
|---|---|---|
| #6756 | Medium | CORS wildcard (*) is set on all API endpoints. This allows any origin to make authenticated requests if the user's access code is known. |
| #6754 / #6742 | High | SSRF vulnerability via the x-base-url header in the proxy fallback path. An attacker can make the server issue requests to internal network addresses. |
| #6753 / #6741 | High | Path traversal in the artifacts endpoint (/api/artifacts). Crafted paths can leak server-side secrets including Cloudflare API tokens. |
| #6757 | Critical | Unauthenticated Remote Code Execution report. Details vary by version — review the issue for applicability to your deployment. |
WARNING: These vulnerabilities are architectural and may persist across versions. Before deploying NextChat in any environment with access to sensitive internal networks or valuable API keys, audit the specific version you are deploying against these CVE-equivalent issues. Consider network-level isolation (dedicated VPC, firewall rules blocking internal access from the NextChat container) as defense-in-depth.
7.3 API Key Security
- Server-side keys are read from environment variables and never sent to the client. They are injected into proxied requests server-side.
- Client-side keys (when
HIDE_USER_API_KEYis not set) are stored in the browser's localStorage in plaintext. Any XSS vulnerability would expose them. - Multi-key rotation reduces the blast radius of a single key compromise — rotate the compromised key without downtime.
IMPORTANT: Never commit API keys to the repository or Dockerfile. Use environment variables, Docker secrets, or your platform's secret management (Vercel Environment Variables, Kubernetes Secrets, etc.).
7.4 URL-Based Settings Import (Fast Link)
When DISABLE_FAST_LINK is not set, NextChat parses URL fragments for settings import. A crafted URL can pre-fill provider keys and other settings when a user clicks it. This is a social-engineering vector — an attacker could distribute URLs that configure the victim's instance to proxy through the attacker's endpoint.
Set DISABLE_FAST_LINK=1 in production to close this vector.
7.5 WebDAV Proxy Security
The /api/webdav/[...path] route proxies requests to user-specified WebDAV servers. The WHITE_WEBDAV_ENDPOINTS variable restricts allowed destinations, but:
- The default whitelist may be overly permissive depending on the version.
- SSRF is possible if internal hostnames resolve within the server's network.
Restrict WHITE_WEBDAV_ENDPOINTS to known-good domains and block internal network ranges at the firewall level.
7.6 Security Hardening Checklist
- Set
CODEwith strong, unique access codes. - Set
HIDE_USER_API_KEY=1to prevent client-side key leakage. - Set
DISABLE_FAST_LINK=1to block URL-based configuration injection. - Set
DISABLE_GPT4=1if you want to limit cost exposure. - Override
GA_IDwith an empty string to stop analytics data from flowing to third parties. - Deploy behind a reverse proxy with TLS termination.
- Network-isolate the NextChat container — block outbound access to internal IP ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16) except for legitimate internal services.
- Set
WHITE_WEBDAV_ENDPOINTSto only your approved sync endpoints. - Restrict
CUSTOM_MODELSto only the models you intend to offer (remove expensive or unnecessary models with-prefix). - Run the container as a non-root user.
8. Developer Internals
8.1 Repository Structure
NextChat/
├── app/ # Next.js App Router source
│ ├── api/ # Server-side API routes
│ │ ├── auth.ts # Access code + API key validation
│ │ ├── openai.ts # OpenAI provider handler
│ │ ├── anthropic.ts # Anthropic provider handler
│ │ ├── google.ts # Google/Gemini provider handler
│ │ ├── tencent/route.ts # Tencent signing + proxy
│ │ ├── config/route.ts # Server config endpoint
│ │ ├── artifacts/route.ts # HTML artifact serving
│ │ ├── webdav/ # WebDAV sync proxy
│ │ ├── upstash/ # Upstash sync proxy
│ │ └── [provider]/ # Unified catch-all proxy
│ ├── client/ # Client-side API layer
│ │ ├── api.ts # ClientApi dispatcher class
│ │ ├── controller.ts # ChatControllerPool (stream lifecycle)
│ │ └── platforms/ # 14 provider implementations
│ │ ├── openai.ts
│ │ ├── anthropic.ts
│ │ ├── google.ts
│ │ └── ... (11 more)
│ ├── components/ # React UI components
│ │ ├── home.tsx # Main layout + router
│ │ ├── chat.tsx # Chat interface
│ │ ├── settings.tsx # Settings panel
│ │ ├── sidebar.tsx # Navigation sidebar
│ │ ├── mask.tsx # Persona/mask editor
│ │ ├── new-chat.tsx # New chat creation
│ │ ├── plugin.tsx # Plugin management
│ │ ├── artifacts.tsx # HTML artifact renderer (iframe sandbox)
│ │ ├── exporter.tsx # Chat export (Markdown, PNG, ShareGPT)
│ │ ├── markdown.tsx # Markdown renderer (highlight.js, KaTeX)
│ │ ├── mcp-market.tsx # MCP server marketplace
│ │ ├── model-config.tsx # Model parameter controls
│ │ ├── error.tsx # ErrorBoundary component
│ │ ├── ui-lib.tsx # Shared UI primitives
│ │ ├── realtime-chat/ # Real-time voice chat components
│ │ ├── sd/ # Stable Diffusion UI
│ │ └── voice-print/ # Voice visualization
│ ├── config/ # Configuration modules
│ │ ├── server.ts # getServerSideConfig() — all 77 env vars
│ │ ├── client.ts # getClientConfig() — build metadata
│ │ └── build.ts # getBuildConfig() — version, commit hash
│ ├── store/ # Zustand persisted stores
│ │ ├── access.ts # Auth, provider keys, server config cache
│ │ ├── chat.ts # Sessions, messages, MCP tool calls
│ │ ├── config.ts # UI + model settings
│ │ ├── mask.ts # Persona management
│ │ ├── plugin.ts # OpenAPI plugin state
│ │ ├── prompt.ts # User prompt library
│ │ ├── sd.ts # Stable Diffusion state
│ │ ├── sync.ts # Sync configuration
│ │ └── update.ts # Version checking
│ ├── mcp/ # Model Context Protocol
│ │ ├── actions.ts # Server actions (start/stop/call)
│ │ ├── client.ts # MCP SDK client wrapper
│ │ ├── logger.ts # MCPClientLogger
│ │ ├── types.ts # Zod schemas for MCP
│ │ ├── utils.ts # JSON extraction from chat messages
│ │ └── mcp_config.default.json
│ ├── locales/ # 20 language translation files
│ ├── masks/ # Built-in persona definitions + build script
│ ├── utils/ # 19 utility modules
│ │ ├── indexedDB-storage.ts # IndexedDBStorage class
│ │ ├── ms_edge_tts.ts # Edge TTS client (MsEdgeTTS class)
│ │ ├── format.ts # Text formatting utilities
│ │ ├── model.ts # Model capability detection
│ │ ├── token.ts # Token counting
│ │ └── ... (14 more)
│ ├── layout.tsx # Root layout (meta, fonts, analytics)
│ └── page.tsx # Entry: renders Home component
├── src-tauri/ # Tauri desktop app (Rust)
│ ├── src/
│ │ ├── main.rs # Tauri entry, registers stream_fetch
│ │ └── stream.rs # StreamResponse, ChunkPayload, EndPayload
│ ├── Cargo.toml # Rust deps (reqwest, tauri, tokio, etc.)
│ └── tauri.conf.json # Window config, bundle identifiers
├── public/ # Static assets (icons, robots.txt)
├── scripts/ # Build/fetch scripts
│ └── fetch-prompts.mjs # Community prompt fetcher
├── Dockerfile # Container build definition
├── docker-compose.yml # Compose configuration
├── package.json # Dependencies, scripts
├── tsconfig.json # TypeScript configuration
├── next.config.mjs # Next.js configuration
├── jest.config.ts # Jest test configuration
├── .eslintrc.json # ESLint rules
├── .prettierrc # Prettier formatting rules
└── .husky/ # Git hooks (pre-commit lint-staged)
8.2 Class Reference
| Class | File | Purpose |
|---|---|---|
ClientApi |
app/client/api.ts |
Top-level API dispatcher. Routes requests to the correct provider platform based on the model's provider field. |
ChatControllerPool |
app/client/controller.ts |
Manages active SSE connections per chat session. Provides stop() and stopAll() for abort control. |
IndexedDBStorage |
app/utils/indexedDB-storage.ts |
Zustand persist adapter wrapping idb-keyval. Implements getItem(), setItem(), removeItem() for the persist middleware. |
MsEdgeTTS |
app/utils/ms_edge_tts.ts |
Client for Microsoft Edge's text-to-speech WebSocket API. Handles SSML generation, WebSocket connection, and audio chunk reassembly. |
ProsodyOptions |
app/utils/ms_edge_tts.ts |
Configuration object for Edge TTS voice prosody (rate, pitch, volume). |
MCPClientLogger |
app/mcp/logger.ts |
Colored console logger for MCP server lifecycle events. Prefixes output with client ID for multi-server debugging. |
ErrorBoundary |
app/components/error.tsx |
React error boundary. Catches component tree errors and renders a fallback UI with stack trace and retry button. |
AudioHandler |
app/lib/audio.ts |
Browser audio recording handler using MediaRecorder API. Manages start/stop/data lifecycle for voice input. |
StreamResponse |
src-tauri/src/stream.rs |
Rust struct representing a streaming HTTP response in the Tauri IPC bridge. |
ChunkPayload |
src-tauri/src/stream.rs |
Rust struct for individual stream data chunks sent via Tauri events. |
EndPayload |
src-tauri/src/stream.rs |
Rust struct signaling stream completion or error via Tauri events. |
8.3 Provider Platform Pattern
Each provider in app/client/platforms/ follows a consistent pattern:
export class SomeProviderApi implements LLMApi {
// Extract headers (API key, version, org ID)
private getHeaders(): Record<string, string> { ... }
// Build the request body with provider-specific field mappings
private formatMessages(messages: ChatMessage[]): ProviderMessage[] { ... }
// Main chat method — streams SSE response
async chat(options: ChatOptions): Promise<void> {
// 1. Build request from options
// 2. Call fetchEventSource or fetch with streaming
// 3. Parse SSE chunks into delta text
// 4. Call options.onUpdate(delta) per chunk
// 5. Call options.onFinish(fullText) on completion
// 6. Call options.onError(err) on failure
}
// Model listing (some providers support dynamic model discovery)
async models(): Promise<LLMModel[]> { ... }
}
The ClientApi class wraps all providers and dispatches based on the ServiceProvider enum.
8.4 State Management Patterns
Store creation:
export const useSomeStore = createPersistStore(
{ /* default state */ },
(set, get) => ({
// actions
someAction() {
set({ field: newValue });
},
}),
{
name: "store-key",
version: 1.0,
migrate(state, version) {
// version-based migration logic
return state;
},
}
);
Persistence flow:
- Zustand's
persistmiddleware serializes state to the configured storage backend. IndexedDBStorageis used for stores with large data (primarilyuseChatStore).- Standard
localStorageis used for smaller stores. - On app load, stores hydrate from persisted data and run migrations if the version has changed.
8.5 MCP (Model Context Protocol) Integration
MCP enables the chat interface to invoke external tool servers:
- Configuration — MCP servers are defined in
app/mcp/mcp_config.json(or the user's custom config). Each server specifies a command and args forStdioClientTransport. - Lifecycle — Server actions in
app/mcp/actions.tsmanage the lifecycle:startMcpServer(),stopMcpServer(),listMcpTools(),callMcpTool(). - Server states —
undefined→initializing→active(orerror). Servers can bepausedand resumed. - Chat integration — When
ENABLE_MCP=true, the assistant's responses are scanned for fenced JSON blocks tagged withjson:mcp:{clientId}. TheexecuteMcpAction()function parses these blocks and calls the corresponding MCP tool viacallMcpTool(). - Supported methods — Only
tools/callis implemented. Resource and prompt MCP capabilities are not used. - Validation — MCP schemas are validated with Zod (
app/mcp/types.ts).
NOTE: MCP server processes run on the Next.js server, not in the browser. This means MCP is unavailable in static export / Tauri desktop mode. It requires a persistent server process.
8.6 Plugin Architecture
NextChat supports OpenAPI-based plugins for extending assistant capabilities:
- Specification — Plugins are defined by an OpenAPI YAML/JSON spec URL. The
openapi-client-axioslibrary parses the spec and generates a type-safe client. - Authentication — Three auth modes:
basic(username/password),bearer(token), orcustom(arbitrary header name/value). - Execution modes:
- Web mode — Plugin API calls are proxied through
/api/proxywith the target URL in anX-Base-URLheader. This avoids CORS issues but inherits the SSRF risk noted in the security section. - Tauri mode — Plugin API calls go directly from the Rust backend to the target server, bypassing CORS entirely.
- Web mode — Plugin API calls are proxied through
- Built-in plugins — Loaded from
/plugins.jsonin the public directory. - State — Managed in
usePluginStore(localStorage key:chat-next-web-plugin).
8.7 Error Messages Reference
| Error Message | Source File | Context |
|---|---|---|
Network response was not ok |
app/utils/ms_edge_tts.ts:263 |
Edge TTS WebSocket connection failure |
Could not infer voiceLocale from voiceName! |
app/utils/ms_edge_tts.ts:296 |
Invalid voice name passed to TTS |
Server ${clientId} not found |
app/mcp/actions.ts:201,237 |
MCP server referenced but not configured |
Client ${clientId} not found |
app/mcp/actions.ts:344 |
MCP client instance not initialized |
Failed to load preset servers |
app/components/mcp-market.tsx:99 |
MCP marketplace fetch failure |
Failed to load tools |
app/components/mcp-market.tsx:235 |
MCP tool listing failure |
Recording not started |
app/lib/audio.ts:88 |
Audio stop called before start |
empty response |
Various provider clients | LLM provider returned empty body |
8.8 Testing Strategy
- Framework: Jest 29 with Testing Library for component tests.
- Configuration:
jest.config.tsat root, TypeScript test files. - Commands:
yarn test(watch mode for development),yarn test:ci(single pass for CI). - Lint enforcement: Husky pre-commit hooks run
lint-staged(ESLint + Prettier on staged files). - Coverage gaps: The TypeScript extraction yielded 0 functions/actions/CLI commands due to the extraction tool's TypeScript parsing limitations. Test coverage should focus on:
- Provider API handler request/response transformations.
- Auth logic in
app/api/auth.ts. - Store migration functions.
- Markdown rendering edge cases.
- MCP JSON block parsing in
app/mcp/utils.ts.
8.9 Code Conventions
- TypeScript strict mode —
tsconfig.jsonenforces strict type checking. - SCSS Modules — Component styles are co-located as
.module.scssfiles. No global CSS beyond minimal resets. - Functional React — All components are functional (hooks-based). The only class component is
ErrorBoundary(required by React's error boundary API). - Named exports — Stores and components use named exports throughout.
- Path aliases —
@/maps to theapp/directory in imports. - Constant naming —
UPPER_SNAKE_CASEfor module-level constants,camelCasefor local variables and functions. - Enum usage —
ServiceProviderenum maps provider names to their string identifiers across the codebase.
9. Integration Points
9.1 LLM Provider Integrations
NextChat integrates with 16 LLM providers through a unified proxy architecture. Each integration follows the same pattern (auth → model check → proxy → stream) but with provider-specific adaptations:
| Provider | Client Module | API Route | Protocol | Notes |
|---|---|---|---|---|
| OpenAI | platforms/openai.ts |
/api/openai/ |
REST + SSE | Default provider. Chat + DALL-E image generation. |
| Azure OpenAI | platforms/openai.ts |
/api/openai/ |
REST + SSE | Shares OpenAI client with Azure-specific URL/key/version. |
| Anthropic | platforms/anthropic.ts |
/api/anthropic/ |
REST + SSE | Claude models. Different message format (system prompt handling). |
| Google Gemini | platforms/google.ts |
/api/google/ |
REST + SSE | Gemini models. Multimodal support (vision). |
| Baidu (Ernie) | platforms/baidu.ts |
/api/baidu/ |
REST + SSE | Requires OAuth token exchange with secret key. |
| ByteDance (Doubao) | platforms/bytedance.ts |
/api/bytedance/ |
REST + SSE | Volcengine ML platform models. |
| Alibaba (Qwen) | platforms/alibaba.ts |
/api/alibaba/ |
REST + SSE | DashScope API. Qwen models. |
| Tencent (Hunyuan) | platforms/tencent.ts |
/api/tencent/ |
REST + SSE | Requires HMAC signing (TC3-HMAC-SHA256). Dedicated route. |
| Moonshot | platforms/moonshot.ts |
/api/moonshot/ |
REST + SSE | OpenAI-compatible API. |
| iFlytek (Spark) | platforms/iflytek.ts |
/api/iflytek/ |
REST + SSE | Requires both API key and API secret. |
| DeepSeek | platforms/deepseek.ts |
/api/deepseek/ |
REST + SSE | OpenAI-compatible. Thinking models use extended timeout. |
| xAI (Grok) | platforms/xai.ts |
/api/xai/ |
REST + SSE | OpenAI-compatible API. |
| ChatGLM | platforms/glm.ts |
/api/glm/ |
REST + SSE | Zhipu AI platform. |
| SiliconFlow | platforms/siliconflow.ts |
/api/siliconflow/ |
REST + SSE | Multi-model inference platform. |
| 302.AI | platforms/ai302.ts |
/api/ai302/ |
REST + SSE | Aggregator platform. |
| Stability AI | platforms/stability.ts |
/api/stability/ |
REST | Image generation only. Not chat. |
9.2 Sync Integrations
| Backend | Protocol | Proxy Route | Configuration |
|---|---|---|---|
| WebDAV | HTTP(S) | /api/webdav/[...path] |
URL, username, password in useSyncStore |
| Upstash | REST | /api/upstash/[action]/[...key] |
Endpoint URL, token in useSyncStore |
9.3 External Service Integrations
| Service | Purpose | Configuration |
|---|---|---|
| Google Tag Manager | Analytics | GTM_ID env var |
| Google Analytics | Usage tracking | GA_ID env var (default tracks upstream project) |
| Cloudflare KV | Response caching | CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_KV_NAMESPACE_ID, CLOUDFLARE_KV_API_KEY, CLOUDFLARE_KV_TTL |
| Microsoft Edge TTS | Text-to-speech | No configuration — uses public WebSocket endpoint |
| GitHub Releases API | Version update checking | No configuration — hardcoded upstream repo |
| Community Prompts | Prompt library | Fetched via yarn prompts from external source |
9.4 Tauri Desktop Integration
The Tauri layer bridges web and native functionality:
stream_fetch— A Tauri command registered inmain.rsthat performs HTTP requests viareqwestand streams responses back to the webview via Tauri events. This bypasses CORS restrictions entirely, allowing the desktop app to call provider APIs directly without the Next.js proxy.StreamResponse/ChunkPayload/EndPayload— Rust structs instream.rsthat serialize stream data for the Tauri event system.- Window configuration — Defined in
tauri.conf.json: title, dimensions, resizability, decorations, and CSP policy. - Build targets — macOS (
.dmg,.app), Windows (.msi), Linux (.AppImage,.deb).
NOTE: In Tauri/desktop mode, MCP is unavailable (no server-side process), but CORS restrictions are absent. Plugin API calls go directly to target servers without the
/api/proxyintermediary.
9.5 Extending NextChat
Adding a new LLM provider:
- Create
app/client/platforms/newprovider.tsimplementing theLLMApiinterface. - Add the provider to the
ServiceProviderenum. - Register it in
ClientApi's provider map (app/client/api.ts). - Create API route handler(s) in
app/api/following the auth-check-proxy pattern. - Add environment variables for API key and optional custom URL to
app/config/server.ts. - Add UI strings to all locale files in
app/locales/.
Adding a new Zustand store:
- Define the store using
createPersistStore()with a unique key and version. - Use
IndexedDBStoragefor stores expected to hold large data;localStorageotherwise. - Always include a
migrate()function, even if initially empty.
10. Troubleshooting
10.1 Common Issues
| Symptom | Likely Cause | Resolution |
|---|---|---|
| "Unauthorized" on every request | CODE is set but user hasn't entered a valid access code |
Verify the access code matches one of the comma-separated values in CODE. Codes are case-sensitive. |
| Streaming responses cut off mid-sentence | Reverse proxy timeout too low | Increase proxy_read_timeout (nginx) or equivalent to 300+ seconds. |
| "Failed to fetch" in browser console | CORS or network issue | Check that the Next.js server is accessible. If behind a proxy, ensure it forwards the Content-Type: text/event-stream header. |
| Chat history disappeared after upgrade | Store schema migration failure | Check browser console for migration errors. Clear localStorage key chat-next-web-store and reimport from backup (Settings → Backup). |
| MCP tools not appearing | ENABLE_MCP not set or MCP config invalid |
Set ENABLE_MCP=true and verify app/mcp/mcp_config.json contains valid server definitions. Check server logs for MCP process startup errors. |
| Desktop app shows blank white screen | Tauri WebView issue | On Linux, ensure webkit2gtk is installed. On Windows, ensure WebView2 runtime is present. |
| Models missing from selection | CUSTOM_MODELS filtering |
Check that CUSTOM_MODELS doesn't exclude the desired model with a - prefix. |
| Azure OpenAI not working | Missing AZURE_API_VERSION |
Azure requires all three variables: AZURE_URL, AZURE_API_KEY, and AZURE_API_VERSION. |
| High memory usage in browser | Large chat history in IndexedDB | Export and delete old conversations. The MAX_RENDER_MSG_COUNT (45) limits DOM rendering but not storage. |
TIP: For persistent issues, open the browser's developer console (F12) and check the Network tab for failed requests. The response body from failed API proxy calls often contains the upstream provider's error message.
NOTE: When running in Docker, use
docker logs <container>to capture server-side errors. Add-ffor real-time tailing during debugging sessions.
10.2 Environment Variable Debugging
The /api/config endpoint returns the server's parsed configuration (with API keys redacted). Fetch it to verify that environment variables are being read correctly:
curl http://localhost:3000/api/config | jq .
Check for:
needCode: true— confirmsCODEis set.- Provider-specific
isAvailableflags — confirms the corresponding API key is present. customModels— confirmsCUSTOM_MODELSis parsed correctly.
10.3 Store Reset Procedures
If a store becomes corrupted:
- Single store — In browser console:
localStorage.removeItem("store-key")and refresh. For IndexedDB stores: use browser DevTools → Application → IndexedDB to delete the database. - Full reset — Clear all site data via browser DevTools → Application → Clear Storage.
- Before resetting — Always export data first via Settings → Backup → Export to avoid data loss.