From abc72180bb3172cef7e01903174155aa90f1a78a Mon Sep 17 00:00:00 2001 From: Nikita Bragin Date: Sun, 8 Mar 2026 01:33:38 +0300 Subject: [PATCH 1/8] feat(db): add node-pg-migrate for reversible database migrations Replace static supabase/schema.sql with proper migration tooling: - Add node-pg-migrate with pg driver - Create initial migration with full up/down support - Add npm scripts: migrate:up, migrate:down, migrate:create, migrate:dry - Add DATABASE_URL to .env.example Co-Authored-By: Claude Opus 4.6 --- .env.example | 3 + .../1772922678763_initial-schema.sql | 32 +- package-lock.json | 528 ++++++++++++++++++ package.json | 8 +- 4 files changed, 558 insertions(+), 13 deletions(-) rename supabase/schema.sql => migrations/1772922678763_initial-schema.sql (82%) diff --git a/.env.example b/.env.example index 2311475..2711aba 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,5 @@ VITE_SUPABASE_URL= VITE_SUPABASE_PUBLISHABLE_KEY= + +# Database connection for migrations (Settings → Database in Supabase dashboard) +DATABASE_URL=postgresql://postgres.[project-ref]:[password]@aws-0-[region].pooler.supabase.com:6543/postgres diff --git a/supabase/schema.sql b/migrations/1772922678763_initial-schema.sql similarity index 82% rename from supabase/schema.sql rename to migrations/1772922678763_initial-schema.sql index d7491d8..4bfa09a 100644 --- a/supabase/schema.sql +++ b/migrations/1772922678763_initial-schema.sql @@ -1,8 +1,7 @@ --- Swarm Intelligence Platform - Supabase Schema --- Run this in the Supabase SQL Editor to set up the database +-- Up Migration --- 1. Profiles (auto-created on sign-up) -CREATE TABLE IF NOT EXISTS profiles ( +-- Profiles (auto-created on sign-up via trigger) +CREATE TABLE profiles ( id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE, display_name TEXT NOT NULL DEFAULT '', avatar_url TEXT, @@ -18,8 +17,8 @@ CREATE POLICY "Anyone can read profiles" CREATE POLICY "Users can update own profile" ON profiles FOR UPDATE USING (auth.uid() = id); --- 2. Saved simulations -CREATE TABLE IF NOT EXISTS saved_simulations ( +-- Saved simulations +CREATE TABLE saved_simulations ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE, title TEXT NOT NULL, @@ -51,10 +50,10 @@ CREATE POLICY "Users can delete own simulations" ON saved_simulations FOR DELETE USING (auth.uid() = user_id); -CREATE INDEX IF NOT EXISTS idx_saved_simulations_user ON saved_simulations(user_id); +CREATE INDEX idx_saved_simulations_user ON saved_simulations(user_id); --- 3. Shared links -CREATE TABLE IF NOT EXISTS shared_links ( +-- Shared links +CREATE TABLE shared_links ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), simulation_id UUID NOT NULL REFERENCES saved_simulations(id) ON DELETE CASCADE, share_token TEXT NOT NULL UNIQUE DEFAULT encode(gen_random_bytes(12), 'hex'), @@ -85,9 +84,9 @@ CREATE POLICY "Users can delete own shared links" ) ); -CREATE INDEX IF NOT EXISTS idx_shared_links_token ON shared_links(share_token); +CREATE INDEX idx_shared_links_token ON shared_links(share_token); --- 4. Auto-create profile on sign-up +-- Auto-create profile on sign-up CREATE OR REPLACE FUNCTION handle_new_user() RETURNS TRIGGER AS $$ BEGIN @@ -101,7 +100,16 @@ BEGIN END; $$ LANGUAGE plpgsql SECURITY DEFINER; -DROP TRIGGER IF EXISTS on_auth_user_created ON auth.users; CREATE TRIGGER on_auth_user_created AFTER INSERT ON auth.users FOR EACH ROW EXECUTE FUNCTION handle_new_user(); + +-- Down Migration + +DROP TRIGGER IF EXISTS on_auth_user_created ON auth.users; +DROP FUNCTION IF EXISTS handle_new_user(); +DROP INDEX IF EXISTS idx_shared_links_token; +DROP TABLE IF EXISTS shared_links; +DROP INDEX IF EXISTS idx_saved_simulations_user; +DROP TABLE IF EXISTS saved_simulations; +DROP TABLE IF EXISTS profiles; diff --git a/package-lock.json b/package-lock.json index 0293007..b328217 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,8 @@ "eslint": "^10.0.3", "husky": "^9.1.7", "lint-staged": "^16.3.2", + "node-pg-migrate": "^8.0.4", + "pg": "^8.20.0", "prettier": "^3.8.1", "typescript": "^5.9.3", "typescript-eslint": "^8.56.1", @@ -751,6 +753,16 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@isaacs/cliui": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", + "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", @@ -1893,6 +1905,110 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2120,6 +2236,16 @@ "@esbuild/win32-x64": "0.27.3" } }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -2415,6 +2541,23 @@ "dev": true, "license": "ISC" }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2440,6 +2583,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-east-asian-width": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", @@ -2453,6 +2606,31 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/glob": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -2679,6 +2857,22 @@ "dev": true, "license": "ISC" }, + "node_modules/jackspeak": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", + "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^9.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -2842,6 +3036,16 @@ "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, + "node_modules/lru-cache": { + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -2918,6 +3122,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -2951,6 +3165,32 @@ "dev": true, "license": "MIT" }, + "node_modules/node-pg-migrate": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/node-pg-migrate/-/node-pg-migrate-8.0.4.tgz", + "integrity": "sha512-HTlJ6fOT/2xHhAUtsqSN85PGMAqSbfGJNRwQF8+ZwQ1+sVGNUTl/ZGEshPsOI3yV22tPIyHXrKXr3S0JxeYLrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "~11.1.0", + "yargs": "~17.7.0" + }, + "bin": { + "node-pg-migrate": "bin/node-pg-migrate.js" + }, + "engines": { + "node": ">=20.11.0" + }, + "peerDependencies": { + "@types/pg": ">=6.0.0 <9.0.0", + "pg": ">=4.3.0 <9.0.0" + }, + "peerDependenciesMeta": { + "@types/pg": { + "optional": true + } + } + }, "node_modules/obug": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", @@ -3028,6 +3268,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -3055,6 +3302,23 @@ "dev": true, "license": "MIT" }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -3062,6 +3326,103 @@ "dev": true, "license": "MIT" }, + "node_modules/pg": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.12.0", + "pg-pool": "^3.13.0", + "pg-protocol": "^1.13.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.3.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz", + "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.12.0.tgz", + "integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz", + "integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz", + "integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -3158,6 +3519,49 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -3231,6 +3635,16 @@ "regexp-tree": "bin/regexp-tree" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", @@ -3421,6 +3835,16 @@ "node": ">=0.10.0" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -3980,6 +4404,26 @@ } } }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yaml": { "version": "2.8.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", @@ -3996,6 +4440,90 @@ "url": "https://github.com/sponsors/eemeli" } }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index fd1c370..130bd24 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,11 @@ "arch": "depcruise src --config .dependency-cruiser.cjs", "check": "npm run typecheck && npm run lint && npm run format:check && npm run arch", "e2e": "playwright test", - "e2e:ui": "playwright test --ui" + "e2e:ui": "playwright test --ui", + "migrate:up": "node-pg-migrate up -m migrations -j sql", + "migrate:down": "node-pg-migrate down -m migrations -j sql", + "migrate:create": "node-pg-migrate create -m migrations -j sql", + "migrate:dry": "node-pg-migrate up -m migrations -j sql --dry-run" }, "lint-staged": { "*.{ts,js}": [ @@ -47,6 +51,8 @@ "eslint": "^10.0.3", "husky": "^9.1.7", "lint-staged": "^16.3.2", + "node-pg-migrate": "^8.0.4", + "pg": "^8.20.0", "prettier": "^3.8.1", "typescript": "^5.9.3", "typescript-eslint": "^8.56.1", From 4db58d8970c728c985223a6a8fa13e9cc6f27c32 Mon Sep 17 00:00:00 2001 From: Nikita Bragin Date: Sun, 8 Mar 2026 01:36:17 +0300 Subject: [PATCH 2/8] ci: run database migrations automatically on merge to main Add migrate job to CI pipeline that runs after build+e2e pass. Only triggers on push to main (i.e. after PR merge). Requires DATABASE_URL secret in GitHub repo settings. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/ci.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c73106a..0c41224 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -105,3 +105,23 @@ jobs: name: playwright-report path: playwright-report/ retention-days: 7 + + migrate: + name: Run Migrations + runs-on: ubuntu-latest + needs: [build, e2e] + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - run: npm ci + + - name: Run pending migrations + run: npm run migrate:up + env: + DATABASE_URL: ${{ secrets.DATABASE_URL }} From 3aa3b5ffa62f86d635db8bd11c099692980f9cf4 Mon Sep 17 00:00:00 2001 From: Nikita Bragin Date: Sun, 8 Mar 2026 01:44:40 +0300 Subject: [PATCH 3/8] ci: add migration dry-run check on PRs Validates pending migrations against the live DB without executing them. Shows the SQL that would run, catching errors before merge. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/ci.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0c41224..42a77d5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -106,6 +106,25 @@ jobs: path: playwright-report/ retention-days: 7 + migrate-check: + name: Migration Dry Run + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - run: npm ci + + - name: Dry run pending migrations + run: npm run migrate:dry + env: + DATABASE_URL: ${{ secrets.DATABASE_URL }} + migrate: name: Run Migrations runs-on: ubuntu-latest From f72668cab161200320291ec16c621bbcdea85e6c Mon Sep 17 00:00:00 2001 From: Nikita Bragin Date: Sun, 8 Mar 2026 01:48:19 +0300 Subject: [PATCH 4/8] ci: test migrations against isolated Postgres container MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace dry-run against prod with a proper isolated test: - Spin up Postgres 16 service container - Create auth schema stubs (auth.users, auth.uid()) - Run migrate up → down → up to verify full reversibility - No connection to production database on PRs Co-Authored-By: Claude Opus 4.6 --- .github/sql/auth-stubs.sql | 19 +++++++++++++++++++ .github/workflows/ci.yml | 34 ++++++++++++++++++++++++++++------ 2 files changed, 47 insertions(+), 6 deletions(-) create mode 100644 .github/sql/auth-stubs.sql diff --git a/.github/sql/auth-stubs.sql b/.github/sql/auth-stubs.sql new file mode 100644 index 0000000..2fbb4e1 --- /dev/null +++ b/.github/sql/auth-stubs.sql @@ -0,0 +1,19 @@ +-- Stubs for Supabase auth schema used in CI migration testing. +-- Mimics the auth.users table and auth.uid() function so migrations +-- can be validated without a full Supabase instance. + +CREATE SCHEMA IF NOT EXISTS auth; + +CREATE TABLE auth.users ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + email TEXT, + raw_user_meta_data JSONB DEFAULT '{}'::jsonb, + created_at TIMESTAMPTZ DEFAULT now() +); + +CREATE OR REPLACE FUNCTION auth.uid() +RETURNS UUID AS $$ + SELECT COALESCE(current_setting('request.jwt.claim.sub', true)::uuid, '00000000-0000-0000-0000-000000000000'::uuid); +$$ LANGUAGE sql STABLE; + +CREATE EXTENSION IF NOT EXISTS pgcrypto; diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 42a77d5..3db8343 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -107,9 +107,24 @@ jobs: retention-days: 7 migrate-check: - name: Migration Dry Run + name: Migration Check runs-on: ubuntu-latest - if: github.event_name == 'pull_request' + services: + postgres: + image: postgres:16 + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: sim2d_test + ports: + - 5432:5432 + options: >- + --health-cmd="pg_isready -U postgres" + --health-interval=5s + --health-timeout=5s + --health-retries=5 + env: + DATABASE_URL: postgresql://postgres:postgres@localhost:5432/sim2d_test steps: - uses: actions/checkout@v4 @@ -120,10 +135,17 @@ jobs: - run: npm ci - - name: Dry run pending migrations - run: npm run migrate:dry - env: - DATABASE_URL: ${{ secrets.DATABASE_URL }} + - name: Create auth schema stubs + run: psql "$DATABASE_URL" -f .github/sql/auth-stubs.sql + + - name: Migrate up + run: npm run migrate:up + + - name: Migrate down + run: npm run migrate:down + + - name: Migrate up again (idempotency) + run: npm run migrate:up migrate: name: Run Migrations From ab901355d4ea1928a227507cc1d799b12e429a95 Mon Sep 17 00:00:00 2001 From: Nikita Bragin Date: Sun, 8 Mar 2026 01:51:58 +0300 Subject: [PATCH 5/8] ci: use full Supabase instance for migration testing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace Postgres + auth stubs with a real Supabase local instance: - Use supabase/setup-cli + supabase db start for full auth schema - Test up → down → up cycle to verify reversibility - Remove auth-stubs.sql (no longer needed) - migrate job on main now depends on migrate-check passing Co-Authored-By: Claude Opus 4.6 --- .github/sql/auth-stubs.sql | 19 ------------------- .github/workflows/ci.yml | 38 ++++++++++++++++++-------------------- 2 files changed, 18 insertions(+), 39 deletions(-) delete mode 100644 .github/sql/auth-stubs.sql diff --git a/.github/sql/auth-stubs.sql b/.github/sql/auth-stubs.sql deleted file mode 100644 index 2fbb4e1..0000000 --- a/.github/sql/auth-stubs.sql +++ /dev/null @@ -1,19 +0,0 @@ --- Stubs for Supabase auth schema used in CI migration testing. --- Mimics the auth.users table and auth.uid() function so migrations --- can be validated without a full Supabase instance. - -CREATE SCHEMA IF NOT EXISTS auth; - -CREATE TABLE auth.users ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - email TEXT, - raw_user_meta_data JSONB DEFAULT '{}'::jsonb, - created_at TIMESTAMPTZ DEFAULT now() -); - -CREATE OR REPLACE FUNCTION auth.uid() -RETURNS UUID AS $$ - SELECT COALESCE(current_setting('request.jwt.claim.sub', true)::uuid, '00000000-0000-0000-0000-000000000000'::uuid); -$$ LANGUAGE sql STABLE; - -CREATE EXTENSION IF NOT EXISTS pgcrypto; diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3db8343..caaffc0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -109,22 +109,6 @@ jobs: migrate-check: name: Migration Check runs-on: ubuntu-latest - services: - postgres: - image: postgres:16 - env: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_DB: sim2d_test - ports: - - 5432:5432 - options: >- - --health-cmd="pg_isready -U postgres" - --health-interval=5s - --health-timeout=5s - --health-retries=5 - env: - DATABASE_URL: postgresql://postgres:postgres@localhost:5432/sim2d_test steps: - uses: actions/checkout@v4 @@ -133,24 +117,38 @@ jobs: node-version: 20 cache: npm + - uses: supabase/setup-cli@v1 + with: + version: latest + - run: npm ci - - name: Create auth schema stubs - run: psql "$DATABASE_URL" -f .github/sql/auth-stubs.sql + - name: Start Supabase local database + run: supabase init --with-intellij-settings false && supabase db start + + - name: Get local DB URL + id: supabase + run: echo "db_url=$(supabase status -o env | grep DATABASE_URL | cut -d= -f2-)" >> "$GITHUB_OUTPUT" - name: Migrate up run: npm run migrate:up + env: + DATABASE_URL: ${{ steps.supabase.outputs.db_url }} - name: Migrate down run: npm run migrate:down + env: + DATABASE_URL: ${{ steps.supabase.outputs.db_url }} - - name: Migrate up again (idempotency) + - name: Migrate up again (verify reversibility) run: npm run migrate:up + env: + DATABASE_URL: ${{ steps.supabase.outputs.db_url }} migrate: name: Run Migrations runs-on: ubuntu-latest - needs: [build, e2e] + needs: [build, e2e, migrate-check] if: github.event_name == 'push' && github.ref == 'refs/heads/main' steps: - uses: actions/checkout@v4 From c0aaed448e527219c6b715a6513cb49aaca0fc48 Mon Sep 17 00:00:00 2001 From: Nikita Bragin Date: Sun, 8 Mar 2026 01:56:29 +0300 Subject: [PATCH 6/8] ci: fix migration check with supabase start, split deploy workflow - Use supabase start (full stack) instead of db start for auth schema - Hardcode known local DB URL instead of parsing supabase status - Move prod migration deploy to separate workflow (triggers on migrations/** changes) - Remove "Run Migrations" job from CI to clean up PR graph Co-Authored-By: Claude Opus 4.6 --- .github/workflows/ci.yml | 90 +++++++++---------------- .github/workflows/deploy-migrations.yml | 26 +++++++ 2 files changed, 57 insertions(+), 59 deletions(-) create mode 100644 .github/workflows/deploy-migrations.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index caaffc0..d39e8fe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,6 +48,37 @@ jobs: - name: Unit & Integration Tests run: npm test -- --reporter=verbose + migrate-check: + name: Migration Check + runs-on: ubuntu-latest + env: + DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:54322/postgres + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - uses: supabase/setup-cli@v1 + with: + version: latest + + - run: npm ci + + - name: Start Supabase + run: supabase init --with-intellij-settings false && supabase start + + - name: Migrate up + run: npm run migrate:up + + - name: Migrate down + run: npm run migrate:down + + - name: Migrate up again (verify reversibility) + run: npm run migrate:up + build: name: Build runs-on: ubuntu-latest @@ -105,62 +136,3 @@ jobs: name: playwright-report path: playwright-report/ retention-days: 7 - - migrate-check: - name: Migration Check - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: npm - - - uses: supabase/setup-cli@v1 - with: - version: latest - - - run: npm ci - - - name: Start Supabase local database - run: supabase init --with-intellij-settings false && supabase db start - - - name: Get local DB URL - id: supabase - run: echo "db_url=$(supabase status -o env | grep DATABASE_URL | cut -d= -f2-)" >> "$GITHUB_OUTPUT" - - - name: Migrate up - run: npm run migrate:up - env: - DATABASE_URL: ${{ steps.supabase.outputs.db_url }} - - - name: Migrate down - run: npm run migrate:down - env: - DATABASE_URL: ${{ steps.supabase.outputs.db_url }} - - - name: Migrate up again (verify reversibility) - run: npm run migrate:up - env: - DATABASE_URL: ${{ steps.supabase.outputs.db_url }} - - migrate: - name: Run Migrations - runs-on: ubuntu-latest - needs: [build, e2e, migrate-check] - if: github.event_name == 'push' && github.ref == 'refs/heads/main' - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: npm - - - run: npm ci - - - name: Run pending migrations - run: npm run migrate:up - env: - DATABASE_URL: ${{ secrets.DATABASE_URL }} diff --git a/.github/workflows/deploy-migrations.yml b/.github/workflows/deploy-migrations.yml new file mode 100644 index 0000000..29aaf5a --- /dev/null +++ b/.github/workflows/deploy-migrations.yml @@ -0,0 +1,26 @@ +name: Deploy Migrations + +on: + push: + branches: [main] + paths: + - 'migrations/**' + +jobs: + migrate: + name: Run Migrations + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - run: npm ci + + - name: Apply pending migrations + run: npm run migrate:up + env: + DATABASE_URL: ${{ secrets.DATABASE_URL }} From f96b4ea9dd148dc5a67c22741b787d6ad525bdd9 Mon Sep 17 00:00:00 2001 From: Nikita Bragin Date: Sun, 8 Mar 2026 02:00:55 +0300 Subject: [PATCH 7/8] fix(db): enable pgcrypto extension for gen_random_bytes Required by shared_links.share_token default value. Co-Authored-By: Claude Opus 4.6 --- migrations/1772922678763_initial-schema.sql | 2 ++ 1 file changed, 2 insertions(+) diff --git a/migrations/1772922678763_initial-schema.sql b/migrations/1772922678763_initial-schema.sql index 4bfa09a..bdc08d5 100644 --- a/migrations/1772922678763_initial-schema.sql +++ b/migrations/1772922678763_initial-schema.sql @@ -1,5 +1,7 @@ -- Up Migration +CREATE EXTENSION IF NOT EXISTS pgcrypto; + -- Profiles (auto-created on sign-up via trigger) CREATE TABLE profiles ( id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE, From d0f1dd1209e52fc33d652f7069314aa1bd4fb7fc Mon Sep 17 00:00:00 2001 From: Nikita Bragin Date: Sun, 8 Mar 2026 02:05:13 +0300 Subject: [PATCH 8/8] fix(db): replace gen_random_bytes with gen_random_uuid for share tokens gen_random_bytes requires pgcrypto which isn't always available. gen_random_uuid() is built into Postgres 13+ and produces equivalent tokens. Co-Authored-By: Claude Opus 4.6 --- migrations/1772922678763_initial-schema.sql | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/migrations/1772922678763_initial-schema.sql b/migrations/1772922678763_initial-schema.sql index bdc08d5..f4a5eb5 100644 --- a/migrations/1772922678763_initial-schema.sql +++ b/migrations/1772922678763_initial-schema.sql @@ -1,7 +1,5 @@ -- Up Migration -CREATE EXTENSION IF NOT EXISTS pgcrypto; - -- Profiles (auto-created on sign-up via trigger) CREATE TABLE profiles ( id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE, @@ -58,7 +56,7 @@ CREATE INDEX idx_saved_simulations_user ON saved_simulations(user_id); CREATE TABLE shared_links ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), simulation_id UUID NOT NULL REFERENCES saved_simulations(id) ON DELETE CASCADE, - share_token TEXT NOT NULL UNIQUE DEFAULT encode(gen_random_bytes(12), 'hex'), + share_token TEXT NOT NULL UNIQUE DEFAULT replace(gen_random_uuid()::text, '-', ''), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), expires_at TIMESTAMPTZ );