From fd1a80d0252649127022e3269463346567640ade Mon Sep 17 00:00:00 2001 From: yufoxda Date: Mon, 27 Jul 2026 22:34:25 +0900 Subject: [PATCH 1/2] fix(db): provision the runtime login without requiring SUPERUSER MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The provisioning runbook restated every role attribute in an ALTER ROLE after creating the role. Changing the SUPERUSER attribute requires SUPERUSER, which the operator role is not on a managed platform: on Supabase the whole runbook failed with "permission denied to alter role", so the dedicated login could never be created and the Workers had to keep connecting as postgres — the BYPASSRLS role the rest of this work exists to stop using. The ALTER now only sets the password. The attributes are already set by the CREATE above, and the branch that handles a pre-existing role verifies them before reaching this point, so nothing is weakened. Also renames the example role from app_runtime_login to app_runtime. In PostgreSQL a role with LOGIN is a login role, so the suffix said nothing, and app_runtime matches both the app_rls/app_api/app_auth naming already in the database and the "runtime role" wording the Workers use when they reject an unsafe login. Found while applying this to the production database, which now runs as app_runtime: not a superuser, no BYPASSRLS, inherits app_rls. Co-Authored-By: Claude Opus 4.8 --- .env.example | 4 ++-- README.md | 4 ++-- community/.dev.vars.example | 2 +- community/.env.example | 2 +- docs/hyperdrive-security.md | 2 +- member/.dev.vars.example | 2 +- member/.env.example | 2 +- supabase/runbooks/provision_app_runtime_login.sql | 6 +++++- 8 files changed, 14 insertions(+), 10 deletions(-) diff --git a/.env.example b/.env.example index 7a4e77f..cc0655a 100644 --- a/.env.example +++ b/.env.example @@ -5,7 +5,7 @@ # Global Settings NODE_ENV=development JWT_SECRET="your_common_jwt_secret_key" -DATABASE_URL="postgresql://app_runtime_login:password@host:port/database" +DATABASE_URL="postgresql://app_runtime:password@host:port/database" # Frontend Configuration (Next.js) FRONTEND_URL="http://localhost:3000" @@ -24,4 +24,4 @@ DISCORD_CLIENT_ID="your_discord_client_id" DISCORD_CLIENT_SECRET="your_discord_client_secret" # Wrangler Hyperdrive Local Connection Emulation String (Secure, ignored by git) -CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE="postgresql://app_runtime_login:your_password@your_postgres_host:5432/postgres" +CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE="postgresql://app_runtime:your_password@your_postgres_host:5432/postgres" diff --git a/README.md b/README.md index b0f4b24..d21df06 100644 --- a/README.md +++ b/README.md @@ -52,10 +52,10 @@ cp member/.dev.vars.example member/.dev.vars ローカル開発時(`wrangler dev`)は、Hyperdriveの接続先をWranglerプロセスの環境変数で上書きします。`.dev.vars` の通常のWorker bindingとは別なので、APIを起動する各シェルで設定してください。 ```bash -export CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE="postgresql://app_runtime_login:password@host:5432/database" +export CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE="postgresql://app_runtime:password@host:5432/database" ``` -`app_runtime_login` はDB運用runbookで作る非superuser・非`BYPASSRLS`の専用ロールです。アプリケーションから`postgres`/owner資格情報を使わないでください。 +`app_runtime` はDB運用runbookで作る非superuser・非`BYPASSRLS`の専用ロールです。アプリケーションから`postgres`/owner資格情報を使わないでください。 ### 3. 各プロジェクトのローカル起動 それぞれのディレクトリで開発サーバーを起動します。 diff --git a/community/.dev.vars.example b/community/.dev.vars.example index 6e444b8..4341cb5 100644 --- a/community/.dev.vars.example +++ b/community/.dev.vars.example @@ -1,5 +1,5 @@ # Database -DATABASE_URL=postgresql://app_runtime_login:password@host:port/dbname +DATABASE_URL=postgresql://app_runtime:password@host:port/dbname # Better Auth Configuration BETTER_AUTH_SECRET=your_secure_random_32_char_string diff --git a/community/.env.example b/community/.env.example index 37a188c..35655b4 100644 --- a/community/.env.example +++ b/community/.env.example @@ -1,5 +1,5 @@ # Database -DATABASE_URL=postgresql://app_runtime_login:password@host:port/dbname +DATABASE_URL=postgresql://app_runtime:password@host:port/dbname # Better Auth Configuration BETTER_AUTH_SECRET=your_secure_random_32_char_string diff --git a/docs/hyperdrive-security.md b/docs/hyperdrive-security.md index 53c04df..01d2b8c 100644 --- a/docs/hyperdrive-security.md +++ b/docs/hyperdrive-security.md @@ -104,7 +104,7 @@ connection override is a Wrangler **process environment variable**, not a normal Worker variable: ```bash -export CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE='postgresql://app_runtime_login:password@localhost:5432/postgres' +export CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE='postgresql://app_runtime:password@localhost:5432/postgres' ``` Do not copy the repository-wide `.env` into both Workers; that exposes secrets diff --git a/member/.dev.vars.example b/member/.dev.vars.example index 5b2ffd5..d2f4006 100644 --- a/member/.dev.vars.example +++ b/member/.dev.vars.example @@ -1,5 +1,5 @@ # Database -DATABASE_URL=postgresql://app_runtime_login:password@host:port/dbname +DATABASE_URL=postgresql://app_runtime:password@host:port/dbname # Application JWT verification (must match community JWT_SECRET) JWT_SECRET=your_application_jwt_secret diff --git a/member/.env.example b/member/.env.example index f2f79f9..307acb2 100644 --- a/member/.env.example +++ b/member/.env.example @@ -1,5 +1,5 @@ # Database -DATABASE_URL=postgresql://app_runtime_login:password@host:port/dbname +DATABASE_URL=postgresql://app_runtime:password@host:port/dbname # Application JWT verification (must match the community service JWT_SECRET; # it must be different from BETTER_AUTH_SECRET) diff --git a/supabase/runbooks/provision_app_runtime_login.sql b/supabase/runbooks/provision_app_runtime_login.sql index 9da64da..28640fb 100644 --- a/supabase/runbooks/provision_app_runtime_login.sql +++ b/supabase/runbooks/provision_app_runtime_login.sql @@ -75,8 +75,12 @@ begin raise exception 'runtime role % owns a database or database objects and cannot be safely constrained', runtime_role; end if; + -- Only the password is set here. Restating the attributes would require + -- SUPERUSER, which the operator role is not on a managed platform such as + -- Supabase, and it is unnecessary: the create above sets them, and the branch + -- for an existing role has already verified them. execute format( - 'alter role %I login inherit nosuperuser nocreatedb nocreaterole noreplication nobypassrls password %L', + 'alter role %I password %L', runtime_role, runtime_password ); From 2a02e46c6cd7282f2d0e90be31da5462be8aa697 Mon Sep 17 00:00:00 2001 From: yufoxda Date: Tue, 28 Jul 2026 16:45:43 +0900 Subject: [PATCH 2/2] fix(db): point the admin bootstrap at app_auth and list the required secrets Promoting the first admin still joined public.account, which the schema split moved to app_auth, so the runbook failed before it could grant the role. That was an oversight in the split, not a separate change. The deployment order also never said which secrets each Worker needs, and three separate outages during the production rollout came from that: member had no DISCORD_TOKEN or DISCORD_GUILD_ID and answered 500 on every application, and frontend had no JWT_SECRET, so its middleware threw on each request and put the site in a redirect loop between / and /login. Both are new requirements of this release that an existing deployment cannot have. Adds the per-Worker table, notes that a deploy can drop values that were only set through the dashboard, and records that DATABASE_URL should stay unset in production rather than keeping a credential that would bypass the dedicated runtime role. Co-Authored-By: Claude Opus 4.8 --- docs/hyperdrive-security.md | 37 +++++++++++++++++++-- supabase/runbooks/promote_initial_admin.sql | 3 +- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/docs/hyperdrive-security.md b/docs/hyperdrive-security.md index 01d2b8c..ea5500d 100644 --- a/docs/hyperdrive-security.md +++ b/docs/hyperdrive-security.md @@ -60,9 +60,40 @@ this sequence. Each result must show `caching.disabled: true`, and the origin user must be the dedicated login rather than `postgres`. -7. Deploy both Workers and exercise sign-in, Discord verification, application - submission, approval, and directory access. Restore writes only after these - checks pass. +7. Apply the schema split, `20260727000000_split_auth_schema.sql`. +8. Confirm every Worker has the secrets listed under "Required secrets" before + deploying. A missing one fails at runtime, not at deploy time. +9. Deploy all three Workers and exercise sign-in, Discord verification, + application submission, approval, and directory access. Restore writes only + after these checks pass. + +## Required secrets + +Check these with `npx wrangler secret list` in each directory. Deploying a +Worker whose secrets were only ever set through the dashboard can drop values +that are not also in `wrangler.jsonc`, so re-check after any deploy. + +| Secret | community | member | frontend | +| --- | --- | --- | --- | +| `JWT_SECRET` | signs the app token | verifies it | verifies it in middleware | +| `BETTER_AUTH_SECRET` | yes | — | — | +| `DISCORD_CLIENT_ID` / `DISCORD_CLIENT_SECRET` | yes | — | — | +| `DISCORD_TOKEN` / `DISCORD_GUILD_ID` | yes | yes | — | +| `COMMUNITY_URL` / `FRONTEND_URL` | yes | yes | — | +| `COOKIE_DOMAIN` | yes | — | — | + +`JWT_SECRET` must be identical in all three. The frontend needs it because +`middleware.ts` verifies the token to route admins; without it every request +throws, redirects to `/login`, and bounces back into a redirect loop. + +`member` needs `DISCORD_TOKEN` and `DISCORD_GUILD_ID` because it re-verifies +guild membership when an application is submitted and again when it is +approved. A deployment that predates that behaviour will not have them, and the +join endpoint answers 500 until they are set. + +`DATABASE_URL` is only a fallback for the Hyperdrive binding. Leave it unset in +production rather than holding credentials that would bypass the dedicated +runtime role if Hyperdrive were unavailable. ## Database verification diff --git a/supabase/runbooks/promote_initial_admin.sql b/supabase/runbooks/promote_initial_admin.sql index 8271f64..66fe4d6 100644 --- a/supabase/runbooks/promote_initial_admin.sql +++ b/supabase/runbooks/promote_initial_admin.sql @@ -22,7 +22,8 @@ begin from public.community_identities identity join public.community_memberships membership on membership.identity_id = identity.identity_id - join public.account auth_account + -- The authentication tables live in app_auth since the schema split. + join app_auth.account auth_account on auth_account.id = identity.auth_account_id where identity.provider = 'discord' and identity.provider_account_id = target_discord_account_id