1- import { Data , Effect , Redacted , Schema } from "effect" ;
1+ import { Data , Effect , Redacted } from "effect" ;
22
33/** A small runtime-neutral SQL boundary used by the internal database bootstrap. */
44export type DatabaseSqlValue = string | number | boolean | null ;
55
6- interface DatabaseRow {
7- readonly [ column : string ] : unknown ;
8- }
9-
106/** Login roles provisioned by the managed database template. */
11- type DatabaseBootstrapRole =
12- | "postgres"
13- | "authenticator"
14- | "pgbouncer"
15- | "supabase_auth_admin"
16- | "supabase_storage_admin"
17- | "supabase_replication_admin"
18- | "supabase_read_only_user" ;
19- const DATABASE_BOOTSTRAP_ROLES : ReadonlyArray < DatabaseBootstrapRole > = [
7+ const DATABASE_BOOTSTRAP_ROLES = [
208 "postgres" ,
219 "authenticator" ,
2210 "pgbouncer" ,
2311 "supabase_auth_admin" ,
2412 "supabase_storage_admin" ,
2513 "supabase_replication_admin" ,
2614 "supabase_read_only_user" ,
27- ] ;
15+ ] as const ;
16+ type DatabaseBootstrapRole = ( typeof DATABASE_BOOTSTRAP_ROLES ) [ number ] ;
2817
2918export type DatabaseBootstrapSetting =
3019 | {
@@ -36,28 +25,17 @@ export type DatabaseBootstrapSetting =
3625 readonly value : number ;
3726 } ;
3827
39- /** Values come from resolved managed secret slots and never become SQL text. */
40- export interface DatabaseBootstrapCredentials {
41- readonly roles ?: Readonly < Partial < Record < DatabaseBootstrapRole , Redacted . Redacted < string > > > > ;
42- }
43-
44- interface DatabaseBootstrapSettings {
28+ export interface DatabaseBootstrapOptions {
29+ /** One managed password shared by the closed login roles. */
30+ readonly databasePassword : Redacted . Redacted < string > ;
31+ /** Managed JWT material applied to the database settings on each invocation. */
4532 readonly jwtSecret : Redacted . Redacted < string > ;
4633 readonly jwtExpiry : number ;
4734}
4835
49- export interface DatabaseBootstrapOptions {
50- /** Ordered plan resolved for the pinned database release. */
51- readonly revisions : ReadonlyArray < DatabaseBootstrapRevision > ;
52- readonly credentials ?: DatabaseBootstrapCredentials ;
53- /** Configuration values are reconciled on every invocation, like role passwords. */
54- readonly settings ?: DatabaseBootstrapSettings ;
55- }
56-
5736export class DatabaseBootstrapError extends Data . TaggedError ( "DatabaseBootstrapError" ) < {
5837 readonly message : string ;
5938 readonly statement ?: string ;
60- readonly revision ?: string ;
6139 /** Whether retrying the database operation may succeed once the server settles. */
6240 readonly retryable ?: boolean ;
6341 readonly cause ?: unknown ;
@@ -80,10 +58,6 @@ export interface DatabaseTransaction {
8058 readonly setDatabaseSetting : (
8159 setting : DatabaseBootstrapSetting ,
8260 ) => Effect . Effect < void , DatabaseBootstrapError > ;
83- readonly query : (
84- statement : string ,
85- parameters ?: ReadonlyArray < DatabaseSqlValue > ,
86- ) => Effect . Effect < ReadonlyArray < DatabaseRow > , DatabaseBootstrapError > ;
8761}
8862
8963export interface DatabaseSession {
@@ -97,157 +71,69 @@ export interface DatabaseSession {
9771 ) => Effect . Effect < void , DatabaseBootstrapError > ;
9872}
9973
100- interface DatabaseBootstrapRevision {
101- readonly id : string ;
102- readonly statement : string ;
103- }
104-
105- const TRACKING_SCHEMA = "supabase_internal" ;
106- const TRACKING_TABLE = `${ TRACKING_SCHEMA } .bootstrap_revisions` ;
107-
108- const TRACKING_SCHEMA_STATEMENT = `CREATE SCHEMA IF NOT EXISTS ${ TRACKING_SCHEMA } ;` ;
109- const TRACKING_TABLE_STATEMENT = `CREATE TABLE IF NOT EXISTS ${ TRACKING_TABLE } (
110- revision text PRIMARY KEY,
111- applied_at timestamptz NOT NULL DEFAULT now()
112- );` ;
113- const APPLIED_REVISIONS_STATEMENT = `
114- SELECT revision FROM ${ TRACKING_TABLE } ORDER BY revision;
115- ` ;
116- const RECORD_REVISION_STATEMENT = `
117- INSERT INTO ${ TRACKING_TABLE } (revision) VALUES ($1)
118- ON CONFLICT (revision) DO NOTHING;
119- ` ;
74+ const REALTIME_SCHEMA_STATEMENT =
75+ "CREATE SCHEMA IF NOT EXISTS _realtime;\nALTER SCHEMA _realtime OWNER TO postgres;" ;
12076const ADVISORY_LOCK_STATEMENT = `SELECT pg_advisory_xact_lock(hashtext('supabase_internal.bootstrap'));` ;
12177
122- const RevisionRowSchema = Schema . Struct ( { revision : Schema . String } ) ;
123-
124- const statementError = ( error : DatabaseBootstrapError , statement : string , revision ?: string ) =>
78+ const statementError = ( error : DatabaseBootstrapError , statement : string ) =>
12579 new DatabaseBootstrapError ( {
12680 message : error . message ,
12781 statement,
128- ...( revision === undefined ? { } : { revision } ) ,
12982 ...( error . retryable === undefined ? { } : { retryable : error . retryable } ) ,
13083 ...( error . cause === undefined ? { } : { cause : error . cause } ) ,
13184 } ) ;
13285
133- /** Runs all unapplied internal revisions, recording each only after it succeeds . */
86+ /** Reconciles the runtime-owned schema, roles, and settings in one transaction . */
13487export const runDatabaseBootstrap = (
13588 session : DatabaseSession ,
13689 options : DatabaseBootstrapOptions ,
13790) : Effect . Effect < void , DatabaseBootstrapError > =>
138- Effect . gen ( function * ( ) {
139- const ids = new Set < string > ( ) ;
140- for ( const revision of options . revisions ) {
141- if ( revision . id . trim ( ) . length === 0 || ids . has ( revision . id ) )
142- return yield * new DatabaseBootstrapError ( {
143- message : "Database bootstrap revision ids must be non-empty and unique" ,
144- revision : revision . id ,
145- } ) ;
146- ids . add ( revision . id ) ;
147- }
148- yield * session . transaction ( ( transaction ) =>
149- Effect . gen ( function * ( ) {
150- yield * transaction
151- . execute ( ADVISORY_LOCK_STATEMENT )
152- . pipe ( Effect . mapError ( ( error ) => statementError ( error , ADVISORY_LOCK_STATEMENT ) ) ) ;
153- yield * transaction
154- . execute ( TRACKING_SCHEMA_STATEMENT )
155- . pipe ( Effect . mapError ( ( error ) => statementError ( error , TRACKING_SCHEMA_STATEMENT ) ) ) ;
156- yield * transaction
157- . execute ( TRACKING_TABLE_STATEMENT )
158- . pipe ( Effect . mapError ( ( error ) => statementError ( error , TRACKING_TABLE_STATEMENT ) ) ) ;
159- } ) ,
160- ) ;
161- for ( const revision of options . revisions ) {
162- yield * session . transaction ( ( transaction ) =>
163- Effect . gen ( function * ( ) {
164- // Re-check under a transaction-scoped advisory lock. This prevents
165- // two owners from both applying a non-idempotent revision after
166- // observing the same pre-lock snapshot.
167- yield * transaction
168- . execute ( ADVISORY_LOCK_STATEMENT )
169- . pipe ( Effect . mapError ( ( error ) => statementError ( error , ADVISORY_LOCK_STATEMENT ) ) ) ;
170- const rows = yield * transaction
171- . query ( APPLIED_REVISIONS_STATEMENT )
172- . pipe ( Effect . mapError ( ( error ) => statementError ( error , APPLIED_REVISIONS_STATEMENT ) ) ) ;
173- const applied = new Set < string > ( ) ;
174- for ( const row of rows ) {
175- const decoded = yield * Schema . decodeUnknownEffect ( RevisionRowSchema ) ( row ) . pipe (
176- Effect . mapError (
177- ( cause ) =>
178- new DatabaseBootstrapError ( {
179- message : `Bootstrap tracking row is malformed: ${ String ( cause ) } ` ,
180- statement : APPLIED_REVISIONS_STATEMENT ,
181- } ) ,
182- ) ,
183- ) ;
184- applied . add ( decoded . revision ) ;
185- }
186- if ( applied . has ( revision . id ) ) return ;
187- yield * transaction
188- . execute ( revision . statement )
189- . pipe (
190- Effect . mapError ( ( error ) => statementError ( error , revision . statement , revision . id ) ) ,
191- ) ;
192- yield * transaction
193- . execute ( RECORD_REVISION_STATEMENT , [ revision . id ] )
194- . pipe (
195- Effect . mapError ( ( error ) =>
196- statementError ( error , RECORD_REVISION_STATEMENT , revision . id ) ,
197- ) ,
198- ) ;
199- } ) ,
200- ) ;
201- }
202- if ( options . credentials ?. roles !== undefined || options . settings !== undefined ) {
203- yield * session . transaction ( ( transaction ) =>
204- Effect . gen ( function * ( ) {
205- yield * transaction
206- . execute ( ADVISORY_LOCK_STATEMENT )
207- . pipe ( Effect . mapError ( ( error ) => statementError ( error , ADVISORY_LOCK_STATEMENT ) ) ) ;
208- if ( options . credentials ?. roles !== undefined ) {
209- for ( const role of DATABASE_BOOTSTRAP_ROLES ) {
210- const password = options . credentials . roles [ role ] ;
211- if ( password === undefined ) continue ;
212- yield * transaction . setRolePassword ( role , password ) . pipe (
213- Effect . mapError (
214- ( ) =>
215- new DatabaseBootstrapError ( {
216- message : `Unable to configure internal database role ${ role } ` ,
217- } ) ,
218- ) ,
219- ) ;
220- }
221- }
222- if ( options . settings !== undefined ) {
223- yield * transaction
224- . setDatabaseSetting ( {
225- name : "app.settings.jwt_secret" ,
226- value : options . settings . jwtSecret ,
227- } )
228- . pipe (
229- Effect . mapError (
230- ( ) =>
231- new DatabaseBootstrapError ( {
232- message : "Unable to configure database JWT secret" ,
233- } ) ,
234- ) ,
235- ) ;
236- yield * transaction
237- . setDatabaseSetting ( {
238- name : "app.settings.jwt_exp" ,
239- value : options . settings . jwtExpiry ,
240- } )
241- . pipe (
242- Effect . mapError (
243- ( ) =>
244- new DatabaseBootstrapError ( {
245- message : "Unable to configure database JWT expiry" ,
246- } ) ,
247- ) ,
248- ) ;
249- }
250- } ) ,
251- ) ;
252- }
253- } ) ;
91+ session . transaction ( ( transaction ) =>
92+ Effect . gen ( function * ( ) {
93+ yield * transaction
94+ . execute ( ADVISORY_LOCK_STATEMENT )
95+ . pipe ( Effect . mapError ( ( error ) => statementError ( error , ADVISORY_LOCK_STATEMENT ) ) ) ;
96+ yield * transaction
97+ . execute ( REALTIME_SCHEMA_STATEMENT )
98+ . pipe ( Effect . mapError ( ( error ) => statementError ( error , REALTIME_SCHEMA_STATEMENT ) ) ) ;
99+ for ( const role of DATABASE_BOOTSTRAP_ROLES ) {
100+ yield * transaction . setRolePassword ( role , options . databasePassword ) . pipe (
101+ Effect . mapError (
102+ ( error ) =>
103+ new DatabaseBootstrapError ( {
104+ message : `Unable to configure internal database role ${ role } ` ,
105+ ...( error . retryable === undefined ? { } : { retryable : error . retryable } ) ,
106+ } ) ,
107+ ) ,
108+ ) ;
109+ }
110+ yield * transaction
111+ . setDatabaseSetting ( {
112+ name : "app.settings.jwt_secret" ,
113+ value : options . jwtSecret ,
114+ } )
115+ . pipe (
116+ Effect . mapError (
117+ ( error ) =>
118+ new DatabaseBootstrapError ( {
119+ message : "Unable to configure database JWT secret" ,
120+ ...( error . retryable === undefined ? { } : { retryable : error . retryable } ) ,
121+ } ) ,
122+ ) ,
123+ ) ;
124+ yield * transaction
125+ . setDatabaseSetting ( {
126+ name : "app.settings.jwt_exp" ,
127+ value : options . jwtExpiry ,
128+ } )
129+ . pipe (
130+ Effect . mapError (
131+ ( error ) =>
132+ new DatabaseBootstrapError ( {
133+ message : "Unable to configure database JWT expiry" ,
134+ ...( error . retryable === undefined ? { } : { retryable : error . retryable } ) ,
135+ } ) ,
136+ ) ,
137+ ) ;
138+ } ) ,
139+ ) ;
0 commit comments