@r8s/supabase 0.2.0 → 0.3.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/src/index.tsx DELETED
@@ -1,454 +0,0 @@
1
- import { jsx, Fragment, useContext } from '@r8s/core'
2
- import { SecretContext } from '@r8s/core/defaults'
3
- import { Database, WebService, Endpoint } from '@r8s/recipes'
4
-
5
- export interface SupabaseProps {
6
- /** Resource name — base for every derived resource (defaults to 'supabase') */
7
- name?: string
8
- /** Kubernetes namespace (defaults to 'default') */
9
- namespace?: string
10
- /** Public hostname for the REST API root — PostgREST (required) */
11
- host: string
12
- /** Replicas per service (defaults to 1; PostgREST/GoTrue scale horizontally) */
13
- replicas?: number
14
- /** Postgres cluster storage size for the Database core (defaults to '10Gi') */
15
- storage?: string
16
- /**
17
- * Render the Storage API service (defaults to true). Set false to run a
18
- * minimal auth + REST-only Supabase. The S3 objectStorage prop is always
19
- * required so a bucket is declared for the platform.
20
- */
21
- storageApi?: boolean
22
- /**
23
- * S3-compatible object storage for the Storage API (RustFS in the platform).
24
- * Reference a bucket whose credentials live in a Secret provisioned by
25
- * the secrets backend (keys: accessKey, secretKey) — never plaintext.
26
- */
27
- objectStorage: {
28
- /** S3 endpoint URL, e.g. https://s3.internal.example.com */
29
- endpoint: string
30
- /** Bucket for uploads and stored files */
31
- bucket: string
32
- /** Name of the Secret holding accessKey / secretKey */
33
- credentialsSecret: string
34
- }
35
- /**
36
- * S3 region reported to the Storage API (GLOBAL_S3_REGION, defaults to
37
- * 'us-east-1'). For S3-compatible stores like RustFS any consistent
38
- * region works — keep it aligned with the provider's default.
39
- */
40
- region?: string
41
- /**
42
- * Name of an existing Secret holding the Supabase JWT bundle with keys
43
- * `jwtSecret`, `anonKey`, `serviceRoleKey` and `referrerURLs`. Required
44
- * unless a secrets backend (openbao/vault) is configured on the
45
- * surrounding Platform — the backend then provisions the bundle at
46
- * path `<path>/<name>/jwt`. Plaintext JWT secrets are not supported.
47
- */
48
- jwtSecretsName?: string
49
- /**
50
- * Additional redirect URLs GoTrue may send users to after signup,
51
- * magic-link or OAuth flows (GOTRUE_URI_ALLOW_LIST). The site URL is
52
- * always allowed; pass a list (joined with ',') or a pre-joined string.
53
- */
54
- uriAllowList?: string | string[]
55
- /** Requested resources — applied to every service in the suite */
56
- resources?: {
57
- requests?: { cpu?: string; memory?: string }
58
- limits?: { cpu?: string; memory?: string }
59
- }
60
- /** TLS configuration (defaults to letsencrypt-prod cluster issuer) */
61
- tls?: {
62
- secretName: string
63
- clusterIssuer: string
64
- }
65
- }
66
-
67
- /**
68
- * Supabase — open-source Firebase alternative: auth, REST, realtime and
69
- * storage on Postgres. This is the Supabase backend platform, NOT Apache
70
- * Superset (which ships in the separate r8s/superset package).
71
- *
72
- * @title Supabase
73
- * @category Backend Platforms
74
- *
75
- * Composes:
76
- * - CNPG Postgres cluster (the Supabase database core) bootstrapped with
77
- * the Supabase roles (anon, authenticated, service_role, authenticator),
78
- * extensions (pgcrypto, pgjwt) and schemas (auth, storage, realtime)
79
- * - GoTrue auth service (`${name}-gotrue`, port 9999)
80
- * - PostgREST REST API (`${name}-postgrest`, port 3000)
81
- * - Realtime websockets service (`${name}-realtime`, port 4000)
82
- * - Storage API on S3/RustFS (`${name}-storage-api`, port 5000)
83
- * - ImgProxy image transform service (`${name}-imgproxy`, port 8080)
84
- * - Endpoints on one host: `/rest/v1` (PostgREST), `/auth/v1` (GoTrue),
85
- * `/realtime/v1` (Realtime, with WebSocket-friendly nginx annotations)
86
- * and `/storage/v1` (Storage API), plus the REST root at `/`
87
- * - JWT bundle (jwtSecret, anonKey, serviceRoleKey, referrerURLs)
88
- * provisioned through the Platform secrets backend (openbao / vault),
89
- * or referenced from an existing Secret
90
- *
91
- * Wrap the component in `<Platform secrets={{ backend: 'openbao' }}>` and
92
- * the `${name}-jwt` bundle is provisioned for you. Without a backend you
93
- * must point `jwtSecretsName` at a pre-created Secret.
94
- *
95
- * @example
96
- * import { Platform } from '@r8s/recipes'
97
- * import { Supabase } from '@r8s/supabase'
98
- *
99
- * export default (
100
- * <Platform secrets={{ backend: 'openbao', mount: 'kv', path: 'apps' }}>
101
- * <Supabase
102
- * name="backend"
103
- * host="backend.example.com"
104
- * objectStorage={{
105
- * endpoint: 'https://s3.internal.example.com',
106
- * bucket: 'backend-uploads',
107
- * credentialsSecret: 'backend-object-store-credentials',
108
- * }}
109
- * />
110
- * </Platform>
111
- * )
112
- *
113
- * @example
114
- * // Reference a pre-created JWT bundle instead of provisioning one
115
- * import { Supabase } from '@r8s/supabase'
116
- *
117
- * export default (
118
- * <Supabase
119
- * host="backend.example.com"
120
- * jwtSecretsName="backend-jwt"
121
- * objectStorage={{
122
- * endpoint: 'https://s3.internal.example.com',
123
- * bucket: 'backend-uploads',
124
- * credentialsSecret: 'backend-object-store-credentials',
125
- * }}
126
- * />
127
- * )
128
- */
129
- export function Supabase(props: SupabaseProps) {
130
- const {
131
- name = 'supabase',
132
- namespace = 'default',
133
- host,
134
- replicas = 1,
135
- storage = '10Gi',
136
- storageApi = true,
137
- region = 'us-east-1',
138
- objectStorage,
139
- jwtSecretsName,
140
- uriAllowList,
141
- resources = {
142
- requests: { memory: '512Mi', cpu: '250m' },
143
- limits: { memory: '2Gi', cpu: '1000m' },
144
- },
145
- tls = { secretName: `${name}-tls`, clusterIssuer: 'letsencrypt-prod' },
146
- } = props
147
-
148
- const secretProvider = useContext(SecretContext)
149
- const resources_: ReturnType<typeof jsx>[] = []
150
-
151
- const dbHost = `${name}-rw`
152
- const dbCredentialsName = `${name}-db-credentials`
153
- const jwtBundleName = jwtSecretsName ?? `${name}-jwt`
154
- // Static parts only — the password arrives via $(PGPASSWORD), which every
155
- // service declares through secretKeyRef (WebService resolves secrets
156
- // before plain env, so Kubernetes dependent expansion sees it).
157
- const dbUri = `postgresql://${name}:$(PGPASSWORD)@${dbHost}:5432/${name}`
158
-
159
- // --- Bootstrap SQL ---------------------------------------------------------
160
- // Minimal Supabase bootstrap applied once by CNPG after initdb
161
- // (bootstrap.initdb.postInitApplicationSQL). Every statement is
162
- // idempotent where the SQL grammar allows it.
163
- const postInitSQL = [
164
- 'CREATE EXTENSION IF NOT EXISTS pgcrypto;',
165
- 'CREATE EXTENSION IF NOT EXISTS pgjwt;',
166
- 'CREATE ROLE anon NOLOGIN;',
167
- 'CREATE ROLE authenticated NOLOGIN;',
168
- 'CREATE ROLE service_role NOLOGIN;',
169
- 'CREATE ROLE authenticator NOLOGIN;',
170
- 'GRANT USAGE ON SCHEMA public TO anon, authenticated, service_role;',
171
- 'CREATE SCHEMA IF NOT EXISTS auth;',
172
- 'CREATE SCHEMA IF NOT EXISTS storage;',
173
- 'CREATE SCHEMA IF NOT EXISTS realtime;',
174
- 'GRANT USAGE ON SCHEMA auth TO anon, authenticated, service_role;',
175
- 'GRANT USAGE ON SCHEMA storage TO anon, authenticated, service_role;',
176
- 'GRANT USAGE ON SCHEMA realtime TO anon, authenticated, service_role;',
177
- 'GRANT anon TO authenticator;',
178
- 'GRANT authenticated TO authenticator;',
179
- 'GRANT service_role TO authenticator;',
180
- ]
181
-
182
- // --- JWT bundle provisioning ----------------------------------------------
183
- // GoTrue, PostgREST and Storage API all verify Supabase JWTs. The bundle
184
- // (jwtSecret, anonKey, serviceRoleKey, referrerURLs) must never be
185
- // rendered as plaintext: with a secrets backend it is provisioned through
186
- // the backend (keys live at <path>/<name>/jwt); otherwise reference a
187
- // pre-created Secret.
188
- if (!jwtSecretsName) {
189
- if (
190
- !secretProvider ||
191
- (secretProvider.backend !== 'vault' && secretProvider.backend !== 'openbao')
192
- ) {
193
- throw new Error(
194
- `Supabase "${name}" requires a JWT secret bundle (keys: jwtSecret, anonKey, serviceRoleKey, referrerURLs).\n` +
195
- `\n` +
196
- `GoTrue, PostgREST and Storage API verify Supabase JWTs — the bundle ` +
197
- `must not be rendered as plaintext.\n` +
198
- `\n` +
199
- `Fix: configure a secrets backend on the Platform holding the bundle ` +
200
- `at path <mount-path>/${name}/jwt:\n` +
201
- ` <Platform secrets={{ backend: 'openbao', mount: 'kv', path: 'apps' }}>\n` +
202
- ` <Supabase name="${name}" host="${host}" objectStorage={{ endpoint: '...', bucket: '...', credentialsSecret: '...' }} />\n` +
203
- ` </Platform>\n` +
204
- `\n` +
205
- `Or reference a pre-created Secret (keys: jwtSecret, anonKey, serviceRoleKey, referrerURLs):\n` +
206
- ` <Supabase name="${name}" host="${host}" jwtSecretsName="${name}-jwt" />`
207
- )
208
- }
209
-
210
- const spec = {
211
- ...(secretProvider.backend === 'vault'
212
- ? { vaultAuthRef: secretProvider.authRef }
213
- : { openbaoAuthRef: secretProvider.authRef }),
214
- mount: secretProvider.mount,
215
- type: 'kv-v2' as const,
216
- path: `${secretProvider.path ?? name}/${name}/jwt`,
217
- destination: { create: true, name: jwtBundleName },
218
- }
219
- resources_.push(
220
- secretProvider.backend === 'vault'
221
- ? jsx('VaultStaticSecret', {
222
- apiVersion: 'secrets.hashicorp.com/v1beta1',
223
- kind: 'VaultStaticSecret',
224
- metadata: { name: `${name}-jwt`, namespace },
225
- spec,
226
- })
227
- : jsx('OpenBaoStaticSecret', {
228
- apiVersion: 'secrets.openbao.org/v1beta1',
229
- kind: 'OpenBaoStaticSecret',
230
- metadata: { name: `${name}-jwt`, namespace },
231
- spec,
232
- })
233
- )
234
- }
235
-
236
- // --- Database (CNPG) — the Postgres core -----------------------------------
237
- // Wraps every Postgres-backed service so credentials stay consistent with
238
- // the r8s Database recipe (CNPG dedicated cluster provisions the secret).
239
- resources_.push(
240
- jsx(Database, {
241
- name,
242
- namespace,
243
- storage,
244
- postInitSQL,
245
- children: (
246
- <>
247
- <WebService
248
- name={`${name}-gotrue`}
249
- namespace={namespace}
250
- image="supabase/gotrue:v2"
251
- port={9999}
252
- replicas={replicas}
253
- resources={resources}
254
- probes={{
255
- liveness: { path: '/health', initialDelaySeconds: 15 },
256
- readiness: { path: '/health', initialDelaySeconds: 15 },
257
- }}
258
- env={{
259
- GOTRUE_API_HOST: '0.0.0.0',
260
- GOTRUE_API_PORT: '9999',
261
- GOTRUE_SITE_URL: `https://${host}`,
262
- API_EXTERNAL_URL: `https://${host}`,
263
- GOTRUE_DB_DRIVER: 'postgres',
264
- GOTRUE_DB_DATABASE_URL: dbUri,
265
- GOTRUE_JWT_ISSUER: `https://${host}/auth/v1`,
266
- GOTRUE_JWT_ADMIN_ROLES: 'service_role',
267
- GOTRUE_JWT_AUD: 'authenticated',
268
- GOTRUE_JWT_DEFAULT_GROUP_NAME: 'authenticated',
269
- GOTRUE_JWT_EXP: '3600',
270
- ...(uriAllowList && {
271
- GOTRUE_URI_ALLOW_LIST: Array.isArray(uriAllowList)
272
- ? uriAllowList.join(',')
273
- : uriAllowList,
274
- }),
275
- }}
276
- secrets={{
277
- PGPASSWORD: { secret: dbCredentialsName, key: 'password' },
278
- GOTRUE_JWT_SECRET: { secret: jwtBundleName, key: 'jwtSecret' },
279
- }}
280
- />
281
- <WebService
282
- name={`${name}-postgrest`}
283
- namespace={namespace}
284
- image="postgrest/postgrest:v12"
285
- port={3000}
286
- replicas={replicas}
287
- resources={resources}
288
- probes={{
289
- liveness: { path: '/' },
290
- readiness: { path: '/' },
291
- }}
292
- env={{
293
- PGRST_SERVER_PORT: '3000',
294
- PGRST_DB_URI: dbUri,
295
- PGRST_DB_SCHEMAS: 'public,storage,graphql_public',
296
- PGRST_DB_ANON_ROLE: 'anon',
297
- PGRST_DB_USE_LEGACY_GUCS: 'false',
298
- }}
299
- secrets={{
300
- PGPASSWORD: { secret: dbCredentialsName, key: 'password' },
301
- PGRST_JWT_SECRET: { secret: jwtBundleName, key: 'jwtSecret' },
302
- }}
303
- />
304
- <WebService
305
- name={`${name}-realtime`}
306
- namespace={namespace}
307
- image="supabase/realtime:v2"
308
- port={4000}
309
- replicas={replicas}
310
- resources={resources}
311
- probes={{
312
- liveness: { path: '/health' },
313
- readiness: { path: '/health' },
314
- }}
315
- env={{
316
- PORT: '4000',
317
- DB_HOST: dbHost,
318
- DB_PORT: '5432',
319
- DB_USER: name,
320
- DB_NAME: name,
321
- DB_ENC_KEY: 'supabaserealtime',
322
- SECURE_CHANNELS: 'true',
323
- }}
324
- secrets={{
325
- DB_PASSWORD: { secret: dbCredentialsName, key: 'password' },
326
- API_JWT_SECRET: { secret: jwtBundleName, key: 'jwtSecret' },
327
- }}
328
- />
329
- {storageApi && (
330
- <WebService
331
- name={`${name}-storage-api`}
332
- namespace={namespace}
333
- image="supabase/storage-api:v0"
334
- port={5000}
335
- replicas={replicas}
336
- resources={resources}
337
- probes={{
338
- liveness: { path: '/status' },
339
- readiness: { path: '/status' },
340
- }}
341
- env={{
342
- PORT: '5000',
343
- DATABASE_URL: dbUri,
344
- STORAGE_BACKEND: 's3',
345
- GLOBAL_S3_BUCKET: objectStorage.bucket,
346
- GLOBAL_S3_ENDPOINT: objectStorage.endpoint,
347
- GLOBAL_S3_REGION: region,
348
- GLOBAL_S3_FORCE_PATH_STYLE: 'true',
349
- IMGPROXY_URL: `http://${name}-imgproxy:8080`,
350
- FILE_SIZE_LIMIT: '50GiB',
351
- }}
352
- secrets={{
353
- PGPASSWORD: { secret: dbCredentialsName, key: 'password' },
354
- PGRST_JWT_SECRET: { secret: jwtBundleName, key: 'jwtSecret' },
355
- ANON_KEY: { secret: jwtBundleName, key: 'anonKey' },
356
- SERVICE_KEY: { secret: jwtBundleName, key: 'serviceRoleKey' },
357
- GLOBAL_S3_ACCESS_KEY: {
358
- secret: objectStorage.credentialsSecret,
359
- key: 'accessKey',
360
- },
361
- GLOBAL_S3_SECRET_KEY: {
362
- secret: objectStorage.credentialsSecret,
363
- key: 'secretKey',
364
- },
365
- }}
366
- />
367
- )}
368
- </>
369
- ),
370
- })
371
- )
372
-
373
- // --- ImgProxy — image transforms for the Storage API ------------------------
374
- // Stateful-free image resizer; no database or secrets needed.
375
- resources_.push(
376
- <WebService
377
- name={`${name}-imgproxy`}
378
- namespace={namespace}
379
- image="darthsim/imgproxy:v3"
380
- port={8080}
381
- replicas={replicas}
382
- resources={resources}
383
- probes={{
384
- liveness: { path: '/health' },
385
- readiness: { path: '/health' },
386
- }}
387
- env={{
388
- IMGPROXY_ALLOW_ORIGIN: '*',
389
- IMGPROXY_ENABLE_WEBP_DETECTION: 'true',
390
- IMGPROXY_MAX_SRC_RESOLUTION: '50',
391
- }}
392
- />
393
- )
394
-
395
- // --- Endpoints — one host, path-based routes --------------------------------
396
- // The REST root (PostgREST) serves `/`; GoTrue, Realtime and Storage API
397
- // are exposed as Prefix routes on the same host so clients use a single
398
- // origin (`/auth/v1`, `/rest/v1`, `/realtime/v1`, `/storage/v1`).
399
- resources_.push(
400
- <Endpoint
401
- name={`${name}-endpoint`}
402
- namespace={namespace}
403
- host={host}
404
- serviceName={`${name}-postgrest`}
405
- servicePort={3000}
406
- tls={tls}
407
- />,
408
- <Endpoint
409
- name={`${name}-auth-endpoint`}
410
- namespace={namespace}
411
- host={host}
412
- path="/auth/v1"
413
- serviceName={`${name}-gotrue`}
414
- servicePort={9999}
415
- tls={tls}
416
- />,
417
- <Endpoint
418
- name={`${name}-rest-endpoint`}
419
- namespace={namespace}
420
- host={host}
421
- path="/rest/v1"
422
- serviceName={`${name}-postgrest`}
423
- servicePort={3000}
424
- tls={tls}
425
- />,
426
- <Endpoint
427
- name={`${name}-realtime-endpoint`}
428
- namespace={namespace}
429
- host={host}
430
- path="/realtime/v1"
431
- serviceName={`${name}-realtime`}
432
- servicePort={4000}
433
- tls={tls}
434
- annotations={{
435
- 'nginx.ingress.kubernetes.io/proxy-buffering': 'off',
436
- 'nginx.ingress.kubernetes.io/proxy-read-timeout': '3600',
437
- 'nginx.ingress.kubernetes.io/proxy-send-timeout': '3600',
438
- }}
439
- />,
440
- storageApi && (
441
- <Endpoint
442
- name={`${name}-storage-endpoint`}
443
- namespace={namespace}
444
- host={host}
445
- path="/storage/v1"
446
- serviceName={`${name}-storage-api`}
447
- servicePort={5000}
448
- tls={tls}
449
- />
450
- )
451
- )
452
-
453
- return jsx(Fragment, { children: resources_ })
454
- }
package/tsconfig.json DELETED
@@ -1,15 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.json",
3
- "compilerOptions": {
4
- "outDir": "./dist",
5
- "rootDir": "./src",
6
- "composite": true
7
- },
8
- "include": ["src/**/*"],
9
- "references": [
10
- { "path": "../core" },
11
- { "path": "../k8s-types" },
12
- { "path": "../crds" },
13
- { "path": "../recipes" }
14
- ]
15
- }