OMNI1 Superapp ยท since Sep 1, 2026

Two-Door Deploys

How we ship the Superapp now, why it changed, and the few rules that keep the portal alive. Written for everyone who deploys โ€” human or agent.

What happened on September 1

The customer portal answered 502 Bad Gateway for several hours, and three deploys in a row failed. The change being shipped was frontend-only โ€” renaming a tab, removing a popup. It was never at fault. The pipeline was:

  1. Every deploy ran the same ritual regardless of the change: full database dump โ†’ checkout โ†’ pip install โ†’ resync โ†’ backend restart โ†’ frontend build.
  2. The dump's runtime hung on one table: scout_competitors_v2, 1447 MB of 1909 MB โ€” raw scraping data, 76 % of the database. It blew past the GitHub Action's 40-minute limit.
  3. Cancelling the Action does not kill the server-side process. The dump kept running and held a read lock on all tables.
  4. Backend startup runs ~50 implicit ALTER TABLEs, which need exclusive locks โ€” and waited with no timeout behind the dump.
  5. Port 8000 was never bound โ†’ nginx answered 502. The next deploy started its own dump and queued into the same blockade. Found on the server: two stuck runs, one 1h21 old.

Four decisions that each sounded reasonable โ€” backup before every deploy, schema check at startup, one script for everything, no cleanup on abort โ€” only fail together. That's why nobody saw it coming.

The fix: two doors behind a router

A change pays only the price of its own risk.

Door A โ€” Frontend no DB, ~5 min

  • Runs when only frontend/ changed
  • Builds next to the live version (frontend/builds/<sha>/), never in place
  • Atomic symlink switch + restart of the Next.js service
  • Health check on page content, not just status codes
  • Fails? Symlink flips back automatically. Last 3 builds kept.
  • Never touches the database, runs a dump, or restarts the backend

Door B โ€” Backend dump + restart, โ‰ค40 min

  • Runs when tool/, deploy/ or requirements changed
  • Pre-deploy dump โ€” schema + all business tables; the scraping table is schema-only (24.7 MB instead of 556 MB). Hard 15-min timeout.
  • pip + resync each capped at 10 min; a resync failure is red, not a warning
  • Health check verifies the running git SHA and DB backend
  • Fails? Previous code is restored automatically. The schema does not roll back โ€” restoring the dump is a deliberate human decision.

The router (deploy.sh on the VPS) holds a lock so two deploys can never overlap, kills its own dump if a run is aborted, picks the door(s) by diffing what actually changed, and writes one audit line per run to /var/log/omni1-deploy.log โ€” who deployed what, when, through which door, with what result.

How you deploy

From your own clone, with your work merged and pushed to origin/master:

bash ops/deploy.sh --frontend   # Door A: frontend/ only
bash ops/deploy.sh --backend    # Door B: tool/, deploy/, requirements
bash ops/deploy.sh              # not sure? the router decides by diff

The script tags the commit, pushes the tag, and follows the GitHub run for that exact SHA until it finishes โ€” then checks the live site. If your local HEAD isn't origin/master, it refuses: you'd be shipping someone else's state.

Never do this

Each rule exists because it went wrong once:

What's still on the list

Schema migrations still run implicitly at backend startup, which is why a schema change is one-way. The planned next step moves them into an explicit tool/migrate.py with a lock timeout โ€” and moves scout_competitors_v2 out of the operational database entirely.