Deploying changes to the server
Local — push the commit
git add <files>
git commit -m "your message"
git push origin masterServer — apply the changes
SSH in and pull:
ssh user@app.deneva.io
cd /opt/deneva-mcp # adjust if you cloned elsewhere
git pull origin masterInstall any new dependencies:
npm ci --omit=devBuild TypeScript:
npm run buildRun database migrations (only if schema changed):
npm run db:migrateRestart 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
# → 200When to run each step
| What changed | npm ci | npm run build | db:migrate | systemd unit edit | systemctl restart |
|---|---|---|---|---|---|
| TypeScript source only | — | yes | — | — | yes |
| New/removed npm package | yes | yes | — | — | yes |
| Drizzle schema file | — | yes | yes | — | yes |
| Config / env only | — | — | — | — | yes |
New REQUIRED_SECRETS entry | — | yes | — | yes (LoadCredentialEncrypted= + .cred) | yes |
New loadConfig() required field | — | yes | — | yes (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
| What | How |
|---|---|
| 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 counters | curl -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).