Skip to Content
Deneva MCPDevDeploy

Deploying changes to the server

Local — push the commit

git add <files> git commit -m "your message" git push origin master

Server — apply the changes

SSH in and pull:

ssh user@app.deneva.io cd /opt/deneva-mcp # adjust if you cloned elsewhere git pull origin master

Install any new dependencies:

npm ci --omit=dev

Build TypeScript:

npm run build

Run database migrations (only if schema changed):

npm run db:migrate

Restart the service:

sudo systemctl restart deneva-mcp sudo systemctl status deneva-mcp # confirm it's active (running)

Smoke-test:

curl -s -o /dev/null -w "%{http_code}" https://app.deneva.io/health # → 200

When to run each step

What changednpm cinpm run builddb:migratesystemd unit editsystemctl restart
TypeScript source onlyyesyes
New/removed npm packageyesyesyes
Drizzle schema fileyesyesyes
Config / env onlyyes
New REQUIRED_SECRETS entryyesyes (LoadCredentialEncrypted= + .cred)yes
New loadConfig() required fieldyesyes (Environment=)yes

Phase 2 deploy heads-up

PR-2 through PR-7 land together. Before the first restart after pulling those commits, the unit needs four new lines (two Environment= and two LoadCredentialEncrypted= — see setup-ubuntu.md Step 10) and scripts/encrypt-prod-secrets.sh has to be re-run to generate the two new .cred files. Without them the process exits non-zero before binding a port. PR-5 also adds an npm dep (google-ads-api), so npm ci --omit=dev is mandatory. Full procedure: phase-2-runbook.md §3a.

Phase 2 ops commands

WhatHow
Disconnect a tenant from Google (best-effort revoke + local wipe)curl -X POST -H "x-admin-token: $ADMIN_TOKEN" https://app.deneva.io/admin/tenants/<uuid>/disconnect/google
Inspect cache hit/miss counterscurl -H "x-admin-token: $ADMIN_TOKEN" https://app.deneva.io/admin/metrics/cache
Rotate a tenant’s DEK (re-encrypts all tokens under a fresh key)On the server, in /opt/deneva-mcp or wherever the deploy lives: npx tsx scripts/rotate-dek.ts <tenantId>

The DEK rotation script imports src/db/index.ts so it connects as mcp_app (NOBYPASSRLS) — same as the runtime. It sets app.current_tenant_id inside the outer transaction so RLS lets the script INSERT/UPDATE/DELETE the tenant’s platform_credentials and tenant_deks rows.

In dev the script reads secrets from ./secrets/. In prod it depends on systemd-creds: secrets.loader looks at /run/credentials/<unit>/<NAME> when NODE_ENV=production. Those paths are only populated while the deneva-mcp service is running (systemd-creds is a tmpfs unmount on stop), and only the deneva-mcp user can read them.

The cleanest way to run rotation on the live server right now (with the service still running):

# tsx is in devDependencies — install it for this one invocation if you # deployed with `npm ci --omit=dev`: cd /opt/deneva-mcp && npm install --no-save tsx sudo -u deneva-mcp env NODE_ENV=production \ SYSTEMD_UNIT=deneva-mcp.service \ npx tsx /opt/deneva-mcp/scripts/rotate-dek.ts <tenantId>

Phase 5 will move this to a dedicated ops command path with its own credential mount (so rotation doesn’t piggyback on the live service’s tmpfs) and ship a built script in dist/ (so tsx isn’t required on the prod box).