diff --git a/frontend/src/lib/components/workspaceSettings/AddDataTableWizard.svelte b/frontend/src/lib/components/workspaceSettings/AddDataTableWizard.svelte
index 3c98eca230..efccd554d3 100644
--- a/frontend/src/lib/components/workspaceSettings/AddDataTableWizard.svelte
+++ b/frontend/src/lib/components/workspaceSettings/AddDataTableWizard.svelte
@@ -31,8 +31,7 @@
+
+
+
+{#if !oauth.authed}
+
+ {#if oauth.pending}
+ Sign in and approve Windmill in the Supabase window, then come back here.
+ {:else}
+ Windmill needs your approval on Supabase to see your databases.
+ {/if}
+
+{:else if loading}
+
+ This usually takes a minute or two. You can leave this open.{provisionStatus
+ ? ` (${provisionStatus})`
+ : ''}
+
+ {/if}
+ {/if}
+ {:else}
+
+ {#each projects ?? [] as p (p.id)}
+ {@const selected = selectedProject?.id === p.id}
+
+ {/each}
+
+ {#if selectedProject}
+
+ Database password
+
+
+ Find it in your Supabase project settings, under Database.
+
+
+ {/if}
+ {/if}
+
+ {#if strandedPassword}
+
+ {strandedPassword}
+
+ The project exists but Windmill could not finish. Supabase never shows this password again.
+
+ {/if}
+{/if}
diff --git a/frontend/src/lib/components/workspaceSettings/instanceDbSteps.ts b/frontend/src/lib/components/workspaceSettings/instanceDbSteps.ts
new file mode 100644
index 0000000000..dd465626f4
--- /dev/null
+++ b/frontend/src/lib/components/workspaceSettings/instanceDbSteps.ts
@@ -0,0 +1,89 @@
+import type { CustomInstanceDb } from '$lib/gen'
+import { runningFrom, type SetupStep } from '../wizards/SetupChecklist.svelte'
+
+/**
+ * The same checks as [`instanceDbSteps`], in the vocabulary the wizard's checklist speaks.
+ * Nothing is reported until the call returns, so an unreported step is either the failure
+ * (when the call errored) or simply not reached yet.
+ */
+export function instanceSetupSteps(
+ dbname: string,
+ status: CustomInstanceDb | undefined,
+ running: boolean
+): SetupStep[] {
+ let firstUnreported = true
+ const steps = instanceDbSteps(dbname, status).map((step): SetupStep => {
+ if (step.status === 'OK') return { ...step, status: 'done' }
+ if (step.status === 'FAIL') return { ...step, status: 'failed' }
+ if (step.status === 'SKIP') return { ...step, status: 'skipped' }
+ const failed = firstUnreported && !!status?.error
+ firstUnreported = false
+ return { ...step, status: failed ? 'failed' : 'pending' }
+ })
+ return runningFrom(steps, running)
+}
+
+/**
+ * The checks `setup_custom_instance_db` reports, in the order it runs them. Shared so the
+ * setup modal and the data table wizard describe the same failure the same way.
+ */
+export function instanceDbSteps(dbname: string, status: CustomInstanceDb | undefined) {
+ return [
+ {
+ title: 'Super admin required',
+ status: status?.logs.super_admin,
+ description:
+ 'You need to be a super admin to create a new database in the Windmill PostgreSQL instance'
+ },
+ {
+ title: 'Retrieve and parse database credentials',
+ status: status?.logs.database_credentials,
+ description:
+ 'Windmill uses the DATABASE_URL or DATABASE_URL_FILE environment variable to connect to the PostgreSQL instance. Make sure it is correctly set'
+ },
+ {
+ title: 'Database name is valid',
+ status: status?.logs.valid_dbname,
+ description:
+ 'The database name must be alphanumeric (underscores and hyphens allowed) and cannot be named the same as the Windmill database (usually "windmill")'
+ },
+ {
+ title:
+ 'Create database' +
+ (status?.logs.created_database === 'SKIP' ? ' (already exists, skipped)' : ''),
+ status: status?.logs.created_database,
+ description: `In the Windmill PostgreSQL instance, run: CREATE DATABASE "${dbname}".`
+ },
+ {
+ title: `Connect to the ${dbname} database`,
+ status: status?.logs.db_connect,
+ description:
+ "Connect to the newly created database with the default admin user (the one in DATABASE_URL, usually 'postgres') to run the next commands"
+ },
+ {
+ title: 'Grant permissions to custom_instance_user',
+ status: status?.logs.grant_permissions,
+ description:
+ 'Gives custom_instance_user the required permissions to use the database. custom_instance_user is already created during a migration and has an auto-generated password stored in global_settings.custom_instance_pg_databases.user_pwd. These are the commands : \n\n' +
+ `GRANT CONNECT ON DATABASE "${dbname}" TO custom_instance_user;\n` +
+ 'GRANT USAGE ON SCHEMA public TO custom_instance_user;\n' +
+ 'GRANT CREATE ON SCHEMA public TO custom_instance_user;\n' +
+ `GRANT CREATE ON DATABASE "${dbname}" TO custom_instance_user;\n` +
+ 'ALTER DEFAULT PRIVILEGES IN SCHEMA public \n' +
+ ' GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES\n TO custom_instance_user;\n' +
+ 'ALTER ROLE custom_instance_user CREATEROLE;'
+ },
+ {
+ title: 'Grant replication to custom_instance_replication_user',
+ status: status?.logs.replication_user,
+ description:
+ 'Postgres triggers on custom-instance datatables connect as custom_instance_replication_user, whose password is stored in global_settings.custom_instance_replication_pwd. The role is cluster-wide, so it is created on the Windmill PostgreSQL instance rather than on this database : \n\n' +
+ 'ALTER ROLE custom_instance_replication_user REPLICATION;\n' +
+ 'GRANT custom_instance_user TO custom_instance_replication_user;\n\n' +
+ 'Setting REPLICATION requires a superuser on PostgreSQL 15 and older. Managed instances never grant one, so on AWS RDS Windmill falls back to GRANT rds_replication TO custom_instance_replication_user. The database stays usable for datatables if this step fails, but postgres triggers on them do not.' +
+ (status?.logs.replication_user_error
+ ? `\n\nError: ${status.logs.replication_user_error}`
+ : '')
+ }
+ ]
+}
diff --git a/frontend/src/lib/components/workspaceSettings/supabaseOauth.svelte.ts b/frontend/src/lib/components/workspaceSettings/supabaseOauth.svelte.ts
new file mode 100644
index 0000000000..2fed2f2947
--- /dev/null
+++ b/frontend/src/lib/components/workspaceSettings/supabaseOauth.svelte.ts
@@ -0,0 +1,53 @@
+import { fromStore } from 'svelte/store'
+import { base } from '$lib/base'
+import { oauthStore } from '$lib/stores'
+
+const OAUTH_WINDOW = 'windmill_supabase_oauth'
+const CONNECT_URL = `${base}/api/oauth/connect/supabase_wizard`
+
+/**
+ * The Supabase authorization leg, driven from a popup.
+ *
+ * A full-page redirect unmounts whatever opened it, so a user who stops to create a Supabase
+ * account lands on their dashboard with nothing left pointing back. Keeping the flow in a
+ * popup keeps the host on screen, and keeps the window ours to steer: after they sign up we
+ * send the same popup back through the connect endpoint and consent follows.
+ */
+export function useSupabaseOauth(opts: { onPopupBlocked?: () => void } = {}) {
+ const oauth = fromStore(oauthStore)
+ let pending = $state(false)
+ let win: Window | null = null
+
+ $effect(() => {
+ function onMessage(e: MessageEvent) {
+ if (e.origin !== window.location.origin || e.data?.type !== 'supabase_oauth') return
+ oauthStore.set(e.data.res)
+ pending = false
+ win?.close()
+ }
+ window.addEventListener('message', onMessage)
+ return () => window.removeEventListener('message', onMessage)
+ })
+
+ return {
+ get token(): string | undefined {
+ return oauth.current?.access_token
+ },
+ get authed(): boolean {
+ return !!oauth.current?.access_token
+ },
+ get pending(): boolean {
+ return pending
+ },
+ /** Opens (or re-points) the popup. Falls back to a redirect where popups are blocked. */
+ connect() {
+ win = window.open(CONNECT_URL, OAUTH_WINDOW, 'width=600,height=820')
+ if (!win) {
+ opts.onPopupBlocked?.()
+ window.location.href = CONNECT_URL
+ return
+ }
+ pending = true
+ }
+ }
+}
diff --git a/frontend/src/lib/components/workspaceSettings/supabaseProvisioning.ts b/frontend/src/lib/components/workspaceSettings/supabaseProvisioning.ts
index ed8c4e9490..3b724896d5 100644
--- a/frontend/src/lib/components/workspaceSettings/supabaseProvisioning.ts
+++ b/frontend/src/lib/components/workspaceSettings/supabaseProvisioning.ts
@@ -6,6 +6,8 @@
* access token.
*/
+import type { SetupStep } from '../wizards/SetupChecklist.svelte'
+
export type SupabaseOrg = { id: string; name: string; slug?: string }
export type SupabaseProject = {
@@ -16,6 +18,22 @@ export type SupabaseProject = {
database?: { host: string }
}
+/**
+ * The provisioning stages, as a checklist. Each entry is driven only by its own index, so a
+ * host that stops at the Supabase side can take the first two and leave the rest.
+ * `stage` is 0 idle, 1 creating, 2 starting, 3 checking, 4 ready.
+ */
+export function supabaseSetupSteps(stage: number, failed = false): SetupStep[] {
+ const titles = ['Created on Supabase', 'Starting it up', 'Checking Windmill can store data']
+ return titles.map((title, i) => {
+ const done = stage >= i + 2
+ const running = stage === i + 1
+ if (done) return { title, status: 'done' }
+ if (running) return { title, status: failed ? 'failed' : 'running' }
+ return { title, status: 'pending' }
+ })
+}
+
/** Region codes accepted by region_selection. */
export const SUPABASE_REGIONS = [
'us-east-1',
@@ -126,6 +144,13 @@ export function supabaseResourceValue(project: SupabaseProject, passwordVarPath:
port: 5432,
dbname: 'postgres',
sslmode: 'prefer',
- password: `$var:${passwordVarPath}`
+ password: `$var:${passwordVarPath}`,
+ // Resource forms fill in every unset property from the schema as soon as they render,
+ // so a postgresql resource saved without these comes up already modified -- and saves a
+ // draft -- the first time anyone opens it. Write them here so opening one is a no-op.
+ // (accept_invalid_certs renders conditionally and is not seeded, so it stays out.)
+ region: '',
+ root_certificate_pem: '',
+ use_iam_auth: false
}
}
diff --git a/frontend/src/routes/kitchen_sink/setup_checklist/+page.svelte b/frontend/src/routes/kitchen_sink/setup_checklist/+page.svelte
new file mode 100644
index 0000000000..7c4aefc51c
--- /dev/null
+++ b/frontend/src/routes/kitchen_sink/setup_checklist/+page.svelte
@@ -0,0 +1,132 @@
+
+
+