#!/bin/bash # Runs once on first container start (docker-entrypoint-initdb.d). # Two cases: # 1. Fresh volume (e.g. prod on ambrosio): the supabase/postgres image's # initdb only creates the `postgres` role. Create all Supabase internal # roles here, then set their passwords. # 2. Pre-existing volume carried over from an older build that already has # the roles (e.g. long-lived dev): only ALTER their passwords. # # Idempotent: safe on both paths. set -e PASS="${POSTGRES_PASSWORD:-postgres}" # Connect as `postgres` (always exists post-initdb) on the Unix socket (trusted). psql -v ON_ERROR_STOP=1 --username postgres --dbname postgres <<-EOSQL -- ── Role bootstrap (idempotent) ─────────────────────────────────────── -- Template for most service roles: NOLOGIN OR LOGIN depending on use. DO \$\$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'supabase_admin') THEN CREATE ROLE supabase_admin SUPERUSER CREATEDB CREATEROLE REPLICATION BYPASSRLS LOGIN PASSWORD '${PASS}'; ELSE ALTER ROLE supabase_admin WITH PASSWORD '${PASS}'; END IF; IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'anon') THEN CREATE ROLE anon NOLOGIN NOINHERIT; END IF; IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'authenticated') THEN CREATE ROLE authenticated NOLOGIN NOINHERIT; END IF; IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'service_role') THEN CREATE ROLE service_role NOLOGIN NOINHERIT BYPASSRLS; END IF; IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'authenticator') THEN CREATE ROLE authenticator LOGIN NOINHERIT PASSWORD '${PASS}'; ELSE ALTER ROLE authenticator WITH PASSWORD '${PASS}'; END IF; -- authenticator must be able to SET ROLE to anon/authenticated/service_role. GRANT anon, authenticated, service_role TO authenticator; IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'supabase_auth_admin') THEN CREATE ROLE supabase_auth_admin LOGIN NOINHERIT CREATEROLE PASSWORD '${PASS}'; ELSE ALTER ROLE supabase_auth_admin WITH PASSWORD '${PASS}'; END IF; IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'supabase_storage_admin') THEN CREATE ROLE supabase_storage_admin LOGIN NOINHERIT CREATEROLE PASSWORD '${PASS}'; ELSE ALTER ROLE supabase_storage_admin WITH PASSWORD '${PASS}'; END IF; GRANT authenticator TO supabase_storage_admin; IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'supabase_replication_admin') THEN CREATE ROLE supabase_replication_admin LOGIN REPLICATION PASSWORD '${PASS}'; ELSE ALTER ROLE supabase_replication_admin WITH PASSWORD '${PASS}'; END IF; IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'supabase_realtime_admin') THEN CREATE ROLE supabase_realtime_admin NOLOGIN NOINHERIT; END IF; IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'pgbouncer') THEN CREATE ROLE pgbouncer LOGIN PASSWORD '${PASS}'; ELSE ALTER ROLE pgbouncer WITH PASSWORD '${PASS}'; END IF; IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'dashboard_user') THEN CREATE ROLE dashboard_user CREATEROLE CREATEDB REPLICATION; END IF; END \$\$; -- Grant postgres membership in service roles so seed inserts work. GRANT anon, authenticated, service_role, supabase_auth_admin, supabase_storage_admin, supabase_realtime_admin TO postgres; -- Per-role search_path + timeouts (match dev to avoid migration drift). -- GoTrue's migrations issue unqualified CREATE TYPE / CREATE TABLE expecting -- these to land in the `auth` schema — with no default here they end up in -- `public` and later migrations that reference `auth.factor_type` blow up. ALTER ROLE supabase_auth_admin SET search_path TO auth; ALTER ROLE supabase_auth_admin SET idle_in_transaction_session_timeout TO 60000; ALTER ROLE supabase_storage_admin SET search_path TO storage; ALTER ROLE anon SET statement_timeout TO '3s'; ALTER ROLE authenticated SET statement_timeout TO '8s'; ALTER ROLE authenticator SET statement_timeout TO '8s'; ALTER ROLE authenticator SET lock_timeout TO '8s'; -- ── Extensions the migrations assume are enabled ───────────────────── -- Migration 005 invokes cron.schedule(...); dev has pg_cron auto-enabled -- via the image's supa-init scripts, prod does not. CREATE EXTENSION IF NOT EXISTS pg_cron; CREATE EXTENSION IF NOT EXISTS pgcrypto; CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -- ── Schemas the Supabase services expect ───────────────────────────── CREATE SCHEMA IF NOT EXISTS auth AUTHORIZATION supabase_auth_admin; CREATE SCHEMA IF NOT EXISTS storage AUTHORIZATION supabase_storage_admin; CREATE SCHEMA IF NOT EXISTS graphql_public; CREATE SCHEMA IF NOT EXISTS _realtime; GRANT ALL ON SCHEMA _realtime TO supabase_replication_admin; ALTER ROLE supabase_replication_admin SET search_path TO _realtime; -- Pre-create the `realtime` schema so Realtime's per-tenant migrations -- don't fail on "schema realtime does not exist". CREATE SCHEMA IF NOT EXISTS realtime AUTHORIZATION supabase_admin; -- Empty publication that migration 007 will ALTER with specific tables. -- (supabase/postgres image ships this pre-created in newer versions; -- on v15.1.1.78 it does not.) CREATE PUBLICATION supabase_realtime; -- PostgREST needs USAGE on public for anon + authenticated + service_role. GRANT USAGE ON SCHEMA public TO anon, authenticated, service_role; -- Default privileges for future tables in public (RLS policies still apply). ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO anon, authenticated, service_role; ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO anon, authenticated, service_role; ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON FUNCTIONS TO anon, authenticated, service_role; -- GoTrue runs its own migrations against `public.schema_migrations`. PG 15 -- revokes CREATE from public by default, so grant it back to the service roles. GRANT CREATE, USAGE ON SCHEMA public TO supabase_auth_admin, supabase_storage_admin; -- Storage migrations call statements that require database-level grants -- (e.g. CREATE EXTENSION, ALTER DEFAULT PRIVILEGES). GRANT ALL ON DATABASE postgres TO supabase_auth_admin, supabase_storage_admin; -- ── Keycloak database + role (separate DB, idempotent) ─────────────── -- CREATE DATABASE cannot run in a transaction, so run it via \gexec. SELECT 'CREATE DATABASE keycloak' WHERE NOT EXISTS (SELECT 1 FROM pg_database WHERE datname = 'keycloak') \gexec DO \$\$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'keycloak') THEN CREATE ROLE keycloak LOGIN PASSWORD '${PASS}'; ELSE ALTER ROLE keycloak WITH PASSWORD '${PASS}'; END IF; END \$\$; GRANT ALL PRIVILEGES ON DATABASE keycloak TO keycloak; EOSQL # Keycloak also needs CREATE on the keycloak database's public schema. psql -v ON_ERROR_STOP=1 --username postgres --dbname keycloak <<-EOSQL GRANT ALL ON SCHEMA public TO keycloak; EOSQL echo "==> db-init: roles bootstrapped, passwords set, schemas created"