Common Issues

Startup, health, licensing, and migration failure modes, framed as symptom → cause → resolution. Simcha DB Studio is two processes: a Next.js frontend (port 3000) that proxies every browser call to an Express backend (port 5002), backed by an internal PostgreSQL metadata database named changeops. Most "the app is broken" reports resolve to one of a handful of product-specific states.

Startup Is Gated on changeops DB Readiness

Symptom: GET /api/health returns { status: "STARTING" } and stays there; the UI renders but every data panel is empty; login fails.

Cause: The backend does not consider itself ready until the changeops pool is reachable and migrations have run. Startup retries the connection 10 times with a 3s delay (~30s window). If the DB is unreachable for the whole window, the server stays in STARTING and auto-migrations never complete, so schema-dependent routes (auth, connections, scripts) fail.

Resolution: Read the backend stdout for the connection error emitted on each retry, then verify the target the app is actually using — not the one you assume:

# The changeops target the process resolved (legacy DB_* are accepted as fallbacks)
docker exec simcha-db-studio env | grep -E "DB_DEVOPS_DB_|^DB_"

# Prove reachability from the app's network namespace, not your host
docker exec simcha-db-studio node -e "const{Pool}=require('pg');new Pool({host:process.env.DB_DEVOPS_DB_HOST,port:process.env.DB_DEVOPS_DB_PORT,database:process.env.DB_DEVOPS_DB_NAME,user:process.env.DB_DEVOPS_DB_USER,password:process.env.DB_DEVOPS_DB_PASSWORD}).query('select 1').then(()=>console.log('OK')).catch(e=>console.log('FAIL',e.message))"

A classic footgun: the app pointing at the wrong Postgres (e.g. :5432 instead of a sidecar on a non-default port), or DB_DEVOPS_DB_PASSWORD not matching the DB role. In dev the password silently falls back to changeops with a warning; in production an unset password is fatal by design.

Reading the Health Signals

Two endpoints are the authoritative liveness/readiness signals and require no auth:

curl -s http://localhost:5002/api/health | python3 -m json.tool
curl -s http://localhost:5002/api/health/migrations | python3 -m json.tool

If /api/health is healthy but a specific table is missing, compare /api/health/migrations against the migration set — a partially applied migration (non-idempotent edit, or a manual DB change that collided with an IF NOT EXISTS guard) is the usual cause.

Migrations Did Not Apply

Symptom: relation "users" does not exist, relation "scripts" does not exist, or a column added by migration 010/011 (target_connection_id, environment) is missing.

Cause: Auto-migration on startup did not complete — the DB role lacks DDL privileges, the database did not exist when the pool first connected, or startup never left STARTING.

Resolution: Confirm the role can DDL (CREATE on the schema), ensure the changeops database exists before the app boots, then restart to re-run migrations. Migration files live at server/database/migrations/NNN_*.sql and are written idempotently (IF NOT EXISTS / ADD COLUMN IF NOT EXISTS); re-running is safe. Verify with /api/health/migrations afterward.

A Backend Route Returns 404 Through the Browser but Works via curl

Symptom: A feature 404s in the UI, yet curl http://localhost:5002/api/<route> against Express succeeds.

Cause: The browser never calls Express directly. The path is browser → pages/api/*.ts (Next.js) → http://localhost:5002/api/* (Express). Every backend route needs a matching Next.js proxy under pages/api/. A missing or misnamed proxy 404s at the Next.js layer before Express is ever reached.

Resolution: Confirm a proxy file exists for the route and that it forwards correctly. The shared helper forwardToBackend() (lib/apiProxy.ts) relays Authorization, X-Request-Id, and X-QA-Bypass-Key. If the proxy targets the wrong host, set BACKEND_URL (defaults to http://localhost:5002). Distinguish the layers by watching both process logs and matching the X-Request-Id across them.

License Activation Fails or Reverts

Symptom: "Invalid key format" or "Failed to validate license key" on activation; or a just-activated tier does not take effect for up to a minute.

Cause and resolution:

Secrets Are Being Stored in Plaintext

Symptom: Stored connection passwords and MFA secrets are readable as cleartext in the changeops DB; a warning about missing encryption appears once at startup.

Cause: ENCRYPTION_KEY is unset. EncryptionService then silently no-ops (fail-open) and persists sensitive fields unencrypted.

Detection: Encrypted values carry the wire-format prefix enc: (followed by base64 of salt‖iv‖tag‖ciphertext, AES-256-GCM, PBKDF2-SHA512 at 100k iterations). Any stored password/secret without the enc: prefix was written in the clear:

psql -U <user> -d changeops -c "select id, left(password,4) from connections where password not like 'enc:%';"

Resolution: Set a high-entropy ENCRYPTION_KEY before first write and never run production without it. Rotating in an existing deployment requires re-encrypting rows already written under the old key (or in the clear); changing the key does not retroactively re-wrap existing ciphertext.

Port Binding on Startup

Symptom: EADDRINUSE on :3000 or :5002; or Electron dev hangs waiting on the frontend.

Note the Electron dev inconsistency: electron:dev waits on port 3001 while next dev binds 3000. Either start Next.js on 3001 explicitly or point the wait-on URL at 3000 — otherwise Electron never launches. Reassign the backend with PORT and keep BACKEND_URL / NEXT_PUBLIC_API_URL in sync.