AgentDBDocs

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

Text
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.1 only. 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: viewer to read, editor to write records. See Least-privilege keys.
  • Store it in the service's environment, for example a systemd EnvironmentFile that only the service user can read (chmod 600).
  • Never commit .env. Commit .env.example with 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.
ini
# /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.

nginx
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

nginx
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

Shell
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d crm.example.com
sudo certbot renew --dry-run    # checks that automatic renewal works

certbot adds the certificate to the nginx site and a redirect from HTTP to HTTPS. Renewal runs on a timer.

#Secure headers

nginx
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-age if you are unsure.
  • nginx drops add_header lines from the server block when a location has its own add_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.

nginx
# 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:

nginx
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

nginx
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.

NetlifyVercel
Backend codenetlify/functions/*.mjsapi/*.js, or route handlers in Next.js
Where the key goesSite settings, Environment variablesProject 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:

JavaScript
// 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:

Shell: wrong, do not copy
NEXT_PUBLIC_AGENTDB_TOKEN=adbk.your-key
VITE_AGENTDB_TOKEN=adbk.your-key
REACT_APP_AGENTDB_TOKEN=adbk.your-key
Shell: secure, do this
AGENTDB_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]]) or vercel.json (headers).

#Traditional VPS

  1. Create a user for the app and copy the code to its folder.
  2. Install the runtime (Node 22, PHP-FPM, Python or .NET).
  3. Put the agent key in the service's environment file. Make it readable by the service user only.
  4. Run the backend as a systemd service on 127.0.0.1.
  5. Configure nginx with the site above, then run certbot.
  6. Open the firewall for ports 80 and 443 only.
  7. 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.html has no-cache, API answers have no-store.
  • Live routes have proxy buffering off and long read timeouts.
  • Your backend checks who the user is before it calls AgentDB for them.