Deployment
Put your app online so that the AgentDB key stays on the server. This page covers a traditional server (VPS) with nginx, and Netlify and Vercel.
#What nginx does, and what it does not
nginx is a web server and a reverse proxy. It serves files, passes requests to your backend, and handles HTTPS.
nginx is not authentication. It does not know your users, and it cannot decide who may read which contact. Your backend signs users in and checks what they may do. AgentDB checks the key your backend sends.
#The shape of a deployment
browser ──HTTPS──> nginx ──> static files (HTML, JS, CSS) no key
└──> your backend on 127.0.0.1:3000 holds the AgentDB key
└──HTTPS──> https://app.agentdb.link- The browser talks to one origin. It never calls AgentDB.
- The backend listens on
127.0.0.1only. nginx is the only way in. - The key is in the backend's environment, nowhere else.
#Credentials and environment variables
- Make one agent key per app with the smallest role it needs:
viewerto read,editorto write records. See Least-privilege keys. - Store it in the service's environment, for example a systemd
EnvironmentFilethat only the service user can read (chmod 600). - Never commit
.env. Commit.env.examplewith placeholders only. - Never copy the key into the static files, the frontend build, a
<meta>tag or a JavaScript variable. - To replace a key, create a new one, deploy it, then revoke the old one.
# /etc/systemd/system/crm.service
[Service]
User=crm
WorkingDirectory=/srv/crm
EnvironmentFile=/srv/crm/.env
ExecStart=/usr/bin/node src/server.ts
Restart=on-failure#Static frontend hosting
nginx serves the built frontend from a folder. Files with a content hash in the name can be cached for a long time. index.html must be checked on every visit, or users keep an old version.
root /srv/crm/public;
location = /index.html {
add_header Cache-Control "no-cache" always;
}
location ~* \.(?:js|css|woff2|png|svg)$ {
# Use "immutable" only for file names that change with their content (app.3f9a1c.js).
add_header Cache-Control "public, max-age=31536000, immutable" always;
}
location / {
try_files $uri $uri/ /index.html; # single-page apps; use =404 for plain sites
}#Reverse-proxying the backend
location /api/ {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
add_header Cache-Control "no-store" always; # API answers are per user and change often
client_max_body_size 1m;
}Cache-Control in short: static files with hashed names get a long cache, index.html gets no-cache, and API answers get no-store.
#HTTPS with certbot
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d crm.example.com
sudo certbot renew --dry-run # checks that automatic renewal workscertbot adds the certificate to the nginx site and a redirect from HTTP to HTTPS. Renewal runs on a timer.
#Secure headers
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;- CSP lets the page load scripts and send requests to its own origin only.
connect-src 'self'also means the page cannot call AgentDB directly, which is what you want. - nosniff stops browsers from guessing file types.
- HSTS tells browsers to use HTTPS only. Add it after HTTPS works. Start with a short
max-ageif you are unsure. - nginx drops
add_headerlines from theserverblock when alocationhas its ownadd_header. Repeat the security headers there, or put them in a shared include file.
#CORS
Prefer no CORS at all. Serve the frontend and the backend from one origin, as above, and the browser needs no CORS headers.
If the frontend must live on another origin, allow exactly that origin on your backend. Never * for routes that use cookies.
# Only when the frontend is on another origin:
add_header Access-Control-Allow-Origin "https://www.example.com" always;
add_header Vary "Origin" always;CORS is not access control. It only tells browsers which pages may read the answer. Scripts and servers ignore it.
#Live connections
Server-sent events and long-polls keep a request open. nginx buffers answers and closes idle connections after 60 seconds by default. Change both for the live route:
location /events {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_buffering off; # send each event at once
proxy_cache off;
proxy_read_timeout 1h; # longer than the gap between events or pings
add_header Cache-Control "no-store" always;
}The live dashboard starter also sends X-Accel-Buffering: no and a ping every 20 seconds. Long-polls to AgentDB (wait=1) answer within about 25 seconds, so keep your backend's own request timeout above 30 seconds.
#A complete nginx site
server {
listen 443 ssl;
http2 on;
server_name crm.example.com;
# ssl_certificate lines: added by certbot
root /srv/crm/public;
include /etc/nginx/snippets/security-headers.conf; # the four add_header lines above
location /api/ { proxy_pass http://127.0.0.1:3000; add_header Cache-Control "no-store" always; include /etc/nginx/snippets/security-headers.conf; }
location /events { proxy_pass http://127.0.0.1:3000; proxy_http_version 1.1; proxy_set_header Connection ""; proxy_buffering off; proxy_read_timeout 1h; }
location / { try_files $uri $uri/ /index.html; }
}
server {
listen 80;
server_name crm.example.com;
return 301 https://$host$request_uri;
}For PHP, replace the /api/ block with your PHP-FPM settings and point root at the app's public/ folder. Keep .env outside that folder.
#Netlify and Vercel
On these hosts, serverless functions are your backend. They hold the key. The static frontend calls them on the same origin.
| Netlify | Vercel | |
|---|---|---|
| Backend code | netlify/functions/*.mjs | api/*.js, or route handlers in Next.js |
| Where the key goes | Site settings, Environment variables | Project settings, Environment Variables |
| Called from the page as | /.netlify/functions/contacts (or a redirect to /api/contacts) | /api/contacts |
A Netlify function that lists contacts:
// netlify/functions/contacts.mjs
export default async (req) => {
const q = (new URL(req.url).searchParams.get("q") ?? "").trim().slice(0, 100);
const res = await fetch(`https://app.agentdb.link/v1/workspaces/${process.env.AGENTDB_WORKSPACE}/query`, {
method: "POST",
headers: { authorization: `Bearer ${process.env.AGENTDB_TOKEN}`, "content-type": "application/json" },
body: JSON.stringify({ object: "contact", where: { all: q ? [{ field: "name", op: "contains", value: q }] : [] }, limit: 50 }),
});
const body = await res.json();
if (!res.ok) return Response.json({ error: "The data service is not available." }, { status: 502 });
return Response.json({ contacts: body.data.records.map((r) => ({ id: r.id, name: r.data.name })) }, { headers: { "cache-control": "no-store" } });
};Never give the key a public prefix. Build tools copy these variables into the browser bundle:
NEXT_PUBLIC_AGENTDB_TOKEN=adbk.your-key
VITE_AGENTDB_TOKEN=adbk.your-key
REACT_APP_AGENTDB_TOKEN=adbk.your-keyAGENTDB_WORKSPACE=ws_your_workspace_id
AGENTDB_TOKEN=replace-with-your-agent-key- Set the variables for the production context only, or use a separate key for preview deploys. Preview URLs are often public.
- Serverless functions end after each request. They cannot keep a change-feed loop or server-sent events open for long. For live pages, poll your function every 10 to 30 seconds, or use a read-only dashboard data link from the browser.
- Set the security headers in
netlify.toml([[headers]]) orvercel.json(headers).
#Traditional VPS
- Create a user for the app and copy the code to its folder.
- Install the runtime (Node 22, PHP-FPM, Python or .NET).
- Put the agent key in the service's environment file. Make it readable by the service user only.
- Run the backend as a systemd service on
127.0.0.1. - Configure nginx with the site above, then run certbot.
- Open the firewall for ports 80 and 443 only.
- Check: the page loads over HTTPS,
/api/...works, and the browser's network tab shows no request to AgentDB and no key.
#Checklist
- The key is in the server environment only. Search your built frontend for
adbk.and find nothing. - Each app has its own agent key with the smallest role.
- The frontend calls your own origin.
connect-src 'self'in the CSP. - HTTPS works, HTTP redirects to HTTPS, HSTS is on.
index.htmlhasno-cache, API answers haveno-store.- Live routes have proxy buffering off and long read timeouts.
- Your backend checks who the user is before it calls AgentDB for them.