4.1 KiB
DeepSeek AI Proxy (nginx auth-injecting reverse proxy)
ShopProQuote uses an auth-injecting nginx reverse proxy to forward /deepseek/ requests to api.deepseek.com. The proxy validates the client's PocketBase token before injecting the DeepSeek API key, so the key never reaches the browser.
Architecture (Roadmap 1.2 — fully deployed 2026-07-06)
Browser → /deepseek/v1/chat/completions
└→ nginx [auth_request /_spq_validate]
├→ 127.0.0.1:8092 (spq-ai-validator, systemd)
│ └→ POST http://127.0.0.1:8091/api/collections/users/auth-refresh
│ with client's Bearer token → 200 or 401
└→ on 200: strip client Authorization, inject DeepSeek key,
rewrite path, proxy_pass https://api.deepseek.com (streaming)
Components
1. Token validator — /opt/spq/ai-proxy/validate.js
Node HTTP service (no deps) on 127.0.0.1:8092. Validates the incoming Bearer token against PocketBase's /api/collections/users/auth-refresh endpoint. Returns 200 (valid) or 401 (invalid).
Key detail: uses auth-refresh, NOT refresh — the roadmap spec wrote /api/collections/users/refresh which is wrong for PB 0.39. The correct endpoint is auth-refresh.
2. systemd unit — /etc/systemd/system/spq-ai-validator.service
Enabled + auto-restart on failure. Environment: PB_URL=http://127.0.0.1:8091, PORT=8092.
If the unit file changes on disk, run sudo systemctl daemon-reload to clear the stale-config warning. No restart needed if the service is already running fine.
3. nginx snippet — /etc/nginx/snippets/spq-deepseek.conf
location /deepseek/ {
limit_req zone=spq_ai burst=5 nodelay; # 20r/m rate limit
auth_request /_spq_validate; # token validation gate
auth_request_set $spq_auth_status $upstream_status;
proxy_set_header Authorization ""; # strip client's token
include /etc/nginx/snippets/deepseek-key.conf; # sets $deepseek_key
proxy_set_header Authorization "Bearer $deepseek_key";
rewrite ^/deepseek/(.*)$ /$1 break;
proxy_pass https://api.deepseek.com;
proxy_ssl_server_name on;
proxy_set_header Host api.deepseek.com;
proxy_set_header X-Real-IP $remote_addr;
proxy_buffering off; # streaming responses
}
location = /_spq_validate {
internal;
proxy_pass http://127.0.0.1:8092/;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header Authorization $http_authorization;
}
The limit_req_zone $binary_remote_addr zone=spq_ai:10m rate=20r/m; directive is in /etc/nginx/nginx.conf:15, not in the snippet.
4. API key — /etc/nginx/snippets/deepseek-key.conf
Mode 0600, root-owned. Contains set $deepseek_key "sk-...";. Included by the snippet, never shipped to the client. Do NOT read this file unless explicitly needed. The key is NO LONGER in the site config directly (old location, now migrated to the separate 0600 snippet file).
5. Inclusion
Included in the shopproquote server block at /etc/nginx/sites-enabled/shopproquote via include /etc/nginx/snippets/spq-deepseek.conf;.
Frontend — src/lib/ai.ts
Calls /deepseek/v1/chat/completions with the PocketBase token as Bearer. Handles 401 (invalid/missing token) and 429 (rate limited) gracefully by returning null.
Model
deepseek-v4-flash with thinking: { type: 'disabled' }. The thinking parameter is critical — without it, DeepSeek-v4-flash puts all tokens into reasoning content and returns empty content field.
Dev proxy (vite.config.ts) — intentionally stale
vite.config.ts still targets https://127.0.0.1:3448 for /deepseek — a dead gateway. Ray confirmed (2026-07-07) he runs prod only (nginx/dist), never npm run dev. The dev proxy was intentionally left stale. If dev-mode AI testing is ever needed, point it at http://127.0.0.1:80.
Verify the proxy is live
# Validator running?
systemctl status spq-ai-validator
ss -tlnp | grep 8092
# nginx config valid?
sudo nginx -t
# Snippet included in site?
grep spq-deepseek /etc/nginx/sites-enabled/shopproquote