caterium-app/ops/supabase-selfhosted/frontend-cutover.md
pavlov346346 88b7f55db4 Ops: add self-hosted Supabase stack for Timeweb Cloud migration
Full production-ready docker-compose stack (db, kong, auth, rest,
realtime, storage, imgproxy, meta, functions, studio) targeting
api.caterium.ru, plus bootstrap script for a fresh Cloud Server,
Caddy reverse-proxy config (HTTPS, WebSocket, upload limits), and
dump/restore/verify/storage-sync scripts for moving off the managed
Supabase project (cksuehzcimitsxmeloes). Does not touch public/ or
any live runtime behavior — frontend cutover is documented separately
in frontend-cutover.md and only applied after Etap 8 verification.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 16:20:45 +03:00

104 lines
5.4 KiB
Markdown

# Frontend cutover (Etap 9-11) — apply only after Etap 8 counts match
Two changes to `public/app-runtime.js`, both required together. Do not commit either until
`scripts/03-verify-counts.sql` shows matching counts on old vs new (14/11/53/52/460/18) and
the Etap 10 smoke test has passed against `https://api.caterium.ru` directly (before touching
the frontend).
## 1. Point the default at the new backend
```js
// public/app-runtime.js, line ~1989-1990
const DEFAULT_SUPABASE_URL = 'https://api.caterium.ru';
const DEFAULT_SUPABASE_KEY = '<NEW_ANON_KEY_FROM_.env>';
```
## 2. Auto-migrate devices that already have the OLD url cached in `localStorage`
Every Caterium device has `sunCloudV2Config` (and possibly the legacy `sunEnterpriseSettingsV1`)
in `localStorage`, which `loadConfig()` reads *before* falling back to `DEFAULT_SUPABASE_URL`.
Just changing the default does nothing for those devices — they keep talking to
`cksuehzcimitsxmeloes.supabase.co` forever unless we rewrite the stored value once. Insert this
right after the existing `DEFAULT_SUPABASE_KEY` line, and call it from inside `loadConfig()`
before the `return { ... }`:
```js
const DEFAULT_SUPABASE_URL = 'https://api.caterium.ru';
const DEFAULT_SUPABASE_KEY = '<NEW_ANON_KEY_FROM_.env>';
const LEGACY_SUPABASE_URL = 'https://cksuehzcimitsxmeloes.supabase.co';
function migrateLegacySupabaseUrl(value) {
// One-time, silent migration: any device still pointed at the retired managed
// Supabase project is switched to the self-hosted backend automatically. Users
// are never asked to clear localStorage or reconfigure anything.
if (String(value.url || '').trim().replace(/\/$/, '') === LEGACY_SUPABASE_URL) {
value.url = DEFAULT_SUPABASE_URL;
value.key = DEFAULT_SUPABASE_KEY;
}
return value;
}
```
Then in `loadConfig()`, wrap the legacy-settings merge:
```js
function loadConfig() {
let value = {};
try { value = JSON.parse(localStorage.getItem(CONFIG_KEY) || '{}') || {}; } catch (_) {}
if (!value.url || !value.key) {
try {
const legacy = JSON.parse(localStorage.getItem('sunEnterpriseSettingsV1') || '{}') || {};
const cloud = legacy.cloud || {};
value.url ||= String(cloud.supabaseUrl || '').trim();
value.key ||= String(cloud.anonKey || '').trim();
} catch (_) {}
}
value = migrateLegacySupabaseUrl(value); // <-- add this line
return {
url: String(value.url || DEFAULT_SUPABASE_URL || '').trim().replace(/\/$/, ''),
key: String(value.key || DEFAULT_SUPABASE_KEY || '').trim(),
...
```
`saveConfig()` (a few lines below) persists `config` back to `localStorage` on every config
change already, so the corrected URL/key get written back to `sunCloudV2Config` the first time
`loadConfig()` runs on each device — no explicit persistence call needed here, but if a save
is not triggered naturally during your smoke test, call `saveConfig()` once right after
`config = loadConfig();` at module init (line ~1992) to force-persist the migration immediately
rather than waiting for the next unrelated write.
## 3. Session invalidation is expected
Because the new stack uses a freshly generated `JWT_SECRET` (README decision, since the old
one can't be safely retrieved), any cached Supabase auth session (JWT) in `localStorage`
becomes invalid the moment the client points at the new URL — `supabase-js` will see a 401 on
its first call, clear the stale session automatically, and the user lands back on the Caterium
login screen. That matches the task's accepted trade-off: users log in once more with their
existing email + password (unchanged, since `auth.users.encrypted_password` is copied
byte-for-byte). No code change needed for this part — it's `supabase-js`'s default behavior.
## Checklist before running this cutover
- [ ] `scripts/03-verify-counts.sql` matches old vs new (14/11/53/52/460/18)
- [ ] `curl https://api.caterium.ru/rest/v1/`, `/auth/v1/`, `/storage/v1/`, `/functions/v1/` all respond (not connection errors)
- [ ] Realtime WebSocket connects: `wscat -c wss://api.caterium.ru/realtime/v1/websocket?apikey=<ANON_KEY>`
- [ ] `scripts/05-storage-sync.sh` finished, spot-checked photo URLs load
- [ ] `scripts/04-deploy-edge-function.sh` shows `caterium-create-employee` rejecting an
unauthenticated call with 401/400 (not 5xx)
- [ ] Manual login test against `https://api.caterium.ru` directly (temporarily point a local
dev copy of `app-runtime.js` at it) with one real existing account, confirm orders/
clients/catalog load
Only once every box above is checked: apply the two edits above, commit to `main` with a
message like `Cutover: point Caterium at self-hosted Supabase (api.caterium.ru)`, push. CI
(`Caterium QA` -> `Caterium Promote Production`) auto-promotes `main` to `production` on green,
and the existing Timeweb cron (`ops/timeweb/caterium-production-sync.sh`) deploys it to
`app.caterium.ru` within 5 minutes. Do NOT skip CI (`--no-verify` or manual production-branch
pushes) for this change — the QA workflow's Playwright e2e run is exactly the safety net you
want on a backend cutover.
After confirming `https://app.caterium.ru` works end-to-end with no `supabase.co` requests in
DevTools Network, leave the old managed Supabase project (`cksuehzcimitsxmeloes`) running
untouched as a cold backup, per the task's explicit instruction — do not pause or delete it
without separate owner confirmation.