@specific.dev/spectest 0.26.0 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/aws-sigv4.d.ts +42 -0
- package/dist/aws-sigv4.js +166 -0
- package/dist/browser.d.ts +314 -0
- package/dist/browser.js +1320 -0
- package/dist/components/email.d.ts +135 -0
- package/dist/components/email.js +271 -0
- package/dist/components/expo.d.ts +69 -0
- package/dist/components/expo.js +125 -0
- package/dist/components/index.d.ts +8 -0
- package/dist/components/index.js +18 -0
- package/dist/components/k3s.d.ts +172 -0
- package/dist/components/k3s.js +1124 -0
- package/dist/components/postgres.d.ts +93 -0
- package/dist/components/postgres.js +58 -0
- package/dist/components/replayFake.d.ts +169 -0
- package/dist/components/replayFake.js +738 -0
- package/dist/components/s3.d.ts +99 -0
- package/dist/components/s3.js +81 -0
- package/dist/components/supabase.d.ts +197 -0
- package/dist/components/supabase.js +1003 -0
- package/dist/daemon.d.ts +1 -0
- package/dist/daemon.js +4611 -0
- package/dist/ids.d.ts +2 -0
- package/{src/ids.ts → dist/ids.js} +46 -50
- package/dist/index.d.ts +1328 -0
- package/dist/index.js +769 -0
- package/dist/ingress.d.ts +114 -0
- package/dist/ingress.js +210 -0
- package/dist/inspect.d.ts +228 -0
- package/dist/inspect.js +429 -0
- package/dist/locator.d.ts +260 -0
- package/dist/locator.js +293 -0
- package/dist/mobile.d.ts +71 -0
- package/dist/mobile.js +65 -0
- package/dist/record-secrets.d.ts +9 -0
- package/{src/record-secrets.ts → dist/record-secrets.js} +13 -15
- package/dist/recorder.d.ts +527 -0
- package/dist/recorder.js +219 -0
- package/dist/redis.d.ts +54 -0
- package/dist/redis.js +126 -0
- package/dist/replay-bundle.d.ts +38 -0
- package/{src/replay-bundle.ts → dist/replay-bundle.js} +29 -47
- package/dist/resolver.d.ts +1 -0
- package/dist/resolver.js +309 -0
- package/dist/s3.d.ts +89 -0
- package/dist/s3.js +198 -0
- package/dist/sql.d.ts +74 -0
- package/dist/sql.js +151 -0
- package/dist/terminal.d.ts +161 -0
- package/dist/terminal.js +538 -0
- package/package.json +24 -9
- package/src/browser.ts +0 -1819
- package/src/components/email.ts +0 -398
- package/src/components/expo.ts +0 -167
- package/src/components/index.ts +0 -63
- package/src/components/k3s.ts +0 -1312
- package/src/components/postgres.ts +0 -105
- package/src/components/replayFake.ts +0 -848
- package/src/components/s3.ts +0 -132
- package/src/components/supabase.ts +0 -1299
- package/src/daemon.ts +0 -4969
- package/src/index.ts +0 -2350
- package/src/ingress.ts +0 -288
- package/src/inspect.ts +0 -673
- package/src/locator.ts +0 -594
- package/src/mobile.ts +0 -133
- package/src/recorder.ts +0 -817
- package/src/redis.ts +0 -202
- package/src/resolver.ts +0 -351
- package/src/s3.ts +0 -333
- package/src/sql.ts +0 -243
- package/src/terminal.ts +0 -740
- package/src/vendor/rrweb-plugin-console-record.umd.js +0 -521
- package/src/vendor/rrweb-record.min.js +0 -5061
|
@@ -1,1299 +0,0 @@
|
|
|
1
|
-
// A ready-to-use, self-hosted **Supabase** stack, expressed as a set of
|
|
2
|
-
// spectest services (one container each) wired together on spectest-net.
|
|
3
|
-
//
|
|
4
|
-
// This is the standard Supabase self-hosted Docker topology
|
|
5
|
-
// (github.com/supabase/supabase, `docker/docker-compose.yml`) reproduced with
|
|
6
|
-
// the *exact upstream images and config* — the Kong API gateway, GoTrue auth,
|
|
7
|
-
// PostgREST, storage-api (+imgproxy), Realtime, and a `supabase/postgres`
|
|
8
|
-
// database with the standard role/JWT bootstrap — just orchestrated as
|
|
9
|
-
// spectest services instead of via docker-compose. That means it rides
|
|
10
|
-
// spectest's parallel image prep, ready-check DAG, DNS, host image cache, and
|
|
11
|
-
// snapshot/fork for free, with no daemon or control-plane changes.
|
|
12
|
-
//
|
|
13
|
-
// Unlike the single-`ServiceDefinition` components (`postgres()`, `s3()`),
|
|
14
|
-
// Supabase is inherently a constellation of containers, so `supabase()` returns
|
|
15
|
-
// a **service group** (one services-map entry that `defineEnvironment` expands)
|
|
16
|
-
// plus the derived connection surface:
|
|
17
|
-
//
|
|
18
|
-
// ```ts
|
|
19
|
-
// import { defineEnvironment } from "@specific.dev/spectest";
|
|
20
|
-
// import { supabase } from "@specific.dev/spectest/components";
|
|
21
|
-
//
|
|
22
|
-
// const sb = supabase({ dbPassword: "postgres" });
|
|
23
|
-
//
|
|
24
|
-
// export const env = defineEnvironment({
|
|
25
|
-
// name: "my-app",
|
|
26
|
-
// services: {
|
|
27
|
-
// supabase: sb.group, // expands to supabase, supabase-db, supabase-auth, ...
|
|
28
|
-
// app: {
|
|
29
|
-
// image: { type: "dockerfile", content: "..." },
|
|
30
|
-
// env: { ...sb.appEnv }, // SUPABASE_URL / _ANON_KEY / _SERVICE_ROLE_KEY / DATABASE_URL
|
|
31
|
-
// dependsOn: [sb.ready], // wait for the gateway (and the stack behind it)
|
|
32
|
-
// },
|
|
33
|
-
// },
|
|
34
|
-
// });
|
|
35
|
-
//
|
|
36
|
-
// // in a test:
|
|
37
|
-
// await ctx.svc.supabase.sql`select * from auth.users`; // instrumented DB client
|
|
38
|
-
// await ctx.fetch(`${sb.url}/rest/v1/todos`, { headers: { apikey: sb.anonKey } });
|
|
39
|
-
// ```
|
|
40
|
-
//
|
|
41
|
-
// The gateway is the group's primary part, so it takes the group key itself:
|
|
42
|
-
// reachable at `http://supabase:8000` on spectest-net, helpers (the group's
|
|
43
|
-
// consolidated handle) at `ctx.svc.supabase`, and `dependsOn: ["supabase"]`
|
|
44
|
-
// waits for the whole stack (group expansion makes the primary the sink).
|
|
45
|
-
// Every other piece lands at `<name>-<part>` (`supabase-db`, `supabase-auth`,
|
|
46
|
-
// …); internal references (env + the Kong declarative config) are built from
|
|
47
|
-
// the group naming context, so two Supabase stacks (or a user service
|
|
48
|
-
// innocently named `db`) never collide. The group must be mounted at the key
|
|
49
|
-
// matching the `name` option (default `"supabase"`) since the derived surface
|
|
50
|
-
// (`sb.url`, `sb.appEnv`) embeds it — a mismatch errors at load.
|
|
51
|
-
|
|
52
|
-
import { createHmac } from "node:crypto";
|
|
53
|
-
import { existsSync } from "node:fs";
|
|
54
|
-
import { readdir, stat } from "node:fs/promises";
|
|
55
|
-
import { join } from "node:path";
|
|
56
|
-
|
|
57
|
-
import {
|
|
58
|
-
serviceGroup,
|
|
59
|
-
type ServiceConfig,
|
|
60
|
-
type ServiceDefinition,
|
|
61
|
-
type ServiceGroup,
|
|
62
|
-
type ServiceSetupContext,
|
|
63
|
-
type ServicesMap,
|
|
64
|
-
} from "../index.js";
|
|
65
|
-
import { SQL, type SqlClient } from "../sql.js";
|
|
66
|
-
import { email, emailHelpers, type EmailHelpers } from "./email.js";
|
|
67
|
-
|
|
68
|
-
// ──────────────────────────────────────────────────────────────────────────
|
|
69
|
-
// Pinned upstream image tags (github.com/supabase/supabase docker-compose.yml,
|
|
70
|
-
// current master). Bump together; each is pulled through the host Hub mirror on
|
|
71
|
-
// first cold boot and captured into the warm-template snapshot thereafter.
|
|
72
|
-
// ──────────────────────────────────────────────────────────────────────────
|
|
73
|
-
const IMAGES = {
|
|
74
|
-
db: "supabase/postgres:17.6.1.136",
|
|
75
|
-
kong: "kong/kong:3.9.1",
|
|
76
|
-
auth: "supabase/gotrue:v2.189.0",
|
|
77
|
-
rest: "postgrest/postgrest:v14.12",
|
|
78
|
-
realtime: "supabase/realtime:v2.102.3",
|
|
79
|
-
storage: "supabase/storage-api:v1.60.4",
|
|
80
|
-
imgproxy: "darthsim/imgproxy:v3.30.1",
|
|
81
|
-
meta: "supabase/postgres-meta:v0.96.6",
|
|
82
|
-
studio: "supabase/studio:2026.07.07-sha-a6a04f2",
|
|
83
|
-
} as const;
|
|
84
|
-
|
|
85
|
-
export interface SupabaseOptions {
|
|
86
|
-
/**
|
|
87
|
-
* Key of the API-gateway service and the prefix for every other piece.
|
|
88
|
-
* Default `"supabase"`. The gateway is reachable at `http://<name>:8000`
|
|
89
|
-
* and its helpers at `ctx.svc.<name>`; the rest are `<name>-db`,
|
|
90
|
-
* `<name>-auth`, `<name>-rest`, `<name>-storage`, `<name>-realtime`, ….
|
|
91
|
-
*/
|
|
92
|
-
name?: string;
|
|
93
|
-
/**
|
|
94
|
-
* Password for the Postgres roles the services authenticate with (and the
|
|
95
|
-
* `postgres` superuser the app / SQL helper connect as). Default
|
|
96
|
-
* `"postgres"`. Purely local to the hermetic VM — not a secret to guard.
|
|
97
|
-
*/
|
|
98
|
-
dbPassword?: string;
|
|
99
|
-
/**
|
|
100
|
-
* HS256 secret that signs and verifies all JWTs (user access tokens plus the
|
|
101
|
-
* derived `anonKey` / `serviceRoleKey` API keys). Must be ≥32 chars. Default
|
|
102
|
-
* is Supabase's well-known demo secret. Changing it re-derives the keys
|
|
103
|
-
* deterministically, so the warm-template cache stays stable.
|
|
104
|
-
*/
|
|
105
|
-
jwtSecret?: string;
|
|
106
|
-
/** Studio dashboard Basic-Auth username (only relevant with `studio`). Default `"supabase"`. */
|
|
107
|
-
dashboardUsername?: string;
|
|
108
|
-
/** Studio dashboard Basic-Auth password (only relevant with `studio`). */
|
|
109
|
-
dashboardPassword?: string;
|
|
110
|
-
|
|
111
|
-
/** Include GoTrue auth (`<name>-auth`). Default `true`. */
|
|
112
|
-
auth?: boolean;
|
|
113
|
-
/** Include storage-api + imgproxy (`<name>-storage`, `<name>-imgproxy`). Default `true`. */
|
|
114
|
-
storage?: boolean;
|
|
115
|
-
/** Include Realtime (`<name>-realtime`). Default `true`. */
|
|
116
|
-
realtime?: boolean;
|
|
117
|
-
/**
|
|
118
|
-
* Include the Studio dashboard (`<name>-studio`) and its postgres-meta
|
|
119
|
-
* backend (`<name>-meta`). Off by default — Studio is a heavy Next.js image
|
|
120
|
-
* and is only useful for interactive `ctx.browser()` exploration, not
|
|
121
|
-
* automated backend assertions. Turning it on implies `meta`.
|
|
122
|
-
*/
|
|
123
|
-
studio?: boolean;
|
|
124
|
-
/** Include postgres-meta (`<name>-meta`). Defaults to whatever `studio` is. */
|
|
125
|
-
meta?: boolean;
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Also serve the gateway over **HTTPS** at `https://<hostname>` via the
|
|
129
|
-
* daemon's CA-trusted TLS reverse proxy (in addition to the plain
|
|
130
|
-
* `http://<name>:8000`). Set this only when the app under test hardcodes a
|
|
131
|
-
* specific Supabase URL it can't be told to override — otherwise leave it and
|
|
132
|
-
* point the app at `sb.url`.
|
|
133
|
-
*/
|
|
134
|
-
hostname?: string;
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Capture GoTrue's outgoing mail (magic links, OTPs, password recovery,
|
|
138
|
-
* invites — and signup confirmations with `autoconfirm: false`) with the
|
|
139
|
-
* standard `email()` component. **On by default**: the stack includes the
|
|
140
|
-
* capture server (`<name>-mail`) with GoTrue's SMTP wired at it, and tests
|
|
141
|
-
* read the mailbox through the typed helpers at `ctx.svc.<name>.mail`
|
|
142
|
-
* (`lastEmail` / `emails` / `clear`), each captured message rendering
|
|
143
|
-
* mail-client-style on the dashboard timeline.
|
|
144
|
-
*
|
|
145
|
-
* Signups stay one-step by default (`autoconfirm: true`) so the mailbox is
|
|
146
|
-
* purely additive; pass `{ autoconfirm: false }` to test real
|
|
147
|
-
* email-confirmation flows. Pass `{ service: "<key>" }` to point GoTrue at
|
|
148
|
-
* an `email()` service the environment already declares — keep a single
|
|
149
|
-
* mailbox per environment rather than one per component: if the app under
|
|
150
|
-
* test also sends mail, share one `email()` between it and Supabase
|
|
151
|
-
* instead of standing up a second capture server. `false` disables mail
|
|
152
|
-
* capture entirely.
|
|
153
|
-
*/
|
|
154
|
-
mail?: boolean | SupabaseMailOptions;
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Extra API keys the gateway accepts alongside the derived JWT keys — for
|
|
158
|
-
* clients that ship a hardcoded key (e.g. an `sb_publishable_…` /
|
|
159
|
-
* `sb_secret_…` pair baked into an app build). Each `anon` key is admitted
|
|
160
|
-
* with anon privileges and each `serviceRole` key with service-role
|
|
161
|
-
* privileges; Kong swaps the matching *derived JWT* in before proxying, so
|
|
162
|
-
* GoTrue/PostgREST/Realtime still receive a validly-signed token.
|
|
163
|
-
*/
|
|
164
|
-
extraApiKeys?: { anon?: string[]; serviceRole?: string[] };
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Auto-apply SQL migrations found in the project before any dependent service
|
|
168
|
-
* starts. `true` (default) applies every `*.sql` under `supabase/migrations/`
|
|
169
|
-
* (sorted, the Supabase convention), then `supabase/seed.sql` if present — a
|
|
170
|
-
* no-op when the directory is absent. Pass a string to point at a different
|
|
171
|
-
* directory (relative to the project root or absolute), or `false` to skip.
|
|
172
|
-
* An *explicitly configured* path that doesn't resolve is an error at env
|
|
173
|
-
* start (a silent no-op there surfaces much later as a confusing
|
|
174
|
-
* `relation … does not exist`).
|
|
175
|
-
* Because `supabase/**` is project content, editing a migration correctly
|
|
176
|
-
* forces a cold rebuild and re-apply (unlike `spectest/tests/**`).
|
|
177
|
-
*/
|
|
178
|
-
migrations?: boolean | string;
|
|
179
|
-
/**
|
|
180
|
-
* Seed SQL applied after migrations. `true` (default) applies
|
|
181
|
-
* `supabase/seed.sql` if it exists; a string overrides the path; `false`
|
|
182
|
-
* skips it.
|
|
183
|
-
*/
|
|
184
|
-
seed?: boolean | string;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/** Tuning for `SupabaseOptions.mail`. */
|
|
188
|
-
export interface SupabaseMailOptions {
|
|
189
|
-
/**
|
|
190
|
-
* Reuse an `email()` service the environment already declares (its
|
|
191
|
-
* services-map key) instead of adding one to the group — the way to keep a
|
|
192
|
-
* single global mailbox when the app under test sends mail too. Assumes
|
|
193
|
-
* the service's default ports (SMTP 1025, API 8025).
|
|
194
|
-
*/
|
|
195
|
-
service?: string;
|
|
196
|
-
/**
|
|
197
|
-
* Whether GoTrue autoconfirms signups. `true` (default): signups complete
|
|
198
|
-
* in one step and send no confirmation mail — recovery, magic-link and OTP
|
|
199
|
-
* mail is still sent and captured. Set `false` to exercise real
|
|
200
|
-
* email-confirmation flows (signups require the emailed verification).
|
|
201
|
-
*/
|
|
202
|
-
autoconfirm?: boolean;
|
|
203
|
-
/** Sender address GoTrue mails from. Default `admin@example.com`. */
|
|
204
|
-
adminEmail?: string;
|
|
205
|
-
/** Sender display name. Default `Supabase`. */
|
|
206
|
-
senderName?: string;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/** Helpers the gateway service exposes on `ctx.svc.<name>`. */
|
|
210
|
-
export interface SupabaseHelpers {
|
|
211
|
-
/**
|
|
212
|
-
* An instrumented {@link SqlClient} connected to the database as the
|
|
213
|
-
* `postgres` superuser — every query lands on the test event log and its rows
|
|
214
|
-
* come back provenance-wrapped, so `expect(...)` on a read links to the
|
|
215
|
-
* query. Point of direct DB verification alongside the REST/auth surface.
|
|
216
|
-
*/
|
|
217
|
-
sql: SqlClient;
|
|
218
|
-
/** The gateway base URL the app uses as `SUPABASE_URL` (e.g. `http://supabase:8000`). */
|
|
219
|
-
url: string;
|
|
220
|
-
/** Long-lived `anon` API key (HS256 JWT). Pass as the `apikey` header. */
|
|
221
|
-
anonKey: string;
|
|
222
|
-
/** Long-lived `service_role` API key (HS256 JWT). Bypasses RLS — server-side only. */
|
|
223
|
-
serviceRoleKey: string;
|
|
224
|
-
/**
|
|
225
|
-
* The captured-mail mailbox (`lastEmail` / `emails` / `clear`) — present
|
|
226
|
-
* unless the stack was built with `mail: false`. Wait for delivery with
|
|
227
|
-
* the standard `ctx.poll`:
|
|
228
|
-
*
|
|
229
|
-
* ```ts
|
|
230
|
-
* const mail = await ctx.poll("confirmation email", () =>
|
|
231
|
-
* ctx.svc.supabase.mail!.lastEmail());
|
|
232
|
-
* expect(mail.to).toContain("alice@example.com");
|
|
233
|
-
* ```
|
|
234
|
-
*/
|
|
235
|
-
mail?: EmailHelpers;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/** What `supabase()` returns: the service group plus the derived
|
|
239
|
-
* connection surface for wiring the app under test. */
|
|
240
|
-
export interface SupabaseStack {
|
|
241
|
-
/** The Supabase stack as a service group. Mount it at the key matching
|
|
242
|
-
* the `name` option (default `"supabase"`):
|
|
243
|
-
* `services: { supabase: sb.group, ... }`. */
|
|
244
|
-
group: ServiceGroup<ServicesMap, SupabaseHelpers>;
|
|
245
|
-
/** Gateway base URL — `http://<name>:8000`, or `https://<hostname>` when set. */
|
|
246
|
-
url: string;
|
|
247
|
-
/** `anon` API key (HS256 JWT). */
|
|
248
|
-
anonKey: string;
|
|
249
|
-
/** `service_role` API key (HS256 JWT). */
|
|
250
|
-
serviceRoleKey: string;
|
|
251
|
-
/** Direct Postgres URL for the app (`postgres` superuser): `postgresql://postgres:<pw>@<name>-db:5432/postgres`. */
|
|
252
|
-
dbUrl: string;
|
|
253
|
-
/** The gateway service key — use in a dependent service's `dependsOn` to wait
|
|
254
|
-
* for the whole stack (the gateway waits on the services behind it). */
|
|
255
|
-
ready: string;
|
|
256
|
-
/**
|
|
257
|
-
* Where the stack's SMTP capture server listens (absent with
|
|
258
|
-
* `mail: false`) — point the app under test's own mail transport here to
|
|
259
|
-
* share the one mailbox: `env: { SMTP_HOST: sb.smtp.host, SMTP_PORT: String(sb.smtp.port) }`.
|
|
260
|
-
*/
|
|
261
|
-
smtp?: { host: string; port: number };
|
|
262
|
-
/** Ready-to-spread env for the app under test: `SUPABASE_URL`,
|
|
263
|
-
* `SUPABASE_ANON_KEY`, `SUPABASE_SERVICE_ROLE_KEY`, `DATABASE_URL`. */
|
|
264
|
-
appEnv: Record<string, string>;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// Supabase's well-known demo secret (≥32 chars). Local-only; override via opts.
|
|
268
|
-
const DEFAULT_JWT_SECRET =
|
|
269
|
-
"your-super-secret-jwt-token-with-at-least-32-characters-long";
|
|
270
|
-
|
|
271
|
-
// Fixed issued-at / expiry for the derived API keys. Constant (not `Date.now`)
|
|
272
|
-
// so the keys are a pure function of the secret and never perturb the
|
|
273
|
-
// warm-template hash. `exp` is 2027-01-01, matching Supabase's demo keys.
|
|
274
|
-
const KEY_IAT = 1641769200;
|
|
275
|
-
const KEY_EXP = 1799535600;
|
|
276
|
-
|
|
277
|
-
function base64url(input: Buffer | string): string {
|
|
278
|
-
return Buffer.from(input)
|
|
279
|
-
.toString("base64")
|
|
280
|
-
.replace(/\+/g, "-")
|
|
281
|
-
.replace(/\//g, "_")
|
|
282
|
-
.replace(/=+$/, "");
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
/** Sign a compact HS256 JWT. Any token with the right `role` claim signed by
|
|
286
|
-
* `JWT_SECRET` is a valid Supabase API key — we don't need to reproduce the
|
|
287
|
-
* exact demo string, just a validly-signed one. */
|
|
288
|
-
function signHs256(payload: Record<string, unknown>, secret: string): string {
|
|
289
|
-
const header = base64url(JSON.stringify({ alg: "HS256", typ: "JWT" }));
|
|
290
|
-
const body = base64url(JSON.stringify(payload));
|
|
291
|
-
const data = `${header}.${body}`;
|
|
292
|
-
const sig = base64url(createHmac("sha256", secret).update(data).digest());
|
|
293
|
-
return `${data}.${sig}`;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
function apiKey(role: "anon" | "service_role", secret: string): string {
|
|
297
|
-
return signHs256({ role, iss: "supabase-demo", iat: KEY_IAT, exp: KEY_EXP }, secret);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// ──────────────────────────────────────────────────────────────────────────
|
|
301
|
-
// Postgres bootstrap SQL. The `supabase/postgres` image already creates every
|
|
302
|
-
// Supabase role/schema in its baked init-scripts; we only need to (a) set the
|
|
303
|
-
// network-facing roles' passwords to ours, (b) plant the JWT secret as a DB GUC
|
|
304
|
-
// (used by RLS `auth.jwt()`), and (c) create the `_realtime` schema.
|
|
305
|
-
//
|
|
306
|
-
// We apply this in the db service's `setup` hook (via `psql`) rather than as
|
|
307
|
-
// mounted init-scripts, deliberately: the image's `migrate.sh` runs mounted
|
|
308
|
-
// files under `ON_ERROR_STOP`, so a single unexpected role (e.g.
|
|
309
|
-
// `supabase_functions_admin`, which only exists when the DB-webhooks script is
|
|
310
|
-
// present) aborts the whole init and the container exits. `setup` runs after
|
|
311
|
-
// the DB is ready and before any dependent service starts, so ordering is still
|
|
312
|
-
// guaranteed, and we touch only roles the base image is known to create.
|
|
313
|
-
// ──────────────────────────────────────────────────────────────────────────
|
|
314
|
-
|
|
315
|
-
/** Quote a Postgres string literal (single-quote, double any embedded quote). */
|
|
316
|
-
function pgLiteral(s: string): string {
|
|
317
|
-
return `'${s.replace(/'/g, "''")}'`;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
function buildBootstrapSql(
|
|
321
|
-
dbPassword: string,
|
|
322
|
-
jwtSecret: string,
|
|
323
|
-
withRealtime: boolean,
|
|
324
|
-
): string {
|
|
325
|
-
const pw = pgLiteral(dbPassword);
|
|
326
|
-
const lines = [
|
|
327
|
-
// Network-facing roles the services authenticate as. supabase_admin and
|
|
328
|
-
// postgres already have their password set to POSTGRES_PASSWORD by the
|
|
329
|
-
// image's own bootstrap, so we leave them alone.
|
|
330
|
-
`ALTER USER authenticator WITH PASSWORD ${pw};`,
|
|
331
|
-
`ALTER USER supabase_auth_admin WITH PASSWORD ${pw};`,
|
|
332
|
-
`ALTER USER supabase_storage_admin WITH PASSWORD ${pw};`,
|
|
333
|
-
// JWT secret GUC for RLS (`current_setting('app.settings.jwt_secret')`).
|
|
334
|
-
`ALTER DATABASE postgres SET "app.settings.jwt_secret" TO ${pgLiteral(jwtSecret)};`,
|
|
335
|
-
`ALTER DATABASE postgres SET "app.settings.jwt_exp" TO '3600';`,
|
|
336
|
-
];
|
|
337
|
-
if (withRealtime) {
|
|
338
|
-
lines.push(
|
|
339
|
-
"CREATE SCHEMA IF NOT EXISTS _realtime;",
|
|
340
|
-
"ALTER SCHEMA _realtime OWNER TO supabase_admin;",
|
|
341
|
-
);
|
|
342
|
-
}
|
|
343
|
-
return lines.join("\n") + "\n";
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
// ──────────────────────────────────────────────────────────────────────────
|
|
347
|
-
// Kong declarative gateway config (docker/volumes/api/kong.yml). Embedded
|
|
348
|
-
// verbatim with `$VAR` placeholders; substituted below the same way
|
|
349
|
-
// kong-entrypoint.sh does — including the legacy (opaque-keys-disabled) Lua
|
|
350
|
-
// expressions that just pass the `apikey` header/query param straight through.
|
|
351
|
-
// ──────────────────────────────────────────────────────────────────────────
|
|
352
|
-
|
|
353
|
-
// The two Lua router expressions kong-entrypoint.sh emits when the opaque
|
|
354
|
-
// `sb_`-style keys are NOT configured: pass the incoming apikey (a legacy
|
|
355
|
-
// HS256 JWT) through unchanged, preferring an existing non-`sb_`
|
|
356
|
-
// Authorization header. `extraApiKeys` extends both: an extra key (arriving
|
|
357
|
-
// as `apikey` header, `Authorization: Bearer <key>`, or the websocket
|
|
358
|
-
// `?apikey=` query param) is swapped for the corresponding *derived JWT*
|
|
359
|
-
// before proxying, so the upstream services still see a validly-signed token.
|
|
360
|
-
function buildLuaExprs(
|
|
361
|
-
extra: Array<{ key: string; jwt: string }>,
|
|
362
|
-
): { auth: string; realtimeWs: string } {
|
|
363
|
-
// Keep a client's `Authorization: Bearer <extraKey>` from winning the
|
|
364
|
-
// header-passthrough clause with the raw (non-JWT) key.
|
|
365
|
-
const guards = extra
|
|
366
|
-
.map((m) => ` and headers.authorization ~= 'Bearer ${m.key}'`)
|
|
367
|
-
.join("");
|
|
368
|
-
const mappings = extra
|
|
369
|
-
.map(
|
|
370
|
-
(m) =>
|
|
371
|
-
` or ((headers.apikey == '${m.key}' or headers.authorization == 'Bearer ${m.key}') and 'Bearer ${m.jwt}')`,
|
|
372
|
-
)
|
|
373
|
-
.join("");
|
|
374
|
-
const auth = `$((headers.authorization ~= nil and headers.authorization:sub(1, 10) ~= 'Bearer sb_'${guards} and headers.authorization)${mappings} or headers.apikey)`;
|
|
375
|
-
const wsMappings = extra
|
|
376
|
-
.map((m) => `(query_params.apikey == '${m.key}' and '${m.jwt}') or `)
|
|
377
|
-
.join("");
|
|
378
|
-
return { auth, realtimeWs: `$(${wsMappings}query_params.apikey)` };
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
const KONG_YML_RAW = String.raw`_format_version: '2.1'
|
|
382
|
-
_transform: true
|
|
383
|
-
|
|
384
|
-
consumers:
|
|
385
|
-
- username: DASHBOARD
|
|
386
|
-
- username: anon
|
|
387
|
-
keyauth_credentials:
|
|
388
|
-
- key: $SUPABASE_ANON_KEY
|
|
389
|
-
- key: $SUPABASE_PUBLISHABLE_KEY
|
|
390
|
-
- username: service_role
|
|
391
|
-
keyauth_credentials:
|
|
392
|
-
- key: $SUPABASE_SERVICE_KEY
|
|
393
|
-
- key: $SUPABASE_SECRET_KEY
|
|
394
|
-
|
|
395
|
-
acls:
|
|
396
|
-
- consumer: anon
|
|
397
|
-
group: anon
|
|
398
|
-
- consumer: service_role
|
|
399
|
-
group: admin
|
|
400
|
-
|
|
401
|
-
basicauth_credentials:
|
|
402
|
-
- consumer: DASHBOARD
|
|
403
|
-
username: '$DASHBOARD_USERNAME'
|
|
404
|
-
password: '$DASHBOARD_PASSWORD'
|
|
405
|
-
|
|
406
|
-
services:
|
|
407
|
-
- name: auth-v1-open
|
|
408
|
-
url: http://auth:9999/verify
|
|
409
|
-
routes:
|
|
410
|
-
- name: auth-v1-open
|
|
411
|
-
strip_path: true
|
|
412
|
-
paths:
|
|
413
|
-
- /auth/v1/verify
|
|
414
|
-
plugins:
|
|
415
|
-
- name: cors
|
|
416
|
-
- name: auth-v1-open-callback
|
|
417
|
-
url: http://auth:9999/callback
|
|
418
|
-
routes:
|
|
419
|
-
- name: auth-v1-open-callback
|
|
420
|
-
strip_path: true
|
|
421
|
-
paths:
|
|
422
|
-
- /auth/v1/callback
|
|
423
|
-
plugins:
|
|
424
|
-
- name: cors
|
|
425
|
-
- name: auth-v1-open-authorize
|
|
426
|
-
url: http://auth:9999/authorize
|
|
427
|
-
routes:
|
|
428
|
-
- name: auth-v1-open-authorize
|
|
429
|
-
strip_path: true
|
|
430
|
-
paths:
|
|
431
|
-
- /auth/v1/authorize
|
|
432
|
-
plugins:
|
|
433
|
-
- name: cors
|
|
434
|
-
- name: auth-v1-open-jwks
|
|
435
|
-
url: http://auth:9999/.well-known/jwks.json
|
|
436
|
-
routes:
|
|
437
|
-
- name: auth-v1-open-jwks
|
|
438
|
-
strip_path: true
|
|
439
|
-
paths:
|
|
440
|
-
- /auth/v1/.well-known/jwks.json
|
|
441
|
-
plugins:
|
|
442
|
-
- name: cors
|
|
443
|
-
- name: auth-v1-open-sso-acs
|
|
444
|
-
url: "http://auth:9999/sso/saml/acs"
|
|
445
|
-
routes:
|
|
446
|
-
- name: auth-v1-open-sso-acs
|
|
447
|
-
strip_path: true
|
|
448
|
-
paths:
|
|
449
|
-
- /auth/v1/sso/saml/acs
|
|
450
|
-
plugins:
|
|
451
|
-
- name: cors
|
|
452
|
-
- name: auth-v1-open-sso-metadata
|
|
453
|
-
url: "http://auth:9999/sso/saml/metadata"
|
|
454
|
-
routes:
|
|
455
|
-
- name: auth-v1-open-sso-metadata
|
|
456
|
-
strip_path: true
|
|
457
|
-
paths:
|
|
458
|
-
- /auth/v1/sso/saml/metadata
|
|
459
|
-
plugins:
|
|
460
|
-
- name: cors
|
|
461
|
-
- name: auth-v1
|
|
462
|
-
url: http://auth:9999/
|
|
463
|
-
routes:
|
|
464
|
-
- name: auth-v1-all
|
|
465
|
-
strip_path: true
|
|
466
|
-
paths:
|
|
467
|
-
- /auth/v1/
|
|
468
|
-
plugins:
|
|
469
|
-
- name: cors
|
|
470
|
-
- name: key-auth
|
|
471
|
-
config:
|
|
472
|
-
hide_credentials: false
|
|
473
|
-
- name: request-transformer
|
|
474
|
-
config:
|
|
475
|
-
add:
|
|
476
|
-
headers:
|
|
477
|
-
- "Authorization: $LUA_AUTH_EXPR"
|
|
478
|
-
replace:
|
|
479
|
-
headers:
|
|
480
|
-
- "Authorization: $LUA_AUTH_EXPR"
|
|
481
|
-
- name: acl
|
|
482
|
-
config:
|
|
483
|
-
hide_groups_header: true
|
|
484
|
-
allow:
|
|
485
|
-
- admin
|
|
486
|
-
- anon
|
|
487
|
-
- name: rest-v1-openapi
|
|
488
|
-
url: http://rest:3000/
|
|
489
|
-
routes:
|
|
490
|
-
- name: rest-v1-openapi-root
|
|
491
|
-
strip_path: true
|
|
492
|
-
expression: 'http.path == "/rest/v1/"'
|
|
493
|
-
plugins:
|
|
494
|
-
- name: cors
|
|
495
|
-
- name: key-auth
|
|
496
|
-
config:
|
|
497
|
-
hide_credentials: false
|
|
498
|
-
- name: request-transformer
|
|
499
|
-
config:
|
|
500
|
-
add:
|
|
501
|
-
headers:
|
|
502
|
-
- "Authorization: $LUA_AUTH_EXPR"
|
|
503
|
-
replace:
|
|
504
|
-
headers:
|
|
505
|
-
- "Authorization: $LUA_AUTH_EXPR"
|
|
506
|
-
- name: acl
|
|
507
|
-
config:
|
|
508
|
-
hide_groups_header: true
|
|
509
|
-
allow:
|
|
510
|
-
- admin
|
|
511
|
-
- name: rest-v1
|
|
512
|
-
url: http://rest:3000/
|
|
513
|
-
routes:
|
|
514
|
-
- name: rest-v1-all
|
|
515
|
-
strip_path: true
|
|
516
|
-
paths:
|
|
517
|
-
- /rest/v1/
|
|
518
|
-
plugins:
|
|
519
|
-
- name: cors
|
|
520
|
-
- name: key-auth
|
|
521
|
-
config:
|
|
522
|
-
hide_credentials: false
|
|
523
|
-
- name: request-transformer
|
|
524
|
-
config:
|
|
525
|
-
add:
|
|
526
|
-
headers:
|
|
527
|
-
- "Authorization: $LUA_AUTH_EXPR"
|
|
528
|
-
replace:
|
|
529
|
-
headers:
|
|
530
|
-
- "Authorization: $LUA_AUTH_EXPR"
|
|
531
|
-
- name: acl
|
|
532
|
-
config:
|
|
533
|
-
hide_groups_header: true
|
|
534
|
-
allow:
|
|
535
|
-
- admin
|
|
536
|
-
- anon
|
|
537
|
-
- name: graphql-v1
|
|
538
|
-
url: http://rest:3000/rpc/graphql
|
|
539
|
-
routes:
|
|
540
|
-
- name: graphql-v1-all
|
|
541
|
-
strip_path: true
|
|
542
|
-
paths:
|
|
543
|
-
- /graphql/v1
|
|
544
|
-
plugins:
|
|
545
|
-
- name: cors
|
|
546
|
-
- name: key-auth
|
|
547
|
-
config:
|
|
548
|
-
hide_credentials: false
|
|
549
|
-
- name: request-transformer
|
|
550
|
-
config:
|
|
551
|
-
add:
|
|
552
|
-
headers:
|
|
553
|
-
- "Content-Profile: graphql_public"
|
|
554
|
-
- "Authorization: $LUA_AUTH_EXPR"
|
|
555
|
-
replace:
|
|
556
|
-
headers:
|
|
557
|
-
- "Authorization: $LUA_AUTH_EXPR"
|
|
558
|
-
- name: acl
|
|
559
|
-
config:
|
|
560
|
-
hide_groups_header: true
|
|
561
|
-
allow:
|
|
562
|
-
- admin
|
|
563
|
-
- anon
|
|
564
|
-
- name: realtime-v1-ws
|
|
565
|
-
url: http://realtime-dev.supabase-realtime:4000/socket
|
|
566
|
-
protocol: ws
|
|
567
|
-
routes:
|
|
568
|
-
- name: realtime-v1-ws
|
|
569
|
-
strip_path: true
|
|
570
|
-
paths:
|
|
571
|
-
- /realtime/v1/
|
|
572
|
-
plugins:
|
|
573
|
-
- name: cors
|
|
574
|
-
- name: key-auth
|
|
575
|
-
config:
|
|
576
|
-
hide_credentials: false
|
|
577
|
-
- name: request-transformer
|
|
578
|
-
config:
|
|
579
|
-
add:
|
|
580
|
-
headers:
|
|
581
|
-
- "x-api-key:$LUA_RT_WS_EXPR"
|
|
582
|
-
replace:
|
|
583
|
-
querystring:
|
|
584
|
-
- "apikey:$LUA_RT_WS_EXPR"
|
|
585
|
-
- name: acl
|
|
586
|
-
config:
|
|
587
|
-
hide_groups_header: true
|
|
588
|
-
allow:
|
|
589
|
-
- admin
|
|
590
|
-
- anon
|
|
591
|
-
- name: realtime-v1-rest-openapi
|
|
592
|
-
url: http://realtime-dev.supabase-realtime:4000/api/openapi
|
|
593
|
-
protocol: http
|
|
594
|
-
routes:
|
|
595
|
-
- name: realtime-v1-rest-openapi
|
|
596
|
-
strip_path: true
|
|
597
|
-
paths:
|
|
598
|
-
- /realtime/v1/api/openapi
|
|
599
|
-
plugins:
|
|
600
|
-
- name: request-termination
|
|
601
|
-
config:
|
|
602
|
-
status_code: 403
|
|
603
|
-
message: "Access is forbidden."
|
|
604
|
-
- name: realtime-v1-rest-tenants
|
|
605
|
-
url: http://realtime-dev.supabase-realtime:4000/api/tenants
|
|
606
|
-
protocol: http
|
|
607
|
-
routes:
|
|
608
|
-
- name: realtime-v1-rest-tenants
|
|
609
|
-
strip_path: true
|
|
610
|
-
paths:
|
|
611
|
-
- /realtime/v1/api/tenants
|
|
612
|
-
plugins:
|
|
613
|
-
- name: request-termination
|
|
614
|
-
config:
|
|
615
|
-
status_code: 403
|
|
616
|
-
message: "Access is forbidden."
|
|
617
|
-
- name: realtime-v1-rest
|
|
618
|
-
url: http://realtime-dev.supabase-realtime:4000/api
|
|
619
|
-
protocol: http
|
|
620
|
-
routes:
|
|
621
|
-
- name: realtime-v1-rest
|
|
622
|
-
strip_path: true
|
|
623
|
-
paths:
|
|
624
|
-
- /realtime/v1/api
|
|
625
|
-
plugins:
|
|
626
|
-
- name: cors
|
|
627
|
-
- name: key-auth
|
|
628
|
-
config:
|
|
629
|
-
hide_credentials: false
|
|
630
|
-
- name: request-transformer
|
|
631
|
-
config:
|
|
632
|
-
add:
|
|
633
|
-
headers:
|
|
634
|
-
- "Authorization: $LUA_AUTH_EXPR"
|
|
635
|
-
replace:
|
|
636
|
-
headers:
|
|
637
|
-
- "Authorization: $LUA_AUTH_EXPR"
|
|
638
|
-
- name: acl
|
|
639
|
-
config:
|
|
640
|
-
hide_groups_header: true
|
|
641
|
-
allow:
|
|
642
|
-
- admin
|
|
643
|
-
- anon
|
|
644
|
-
- name: storage-v1
|
|
645
|
-
url: http://storage:5000/
|
|
646
|
-
routes:
|
|
647
|
-
- name: storage-v1-all
|
|
648
|
-
strip_path: true
|
|
649
|
-
paths:
|
|
650
|
-
- /storage/v1/
|
|
651
|
-
plugins:
|
|
652
|
-
- name: cors
|
|
653
|
-
- name: request-transformer
|
|
654
|
-
config:
|
|
655
|
-
add:
|
|
656
|
-
headers:
|
|
657
|
-
- "Authorization: $LUA_AUTH_EXPR"
|
|
658
|
-
replace:
|
|
659
|
-
headers:
|
|
660
|
-
- "Authorization: $LUA_AUTH_EXPR"
|
|
661
|
-
- name: post-function
|
|
662
|
-
config:
|
|
663
|
-
access:
|
|
664
|
-
- |
|
|
665
|
-
local auth = kong.request.get_header("authorization")
|
|
666
|
-
if auth == nil or auth == "" or auth:find("^%s*$") then
|
|
667
|
-
kong.service.request.clear_header("authorization")
|
|
668
|
-
end
|
|
669
|
-
- name: functions-v1
|
|
670
|
-
url: http://functions:9000/
|
|
671
|
-
read_timeout: 150000
|
|
672
|
-
routes:
|
|
673
|
-
- name: functions-v1-all
|
|
674
|
-
strip_path: true
|
|
675
|
-
paths:
|
|
676
|
-
- /functions/v1/
|
|
677
|
-
plugins:
|
|
678
|
-
- name: cors
|
|
679
|
-
- name: well-known-oauth
|
|
680
|
-
url: http://auth:9999/.well-known/oauth-authorization-server
|
|
681
|
-
routes:
|
|
682
|
-
- name: well-known-oauth
|
|
683
|
-
strip_path: true
|
|
684
|
-
paths:
|
|
685
|
-
- /.well-known/oauth-authorization-server
|
|
686
|
-
plugins:
|
|
687
|
-
- name: cors
|
|
688
|
-
- name: meta
|
|
689
|
-
url: http://meta:8080/
|
|
690
|
-
routes:
|
|
691
|
-
- name: meta-all
|
|
692
|
-
strip_path: true
|
|
693
|
-
paths:
|
|
694
|
-
- /pg/
|
|
695
|
-
plugins:
|
|
696
|
-
- name: key-auth
|
|
697
|
-
config:
|
|
698
|
-
hide_credentials: false
|
|
699
|
-
- name: acl
|
|
700
|
-
config:
|
|
701
|
-
hide_groups_header: true
|
|
702
|
-
allow:
|
|
703
|
-
- admin
|
|
704
|
-
- name: dashboard
|
|
705
|
-
url: http://studio:3000/
|
|
706
|
-
routes:
|
|
707
|
-
- name: dashboard-all
|
|
708
|
-
strip_path: true
|
|
709
|
-
paths:
|
|
710
|
-
- /
|
|
711
|
-
plugins:
|
|
712
|
-
- name: cors
|
|
713
|
-
- name: basic-auth
|
|
714
|
-
config:
|
|
715
|
-
hide_credentials: true
|
|
716
|
-
`;
|
|
717
|
-
|
|
718
|
-
/** Produce the final Kong declarative config: rewrite upstream bare hostnames
|
|
719
|
-
* to the group's final service DNS names, substitute the API keys / dashboard
|
|
720
|
-
* credentials / Lua expressions, and drop the empty opaque-key lines. */
|
|
721
|
-
function buildKongYaml(
|
|
722
|
-
key: (part: string) => string,
|
|
723
|
-
vals: {
|
|
724
|
-
anonKey: string;
|
|
725
|
-
serviceRoleKey: string;
|
|
726
|
-
dashboardUsername: string;
|
|
727
|
-
dashboardPassword: string;
|
|
728
|
-
extraAnonKeys: string[];
|
|
729
|
-
extraServiceRoleKeys: string[];
|
|
730
|
-
},
|
|
731
|
-
): string {
|
|
732
|
-
let y = KONG_YML_RAW;
|
|
733
|
-
// Extra API keys land where kong-entrypoint.sh's opaque keys would: as
|
|
734
|
-
// additional keyauth credentials on the matching consumer. The template's
|
|
735
|
-
// placeholder lines are either expanded here or dropped by the filter below.
|
|
736
|
-
if (vals.extraAnonKeys.length > 0) {
|
|
737
|
-
y = y.replace(
|
|
738
|
-
" - key: $SUPABASE_PUBLISHABLE_KEY",
|
|
739
|
-
vals.extraAnonKeys.map((k) => ` - key: ${k}`).join("\n"),
|
|
740
|
-
);
|
|
741
|
-
}
|
|
742
|
-
if (vals.extraServiceRoleKeys.length > 0) {
|
|
743
|
-
y = y.replace(
|
|
744
|
-
" - key: $SUPABASE_SECRET_KEY",
|
|
745
|
-
vals.extraServiceRoleKeys.map((k) => ` - key: ${k}`).join("\n"),
|
|
746
|
-
);
|
|
747
|
-
}
|
|
748
|
-
// Rewrite upstream bare service hosts → the group's final DNS names.
|
|
749
|
-
y = y
|
|
750
|
-
.replaceAll("http://auth:9999", `http://${key("auth")}:9999`)
|
|
751
|
-
.replaceAll("http://rest:3000", `http://${key("rest")}:3000`)
|
|
752
|
-
.replaceAll("http://storage:5000", `http://${key("storage")}:5000`)
|
|
753
|
-
.replaceAll("http://meta:8080", `http://${key("meta")}:8080`)
|
|
754
|
-
.replaceAll("http://studio:3000", `http://${key("studio")}:3000`)
|
|
755
|
-
.replaceAll("http://functions:9000", `http://${key("functions")}:9000`)
|
|
756
|
-
.replaceAll("realtime-dev.supabase-realtime", `realtime-dev.${key("realtime")}`);
|
|
757
|
-
// Lua expressions first (they contain no `$SUPABASE_*` tokens).
|
|
758
|
-
const lua = buildLuaExprs([
|
|
759
|
-
...vals.extraAnonKeys.map((k) => ({ key: k, jwt: vals.anonKey })),
|
|
760
|
-
...vals.extraServiceRoleKeys.map((k) => ({ key: k, jwt: vals.serviceRoleKey })),
|
|
761
|
-
]);
|
|
762
|
-
y = y.replaceAll("$LUA_AUTH_EXPR", lua.auth).replaceAll(
|
|
763
|
-
"$LUA_RT_WS_EXPR",
|
|
764
|
-
lua.realtimeWs,
|
|
765
|
-
);
|
|
766
|
-
// API keys + dashboard credentials.
|
|
767
|
-
y = y
|
|
768
|
-
.replaceAll("$SUPABASE_ANON_KEY", vals.anonKey)
|
|
769
|
-
.replaceAll("$SUPABASE_SERVICE_KEY", vals.serviceRoleKey)
|
|
770
|
-
.replaceAll("$DASHBOARD_USERNAME", vals.dashboardUsername)
|
|
771
|
-
.replaceAll("$DASHBOARD_PASSWORD", vals.dashboardPassword);
|
|
772
|
-
// Drop the unconfigured opaque-key credential lines (kong-entrypoint.sh does
|
|
773
|
-
// the same with a `sed` after substitution).
|
|
774
|
-
y = y
|
|
775
|
-
.split("\n")
|
|
776
|
-
.filter(
|
|
777
|
-
(line) =>
|
|
778
|
-
!/^\s*- key:\s*\$SUPABASE_PUBLISHABLE_KEY\s*$/.test(line) &&
|
|
779
|
-
!/^\s*- key:\s*\$SUPABASE_SECRET_KEY\s*$/.test(line),
|
|
780
|
-
)
|
|
781
|
-
.join("\n");
|
|
782
|
-
return y;
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
// ──────────────────────────────────────────────────────────────────────────
|
|
786
|
-
// Migration application (runs in the db service's `setup`, after the DB is
|
|
787
|
-
// ready and before any dependent service starts). Uses the setup context's
|
|
788
|
-
// `exec` (docker exec with piped stdin) and `projectRoot` — no hand-rolled
|
|
789
|
-
// child_process, no hard-coded workspace path.
|
|
790
|
-
// ──────────────────────────────────────────────────────────────────────────
|
|
791
|
-
|
|
792
|
-
/** Pipe one SQL file's contents into `psql` inside the db container. Uses
|
|
793
|
-
* `psql -f -` (stdin) so multi-statement files and non-transactional DDL
|
|
794
|
-
* (e.g. `CREATE INDEX CONCURRENTLY`) behave exactly as `supabase db reset`
|
|
795
|
-
* applies them — not wrapped in a single transaction. `ON_ERROR_STOP=1` fails
|
|
796
|
-
* fast on the first bad statement. */
|
|
797
|
-
async function applySqlFile(
|
|
798
|
-
ctx: ServiceSetupContext,
|
|
799
|
-
sql: string,
|
|
800
|
-
label: string,
|
|
801
|
-
user: string,
|
|
802
|
-
): Promise<void> {
|
|
803
|
-
const res = await ctx.exec(
|
|
804
|
-
ctx.name,
|
|
805
|
-
["psql", "-v", "ON_ERROR_STOP=1", "-U", user, "-d", "postgres", "-f", "-"],
|
|
806
|
-
{ stdin: sql, timeoutMs: 300_000 },
|
|
807
|
-
);
|
|
808
|
-
if (res.exitCode !== 0) {
|
|
809
|
-
throw new Error(
|
|
810
|
-
`supabase: applying ${label} failed (psql rc=${res.exitCode}):\n${
|
|
811
|
-
res.stderr.trim() || res.stdout.trim()
|
|
812
|
-
}`,
|
|
813
|
-
);
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
/** Resolve, sort, and apply the project's Supabase migrations + seed against
|
|
818
|
-
* the running db container as the `postgres` role (matching `supabase db
|
|
819
|
-
* reset` — tables end up owned by postgres). No-op when nothing is
|
|
820
|
-
* configured/present. */
|
|
821
|
-
async function applyMigrations(
|
|
822
|
-
ctx: ServiceSetupContext,
|
|
823
|
-
cfg: { migrations: boolean | string; seed: boolean | string },
|
|
824
|
-
): Promise<void> {
|
|
825
|
-
// Migrations directory. An *explicitly configured* path that doesn't
|
|
826
|
-
// resolve is a hard error — a silent no-op here surfaces much later as a
|
|
827
|
-
// baffling `relation "public.…" does not exist`. Only the `true` default
|
|
828
|
-
// (conventional `supabase/migrations/`) is allowed to be absent.
|
|
829
|
-
if (cfg.migrations !== false) {
|
|
830
|
-
const explicit = typeof cfg.migrations === "string";
|
|
831
|
-
const dir = explicit
|
|
832
|
-
? (cfg.migrations as string).startsWith("/")
|
|
833
|
-
? (cfg.migrations as string)
|
|
834
|
-
: join(ctx.projectRoot, cfg.migrations as string)
|
|
835
|
-
: join(ctx.projectRoot, "supabase", "migrations");
|
|
836
|
-
if (existsSync(dir) && (await stat(dir)).isDirectory()) {
|
|
837
|
-
const files = (await readdir(dir))
|
|
838
|
-
.filter((f) => f.endsWith(".sql"))
|
|
839
|
-
.sort();
|
|
840
|
-
for (const f of files) {
|
|
841
|
-
const sql = await ctx.readProjectFile(join(dir, f));
|
|
842
|
-
await applySqlFile(ctx, sql, `migration ${f}`, "postgres");
|
|
843
|
-
}
|
|
844
|
-
} else if (explicit) {
|
|
845
|
-
throw new Error(
|
|
846
|
-
`supabase: configured migrations path ${JSON.stringify(
|
|
847
|
-
cfg.migrations,
|
|
848
|
-
)} does not resolve to a directory (looked at ${dir}). ` +
|
|
849
|
-
`Fix the \`migrations\` option, or remove it to use the default ` +
|
|
850
|
-
`supabase/migrations/. Note the path is resolved inside the uploaded ` +
|
|
851
|
-
`project — check it isn't ignored (e.g. via .spectestignore).`,
|
|
852
|
-
);
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
|
-
// Seed file — same contract: only the conventional default may be absent.
|
|
856
|
-
if (cfg.seed !== false) {
|
|
857
|
-
const explicit = typeof cfg.seed === "string";
|
|
858
|
-
const seedPath = explicit
|
|
859
|
-
? (cfg.seed as string).startsWith("/")
|
|
860
|
-
? (cfg.seed as string)
|
|
861
|
-
: join(ctx.projectRoot, cfg.seed as string)
|
|
862
|
-
: join(ctx.projectRoot, "supabase", "seed.sql");
|
|
863
|
-
if (existsSync(seedPath)) {
|
|
864
|
-
const sql = await ctx.readProjectFile(seedPath);
|
|
865
|
-
await applySqlFile(ctx, sql, "seed.sql", "postgres");
|
|
866
|
-
} else if (explicit) {
|
|
867
|
-
throw new Error(
|
|
868
|
-
`supabase: configured seed path ${JSON.stringify(cfg.seed)} not found ` +
|
|
869
|
-
`(looked at ${seedPath}). Fix the \`seed\` option or remove it to ` +
|
|
870
|
-
`use the default supabase/seed.sql.`,
|
|
871
|
-
);
|
|
872
|
-
}
|
|
873
|
-
}
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
/**
|
|
877
|
-
* A ready-to-use self-hosted Supabase stack. Mount `.group` at the key
|
|
878
|
-
* matching the `name` option (default `"supabase"`) and wire the app with
|
|
879
|
-
* `.appEnv` / `.ready`:
|
|
880
|
-
*
|
|
881
|
-
* ```ts
|
|
882
|
-
* const sb = supabase({ dbPassword: "postgres" });
|
|
883
|
-
* services: { supabase: sb.group, app: { image, env: sb.appEnv, dependsOn: [sb.ready] } }
|
|
884
|
-
* ```
|
|
885
|
-
*
|
|
886
|
-
* See the file header for the full picture. Tests get an instrumented DB client
|
|
887
|
-
* at `ctx.svc.<name>.sql` plus `sb.url` / `sb.anonKey` / `sb.serviceRoleKey`
|
|
888
|
-
* for hitting the REST/auth/storage APIs through the gateway.
|
|
889
|
-
*/
|
|
890
|
-
export function supabase(opts: SupabaseOptions = {}): SupabaseStack {
|
|
891
|
-
const name = opts.name ?? "supabase";
|
|
892
|
-
const dbPassword = opts.dbPassword ?? "postgres";
|
|
893
|
-
const jwtSecret = opts.jwtSecret ?? DEFAULT_JWT_SECRET;
|
|
894
|
-
if (jwtSecret.length < 32) {
|
|
895
|
-
throw new Error(
|
|
896
|
-
`supabase(): jwtSecret must be at least 32 characters (got ${jwtSecret.length})`,
|
|
897
|
-
);
|
|
898
|
-
}
|
|
899
|
-
const dashboardUsername = opts.dashboardUsername ?? "supabase";
|
|
900
|
-
const dashboardPassword =
|
|
901
|
-
opts.dashboardPassword ?? "this_password_is_insecure_and_should_be_updated";
|
|
902
|
-
|
|
903
|
-
const withAuth = opts.auth ?? true;
|
|
904
|
-
const withStorage = opts.storage ?? true;
|
|
905
|
-
const withRealtime = opts.realtime ?? true;
|
|
906
|
-
const withStudio = opts.studio ?? false;
|
|
907
|
-
const withMeta = opts.meta ?? withStudio;
|
|
908
|
-
|
|
909
|
-
// Mail capture (see SupabaseOptions.mail) — on unless explicitly disabled.
|
|
910
|
-
// `mailKey` is the SMTP host GoTrue mails through — the in-group part
|
|
911
|
-
// (`<name>-mail`) or a user-declared `email()` service shared with the app
|
|
912
|
-
// under test.
|
|
913
|
-
const withMail = opts.mail !== false;
|
|
914
|
-
const mailOpts: SupabaseMailOptions =
|
|
915
|
-
typeof opts.mail === "object" ? opts.mail : {};
|
|
916
|
-
const mailExternal = mailOpts.service !== undefined;
|
|
917
|
-
const mailKey = mailOpts.service ?? `${name}-mail`;
|
|
918
|
-
const mailAutoconfirm = mailOpts.autoconfirm ?? true;
|
|
919
|
-
|
|
920
|
-
const extraAnonKeys = opts.extraApiKeys?.anon ?? [];
|
|
921
|
-
const extraServiceRoleKeys = opts.extraApiKeys?.serviceRole ?? [];
|
|
922
|
-
for (const k of [...extraAnonKeys, ...extraServiceRoleKeys]) {
|
|
923
|
-
// The keys are spliced into Kong's YAML and its quoted Lua expressions —
|
|
924
|
-
// restrict to the token charset real API keys use so neither can break.
|
|
925
|
-
if (!/^[A-Za-z0-9._-]+$/.test(k)) {
|
|
926
|
-
throw new Error(
|
|
927
|
-
`supabase(): extraApiKeys entries must match [A-Za-z0-9._-]+ (got ${JSON.stringify(k)})`,
|
|
928
|
-
);
|
|
929
|
-
}
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
const anonKey = apiKey("anon", jwtSecret);
|
|
933
|
-
const serviceRoleKey = apiKey("service_role", jwtSecret);
|
|
934
|
-
|
|
935
|
-
// Derived surface, embedded in `appEnv` / `url` before the group expands.
|
|
936
|
-
// `expectKey: name` on the group guarantees these names and the expanded
|
|
937
|
-
// service keys agree (`g.key("db")` at expansion === `${name}-db` here).
|
|
938
|
-
const dbKey = `${name}-db`;
|
|
939
|
-
const url = opts.hostname ? `https://${opts.hostname}` : `http://${name}:8000`;
|
|
940
|
-
// App-facing URL: the conventional `postgres` role (owns migrated tables).
|
|
941
|
-
const dbUrl = `postgresql://postgres:${dbPassword}@${dbKey}:5432/postgres`;
|
|
942
|
-
// The test-side `ctx.svc.<name>.sql` helper connects as the superuser
|
|
943
|
-
// `supabase_admin` so it can inspect everything — public tables plus the
|
|
944
|
-
// `auth`/`storage` schemas — regardless of ownership.
|
|
945
|
-
const adminDbUrl = `postgresql://supabase_admin:${dbPassword}@${dbKey}:5432/postgres`;
|
|
946
|
-
|
|
947
|
-
const group = serviceGroup({
|
|
948
|
-
// Kong: the group key itself is the gateway (`http://<name>:8000`), and
|
|
949
|
-
// as the primary it's the group's sink — `dependsOn: [sb.ready]` from
|
|
950
|
-
// outside waits for the whole stack.
|
|
951
|
-
primary: "gateway",
|
|
952
|
-
expectKey: name,
|
|
953
|
-
services: (g) => {
|
|
954
|
-
const db = g.key("db");
|
|
955
|
-
// Realtime derives its tenant id from the host subdomain, so it must
|
|
956
|
-
// be reachable at `realtime-dev.<realtime key>` — the host Kong
|
|
957
|
-
// routes to.
|
|
958
|
-
const realtimeHost = `realtime-dev.${g.key("realtime")}`;
|
|
959
|
-
const parts: Record<string, ServiceConfig> = {};
|
|
960
|
-
|
|
961
|
-
// ── db ──────────────────────────────────────────────────────────
|
|
962
|
-
const dbDef: ServiceDefinition = {
|
|
963
|
-
image: { type: "registry", reference: IMAGES.db },
|
|
964
|
-
// `args` (not `command`): keep the image's docker-entrypoint.sh so it
|
|
965
|
-
// runs initdb + its baked init-scripts before starting.
|
|
966
|
-
args: [
|
|
967
|
-
"postgres",
|
|
968
|
-
"-c",
|
|
969
|
-
"config_file=/etc/postgresql/postgresql.conf",
|
|
970
|
-
"-c",
|
|
971
|
-
"log_min_messages=fatal",
|
|
972
|
-
],
|
|
973
|
-
env: {
|
|
974
|
-
POSTGRES_HOST: "/var/run/postgresql",
|
|
975
|
-
PGPORT: "5432",
|
|
976
|
-
POSTGRES_PORT: "5432",
|
|
977
|
-
PGPASSWORD: dbPassword,
|
|
978
|
-
POSTGRES_PASSWORD: dbPassword,
|
|
979
|
-
PGDATABASE: "postgres",
|
|
980
|
-
POSTGRES_DB: "postgres",
|
|
981
|
-
JWT_SECRET: jwtSecret,
|
|
982
|
-
JWT_EXP: "3600",
|
|
983
|
-
},
|
|
984
|
-
ports: [5432],
|
|
985
|
-
// `pg_isready` succeeds only once the image finishes initdb + its
|
|
986
|
-
// baked init-scripts and starts serving for real.
|
|
987
|
-
readyCheck: { type: "exec", command: "pg_isready -U postgres -h 127.0.0.1 -p 5432", timeoutSecs: 180 },
|
|
988
|
-
setup: async (ctx) => {
|
|
989
|
-
// Bootstrap first (role passwords / JWT GUC / _realtime schema),
|
|
990
|
-
// then the project's own migrations + seed — both before any
|
|
991
|
-
// dependent service.
|
|
992
|
-
await applySqlFile(
|
|
993
|
-
ctx,
|
|
994
|
-
buildBootstrapSql(dbPassword, jwtSecret, withRealtime),
|
|
995
|
-
"supabase bootstrap",
|
|
996
|
-
// The bootstrap alters reserved roles (authenticator, …) and the
|
|
997
|
-
// database — superuser-only. `postgres` is deliberately NOT a
|
|
998
|
-
// superuser in modern supabase/postgres; `supabase_admin` is.
|
|
999
|
-
"supabase_admin",
|
|
1000
|
-
);
|
|
1001
|
-
await applyMigrations(ctx, {
|
|
1002
|
-
migrations: opts.migrations ?? true,
|
|
1003
|
-
seed: opts.seed ?? true,
|
|
1004
|
-
});
|
|
1005
|
-
},
|
|
1006
|
-
};
|
|
1007
|
-
parts.db = dbDef;
|
|
1008
|
-
|
|
1009
|
-
// ── rest (PostgREST) ────────────────────────────────────────────
|
|
1010
|
-
parts.rest = {
|
|
1011
|
-
image: { type: "registry", reference: IMAGES.rest },
|
|
1012
|
-
env: {
|
|
1013
|
-
PGRST_DB_URI: `postgres://authenticator:${dbPassword}@${db}:5432/postgres`,
|
|
1014
|
-
PGRST_DB_SCHEMAS: "public,graphql_public",
|
|
1015
|
-
PGRST_DB_ANON_ROLE: "anon",
|
|
1016
|
-
PGRST_JWT_SECRET: jwtSecret,
|
|
1017
|
-
PGRST_DB_USE_LEGACY_GUCS: "false",
|
|
1018
|
-
PGRST_APP_SETTINGS_JWT_SECRET: jwtSecret,
|
|
1019
|
-
PGRST_APP_SETTINGS_JWT_EXP: "3600",
|
|
1020
|
-
PGRST_ADMIN_SERVER_PORT: "3001",
|
|
1021
|
-
PGRST_DB_MAX_ROWS: "1000",
|
|
1022
|
-
PGRST_DB_EXTRA_SEARCH_PATH: "public",
|
|
1023
|
-
},
|
|
1024
|
-
ports: [3000],
|
|
1025
|
-
dependsOn: ["db"],
|
|
1026
|
-
// PostgREST opens :3000 only after connecting to the DB and loading
|
|
1027
|
-
// the schema cache, so a TCP connect is a good ready signal.
|
|
1028
|
-
readyCheck: { type: "tcp", port: 3000, timeoutSecs: 120 },
|
|
1029
|
-
};
|
|
1030
|
-
|
|
1031
|
-
// ── mail (SMTP capture, the standard email() component) ─────────
|
|
1032
|
-
if (withMail && !mailExternal) {
|
|
1033
|
-
parts.mail = email();
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
// ── auth (GoTrue) ───────────────────────────────────────────────
|
|
1037
|
-
if (withAuth) {
|
|
1038
|
-
const apiExternalUrl = `${url}/auth/v1`;
|
|
1039
|
-
parts.auth = {
|
|
1040
|
-
image: { type: "registry", reference: IMAGES.auth },
|
|
1041
|
-
env: {
|
|
1042
|
-
GOTRUE_API_HOST: "0.0.0.0",
|
|
1043
|
-
GOTRUE_API_PORT: "9999",
|
|
1044
|
-
API_EXTERNAL_URL: apiExternalUrl,
|
|
1045
|
-
GOTRUE_DB_DRIVER: "postgres",
|
|
1046
|
-
GOTRUE_DB_DATABASE_URL: `postgres://supabase_auth_admin:${dbPassword}@${db}:5432/postgres`,
|
|
1047
|
-
GOTRUE_SITE_URL: url,
|
|
1048
|
-
GOTRUE_URI_ALLOW_LIST: "*",
|
|
1049
|
-
GOTRUE_DISABLE_SIGNUP: "false",
|
|
1050
|
-
GOTRUE_JWT_ADMIN_ROLES: "service_role",
|
|
1051
|
-
GOTRUE_JWT_AUD: "authenticated",
|
|
1052
|
-
GOTRUE_JWT_DEFAULT_GROUP_NAME: "authenticated",
|
|
1053
|
-
GOTRUE_JWT_EXP: "3600",
|
|
1054
|
-
GOTRUE_JWT_SECRET: jwtSecret,
|
|
1055
|
-
GOTRUE_JWT_ISSUER: apiExternalUrl,
|
|
1056
|
-
GOTRUE_EXTERNAL_EMAIL_ENABLED: "true",
|
|
1057
|
-
GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED: "true",
|
|
1058
|
-
// Autoconfirm defaults ON (one-step signups, no confirmation
|
|
1059
|
-
// mail) so the always-on mailbox is purely additive; the mail
|
|
1060
|
-
// option's `autoconfirm: false` opts into real confirmation
|
|
1061
|
-
// flows.
|
|
1062
|
-
GOTRUE_MAILER_AUTOCONFIRM:
|
|
1063
|
-
withMail && !mailAutoconfirm ? "false" : "true",
|
|
1064
|
-
GOTRUE_EXTERNAL_PHONE_ENABLED: "false",
|
|
1065
|
-
GOTRUE_SMS_AUTOCONFIRM: "true",
|
|
1066
|
-
...(withMail
|
|
1067
|
-
? {
|
|
1068
|
-
GOTRUE_SMTP_HOST: mailKey,
|
|
1069
|
-
GOTRUE_SMTP_PORT: "1025",
|
|
1070
|
-
// Deliberately NO GOTRUE_SMTP_USER/PASS: the capture
|
|
1071
|
-
// server advertises AUTH, and with credentials set
|
|
1072
|
-
// GoTrue's Go smtp.PlainAuth refuses to send them over a
|
|
1073
|
-
// plaintext connection ("unencrypted connection" → every
|
|
1074
|
-
// mail 500s). Unauthenticated submission is accepted.
|
|
1075
|
-
GOTRUE_SMTP_ADMIN_EMAIL:
|
|
1076
|
-
mailOpts.adminEmail ?? "admin@example.com",
|
|
1077
|
-
GOTRUE_SMTP_SENDER_NAME: mailOpts.senderName ?? "Supabase",
|
|
1078
|
-
// GoTrue rate-limits repeat mail to the same address
|
|
1079
|
-
// (default 1/min) — far too slow for tests that
|
|
1080
|
-
// request an OTP, assert, and request again.
|
|
1081
|
-
GOTRUE_SMTP_MAX_FREQUENCY: "1s",
|
|
1082
|
-
GOTRUE_MAILER_URLPATHS_INVITE: "/auth/v1/verify",
|
|
1083
|
-
GOTRUE_MAILER_URLPATHS_CONFIRMATION: "/auth/v1/verify",
|
|
1084
|
-
GOTRUE_MAILER_URLPATHS_RECOVERY: "/auth/v1/verify",
|
|
1085
|
-
GOTRUE_MAILER_URLPATHS_EMAIL_CHANGE: "/auth/v1/verify",
|
|
1086
|
-
}
|
|
1087
|
-
: {}),
|
|
1088
|
-
},
|
|
1089
|
-
ports: [9999],
|
|
1090
|
-
dependsOn: ["db", ...(withMail && !mailExternal ? ["mail"] : [])],
|
|
1091
|
-
readyCheck: { type: "http", port: 9999, path: "/health", timeoutSecs: 90 },
|
|
1092
|
-
};
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
// ── storage (+ imgproxy) ────────────────────────────────────────
|
|
1096
|
-
if (withStorage) {
|
|
1097
|
-
// Named shared volume so imgproxy can read what storage-api wrote —
|
|
1098
|
-
// one backing dir mounted by both containers, snapshot/fork- and
|
|
1099
|
-
// teardown-managed like any other volume.
|
|
1100
|
-
const storageData = { name: `${g.name}-storage-data`, target: "/var/lib/storage" };
|
|
1101
|
-
parts.imgproxy = {
|
|
1102
|
-
image: { type: "registry", reference: IMAGES.imgproxy },
|
|
1103
|
-
env: {
|
|
1104
|
-
IMGPROXY_BIND: ":5001",
|
|
1105
|
-
IMGPROXY_LOCAL_FILESYSTEM_ROOT: "/",
|
|
1106
|
-
IMGPROXY_USE_ETAG: "true",
|
|
1107
|
-
IMGPROXY_AUTO_WEBP: "true",
|
|
1108
|
-
IMGPROXY_MAX_SRC_RESOLUTION: "16.8",
|
|
1109
|
-
},
|
|
1110
|
-
volumes: [storageData],
|
|
1111
|
-
ports: [5001],
|
|
1112
|
-
readyCheck: { type: "tcp", port: 5001, timeoutSecs: 60 },
|
|
1113
|
-
};
|
|
1114
|
-
parts.storage = {
|
|
1115
|
-
image: { type: "registry", reference: IMAGES.storage },
|
|
1116
|
-
env: {
|
|
1117
|
-
ANON_KEY: anonKey,
|
|
1118
|
-
SERVICE_KEY: serviceRoleKey,
|
|
1119
|
-
POSTGREST_URL: `http://${g.key("rest")}:3000`,
|
|
1120
|
-
AUTH_JWT_SECRET: jwtSecret,
|
|
1121
|
-
DATABASE_URL: `postgres://supabase_storage_admin:${dbPassword}@${db}:5432/postgres`,
|
|
1122
|
-
STORAGE_PUBLIC_URL: url,
|
|
1123
|
-
REQUEST_ALLOW_X_FORWARDED_PATH: "true",
|
|
1124
|
-
FILE_SIZE_LIMIT: "52428800",
|
|
1125
|
-
STORAGE_BACKEND: "file",
|
|
1126
|
-
GLOBAL_S3_BUCKET: "stub",
|
|
1127
|
-
FILE_STORAGE_BACKEND_PATH: "/var/lib/storage",
|
|
1128
|
-
TENANT_ID: "stub",
|
|
1129
|
-
REGION: "stub",
|
|
1130
|
-
ENABLE_IMAGE_TRANSFORMATION: "true",
|
|
1131
|
-
IMGPROXY_URL: `http://${g.key("imgproxy")}:5001`,
|
|
1132
|
-
S3_PROTOCOL_ACCESS_KEY_ID: "625729a08b95bf1b7ff351a663f3a23c",
|
|
1133
|
-
S3_PROTOCOL_ACCESS_KEY_SECRET:
|
|
1134
|
-
"850181e4652dd023b7a98c58ae0d2d34bd487ee0cc3254aed6eda37307425907",
|
|
1135
|
-
},
|
|
1136
|
-
volumes: [storageData],
|
|
1137
|
-
ports: [5000],
|
|
1138
|
-
dependsOn: ["db", "rest", "imgproxy"],
|
|
1139
|
-
readyCheck: { type: "http", port: 5000, path: "/status", timeoutSecs: 90 },
|
|
1140
|
-
};
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1143
|
-
// ── realtime ────────────────────────────────────────────────────
|
|
1144
|
-
if (withRealtime) {
|
|
1145
|
-
parts.realtime = {
|
|
1146
|
-
image: { type: "registry", reference: IMAGES.realtime },
|
|
1147
|
-
// Extra peer alias so Kong (and the tenant-subdomain parser)
|
|
1148
|
-
// reaches it at `realtime-dev.<realtime key>`.
|
|
1149
|
-
hostnames: [realtimeHost],
|
|
1150
|
-
env: {
|
|
1151
|
-
PORT: "4000",
|
|
1152
|
-
DB_HOST: db,
|
|
1153
|
-
DB_PORT: "5432",
|
|
1154
|
-
DB_USER: "supabase_admin",
|
|
1155
|
-
DB_PASSWORD: dbPassword,
|
|
1156
|
-
DB_NAME: "postgres",
|
|
1157
|
-
DB_AFTER_CONNECT_QUERY: "SET search_path TO _realtime",
|
|
1158
|
-
DB_ENC_KEY: "supabaserealtime",
|
|
1159
|
-
API_JWT_SECRET: jwtSecret,
|
|
1160
|
-
METRICS_JWT_SECRET: jwtSecret,
|
|
1161
|
-
SECRET_KEY_BASE:
|
|
1162
|
-
"UpNVntn3cDxHJpq99YMc1T1AQgQpc8kfYTuRgBiYa15BLrx8etQoXz3gZv1/u2oq",
|
|
1163
|
-
ERL_AFLAGS: "-proto_dist inet_tcp",
|
|
1164
|
-
DNS_NODES: "''",
|
|
1165
|
-
RLIMIT_NOFILE: "10000",
|
|
1166
|
-
APP_NAME: "realtime",
|
|
1167
|
-
SEED_SELF_HOST: "true",
|
|
1168
|
-
RUN_JANITOR: "true",
|
|
1169
|
-
DISABLE_HEALTHCHECK_LOGGING: "true",
|
|
1170
|
-
},
|
|
1171
|
-
ports: [4000],
|
|
1172
|
-
dependsOn: ["db"],
|
|
1173
|
-
// The health endpoint wants a valid JWT — `headers` keeps the
|
|
1174
|
-
// probe image-agnostic (no curl-in-image dependency).
|
|
1175
|
-
readyCheck: {
|
|
1176
|
-
type: "http",
|
|
1177
|
-
port: 4000,
|
|
1178
|
-
path: "/api/tenants/realtime-dev/health",
|
|
1179
|
-
headers: { Authorization: `Bearer ${anonKey}` },
|
|
1180
|
-
timeoutSecs: 120,
|
|
1181
|
-
},
|
|
1182
|
-
};
|
|
1183
|
-
}
|
|
1184
|
-
|
|
1185
|
-
// ── meta (postgres-meta) ────────────────────────────────────────
|
|
1186
|
-
if (withMeta) {
|
|
1187
|
-
parts.meta = {
|
|
1188
|
-
image: { type: "registry", reference: IMAGES.meta },
|
|
1189
|
-
env: {
|
|
1190
|
-
PG_META_PORT: "8080",
|
|
1191
|
-
PG_META_DB_HOST: db,
|
|
1192
|
-
PG_META_DB_PORT: "5432",
|
|
1193
|
-
PG_META_DB_NAME: "postgres",
|
|
1194
|
-
PG_META_DB_USER: "postgres",
|
|
1195
|
-
PG_META_DB_PASSWORD: dbPassword,
|
|
1196
|
-
CRYPTO_KEY: "your-encryption-key-32-chars-min",
|
|
1197
|
-
},
|
|
1198
|
-
ports: [8080],
|
|
1199
|
-
dependsOn: ["db"],
|
|
1200
|
-
readyCheck: { type: "tcp", port: 8080, timeoutSecs: 90 },
|
|
1201
|
-
};
|
|
1202
|
-
}
|
|
1203
|
-
|
|
1204
|
-
// ── studio ──────────────────────────────────────────────────────
|
|
1205
|
-
if (withStudio) {
|
|
1206
|
-
parts.studio = {
|
|
1207
|
-
image: { type: "registry", reference: IMAGES.studio },
|
|
1208
|
-
env: {
|
|
1209
|
-
HOSTNAME: "0.0.0.0",
|
|
1210
|
-
STUDIO_PG_META_URL: `http://${g.key("meta")}:8080`,
|
|
1211
|
-
POSTGRES_PORT: "5432",
|
|
1212
|
-
POSTGRES_HOST: db,
|
|
1213
|
-
POSTGRES_DB: "postgres",
|
|
1214
|
-
POSTGRES_PASSWORD: dbPassword,
|
|
1215
|
-
POSTGRES_USER_READ_WRITE: "postgres",
|
|
1216
|
-
PG_META_CRYPTO_KEY: "your-encryption-key-32-chars-min",
|
|
1217
|
-
PGRST_DB_SCHEMAS: "public,graphql_public",
|
|
1218
|
-
DEFAULT_ORGANIZATION_NAME: "Default Organization",
|
|
1219
|
-
DEFAULT_PROJECT_NAME: "Default Project",
|
|
1220
|
-
SUPABASE_URL: `http://${g.name}:8000`,
|
|
1221
|
-
SUPABASE_PUBLIC_URL: url,
|
|
1222
|
-
SUPABASE_ANON_KEY: anonKey,
|
|
1223
|
-
SUPABASE_SERVICE_KEY: serviceRoleKey,
|
|
1224
|
-
AUTH_JWT_SECRET: jwtSecret,
|
|
1225
|
-
},
|
|
1226
|
-
ports: [3000],
|
|
1227
|
-
dependsOn: ["meta"],
|
|
1228
|
-
readyCheck: {
|
|
1229
|
-
type: "http",
|
|
1230
|
-
port: 3000,
|
|
1231
|
-
path: "/api/platform/profile",
|
|
1232
|
-
timeoutSecs: 180,
|
|
1233
|
-
},
|
|
1234
|
-
};
|
|
1235
|
-
}
|
|
1236
|
-
|
|
1237
|
-
// ── kong (gateway / primary) — no explicit dependsOn: the group
|
|
1238
|
-
// expansion makes the primary depend on every member ──────────────
|
|
1239
|
-
parts.gateway = {
|
|
1240
|
-
image: { type: "registry", reference: IMAGES.kong },
|
|
1241
|
-
env: {
|
|
1242
|
-
KONG_DATABASE: "off",
|
|
1243
|
-
KONG_DECLARATIVE_CONFIG: "/home/kong/kong.yml",
|
|
1244
|
-
KONG_ROUTER_FLAVOR: "expressions",
|
|
1245
|
-
KONG_DNS_ORDER: "LAST,A,CNAME",
|
|
1246
|
-
KONG_DNS_NOT_FOUND_TTL: "1",
|
|
1247
|
-
KONG_PLUGINS:
|
|
1248
|
-
"request-transformer,cors,key-auth,acl,basic-auth,request-termination,ip-restriction,post-function",
|
|
1249
|
-
KONG_NGINX_PROXY_PROXY_BUFFER_SIZE: "160k",
|
|
1250
|
-
KONG_NGINX_PROXY_PROXY_BUFFERS: "64 160k",
|
|
1251
|
-
},
|
|
1252
|
-
files: [
|
|
1253
|
-
{
|
|
1254
|
-
path: "/home/kong/kong.yml",
|
|
1255
|
-
content: buildKongYaml(g.key, {
|
|
1256
|
-
anonKey,
|
|
1257
|
-
serviceRoleKey,
|
|
1258
|
-
dashboardUsername,
|
|
1259
|
-
dashboardPassword,
|
|
1260
|
-
extraAnonKeys,
|
|
1261
|
-
extraServiceRoleKeys,
|
|
1262
|
-
}),
|
|
1263
|
-
},
|
|
1264
|
-
],
|
|
1265
|
-
ports: [8000, 8443],
|
|
1266
|
-
...(opts.hostname ? { tls: [{ hostname: opts.hostname, port: 8000 }] } : {}),
|
|
1267
|
-
readyCheck: { type: "exec", command: "kong health", timeoutSecs: 90 },
|
|
1268
|
-
};
|
|
1269
|
-
|
|
1270
|
-
return parts;
|
|
1271
|
-
},
|
|
1272
|
-
// The group's consolidated handle at `ctx.svc.<name>`.
|
|
1273
|
-
helpers: (): SupabaseHelpers => ({
|
|
1274
|
-
sql: new SQL(adminDbUrl, { label: name }),
|
|
1275
|
-
url,
|
|
1276
|
-
anonKey,
|
|
1277
|
-
serviceRoleKey,
|
|
1278
|
-
...(withMail ? { mail: emailHelpers(mailKey) } : {}),
|
|
1279
|
-
}),
|
|
1280
|
-
});
|
|
1281
|
-
|
|
1282
|
-
const appEnv = {
|
|
1283
|
-
SUPABASE_URL: url,
|
|
1284
|
-
SUPABASE_ANON_KEY: anonKey,
|
|
1285
|
-
SUPABASE_SERVICE_ROLE_KEY: serviceRoleKey,
|
|
1286
|
-
DATABASE_URL: dbUrl,
|
|
1287
|
-
};
|
|
1288
|
-
|
|
1289
|
-
return {
|
|
1290
|
-
group,
|
|
1291
|
-
url,
|
|
1292
|
-
anonKey,
|
|
1293
|
-
serviceRoleKey,
|
|
1294
|
-
dbUrl,
|
|
1295
|
-
ready: name,
|
|
1296
|
-
appEnv,
|
|
1297
|
-
...(withMail ? { smtp: { host: mailKey, port: 1025 } } : {}),
|
|
1298
|
-
};
|
|
1299
|
-
}
|