Stratify
Engineering

Hetzner Cutover

docs/runbooks/hetzner-cutover.md

Source updated 03. Aug. 2026

Hetzner cutover runbook — Stratify on app.stratifyinvest.com

Status: COMPLETED (2026-06-03). Stratify is live on Hetzner (128.140.8.187, Docker + Traefik) at https://app.stratifyinvest.com. Vercel is deactivated. This runbook is retained for reference + rollback; the steps below were executed, not pending.

Moved Stratify from Vercel + Supabase Cloud to coolify-main (128.140.8.187). Did not migrate Loki or Ecosystem Studio (already on Hetzner under other paths).

Pre-flight (Hetzner ready)

  • [ ] stratify-* containers healthy (docker ps | grep stratify)
  • [ ] Traefik stratify.yaml installed (not stratify-staging.yaml)
  • [ ] Smoke via IP + Host header:
  curl -sS -o /dev/null -w '%{http_code}\n' -H 'Host: app.stratifyinvest.com' \
    https://128.140.8.187/api/health
  curl -sS -o /dev/null -w '%{http_code}\n' -H 'Host: app.stratifyinvest.com' \
    https://128.140.8.187/supabase/rest/v1/
  • [ ] Demo login works (after cutover): [email protected] / env DEMO_PASSWORD
  • [ ] ./scripts/hetzner-backup-postgres.sh tested once

Isolation verified

CheckCommand
No Loki port clash`ss -lntp \grep 5433` on server — only stratify
Traefik pathsgrep stratify /data/coolify/proxy/dynamic/stratify.yaml — no ecosystem
Compose projectdocker compose -p stratify-supabase ps — not supabase (Loki)

Cloudflare cutover (Toby — requires valid API token)

  1. Maintenance window — brief 502 acceptable if DNS TTL low.
  2. Run: ./scripts/hetzner-cloudflare-app-dns.sh
  • Sets app.stratifyinvest.com A → 128.140.8.187 (proxied default).
  1. No Zero Trust application covers app.stratifyinvest.com. The edge gate that

used to sit on the ventures host is gone; /ops and /pilot rely on the app's own auth. Add one if you want it back — docs/runbooks/cloudflare-access.md.

Rollback: Point the app A record back to Vercel IPs / re-enable Vercel DNS.

Post-cutover dashboard updates

SystemAction
StripeWebhook URL stays https://app.stratifyinvest.com/api/webhooks/stripe — re-send test event
Supabase CloudDeprecated for Stratify after data migrate — do not run config push to cloud project for Hetzner auth
Hetzner Supabase .envSITE_URL = https://app.stratifyinvest.com, API_EXTERNAL_URL = https://app.stratifyinvest.com/supabase (via hetzner-update-public-urls.sh)
Mobile (fastlane)EXPO_PUBLIC_* now point at app.stratifyinvest.com; build 7 still holds the old Supabase URL, so build 8 must ship before the legacy ventures router is removed — apps/mobile/scripts/testflight-local.sh (EAS is dead; see docs/runbooks/mobile-release.md)
Crons/etc/cron.d/stratify via ./scripts/hetzner-install-crons.sh (source: infra/hetzner/cron-stratify.example); auth CRON_SECRET

Production data from Supabase Cloud

Destructive on Hetzner `stratify-db` only — requires explicit confirm env:

STRATIFY_CONFIRM_CLOUD_RESTORE=yes \
  ./scripts/with-stratify-hetzner-supabase-db.sh ./scripts/hetzner-restore-from-cloud.sh

Guards: linked project ddkpedfjdzdcmgwuxlpb, tunnel to 127.0.0.1:5433, auto backup first. Does not touch Loki (supabase-db / port 5432) or Ecosystem stacks.

E2E checklist (after cutover)

  • [ ] /sign-in — request a 6-digit code, enter it, land on /feed
  • [ ] /feed — authenticated
  • [ ] /ops — admin role
  • [ ] Partner console — demo partner
  • [ ] POST /api/webhooks/stripe — Stripe test event
  • [ ] Cron endpoints with CRON_SECRET

DSGVO / ops

  • Backups: nightly hetzner-backup-postgres.sh/data/stratify/backups
  • EU host: Hetzner nbg1 — document in docs/compliance/dsgvo-data-flow.md when cutover is permanent
  • Audit retention: unchanged (chain-hashed, 2y pre-license)

Server power schedule

See `docs/runbooks/hetzner-power-schedule.md` — recommended ON 04:45 / OFF 23:30 Europe/Zurich if you enable overnight shutdown (shared with Ecosystem 03:00 UTC jobs).

Crons (Hetzner)

./scripts/hetzner-install-crons.sh

Logs: /var/log/stratify-cron.log. Vercel crons removed from apps/web/vercel.json (Hetzner-only).