@supatype/cli 0.2.1 → 0.3.1

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.
Files changed (173) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-test.log +193 -157
  3. package/.turbo/turbo-typecheck.log +1 -1
  4. package/dist/api-config-cache.d.ts +34 -0
  5. package/dist/api-config-cache.d.ts.map +1 -0
  6. package/dist/api-config-cache.js +123 -0
  7. package/dist/api-config-cache.js.map +1 -0
  8. package/dist/cache-identity-scope.d.ts +38 -0
  9. package/dist/cache-identity-scope.d.ts.map +1 -0
  10. package/dist/cache-identity-scope.js +70 -0
  11. package/dist/cache-identity-scope.js.map +1 -0
  12. package/dist/cache-provider.d.ts +30 -0
  13. package/dist/cache-provider.d.ts.map +1 -0
  14. package/dist/cache-provider.js +60 -0
  15. package/dist/cache-provider.js.map +1 -0
  16. package/dist/cli-version-embedded.js +1 -1
  17. package/dist/commands/add.js +1 -1
  18. package/dist/commands/add.js.map +1 -1
  19. package/dist/commands/admin.d.ts.map +1 -1
  20. package/dist/commands/admin.js +21 -4
  21. package/dist/commands/admin.js.map +1 -1
  22. package/dist/commands/cache.d.ts +1 -1
  23. package/dist/commands/cache.js +3 -3
  24. package/dist/commands/cache.js.map +1 -1
  25. package/dist/commands/cloud.d.ts.map +1 -1
  26. package/dist/commands/cloud.js +2 -1
  27. package/dist/commands/cloud.js.map +1 -1
  28. package/dist/commands/deploy.d.ts.map +1 -1
  29. package/dist/commands/deploy.js +2 -1
  30. package/dist/commands/deploy.js.map +1 -1
  31. package/dist/commands/dev.d.ts.map +1 -1
  32. package/dist/commands/dev.js +61 -8
  33. package/dist/commands/dev.js.map +1 -1
  34. package/dist/commands/diff.d.ts.map +1 -1
  35. package/dist/commands/diff.js +2 -1
  36. package/dist/commands/diff.js.map +1 -1
  37. package/dist/commands/functions.d.ts.map +1 -1
  38. package/dist/commands/functions.js +6 -2
  39. package/dist/commands/functions.js.map +1 -1
  40. package/dist/commands/init.d.ts.map +1 -1
  41. package/dist/commands/init.js +13 -9
  42. package/dist/commands/init.js.map +1 -1
  43. package/dist/commands/keys.d.ts +35 -0
  44. package/dist/commands/keys.d.ts.map +1 -1
  45. package/dist/commands/keys.js +90 -6
  46. package/dist/commands/keys.js.map +1 -1
  47. package/dist/commands/migrate.d.ts.map +1 -1
  48. package/dist/commands/migrate.js +2 -1
  49. package/dist/commands/migrate.js.map +1 -1
  50. package/dist/commands/push.d.ts.map +1 -1
  51. package/dist/commands/push.js +61 -1
  52. package/dist/commands/push.js.map +1 -1
  53. package/dist/compose-services.d.ts.map +1 -1
  54. package/dist/compose-services.js +2 -1
  55. package/dist/compose-services.js.map +1 -1
  56. package/dist/dev-compose.d.ts +13 -0
  57. package/dist/dev-compose.d.ts.map +1 -1
  58. package/dist/dev-compose.js +119 -16
  59. package/dist/dev-compose.js.map +1 -1
  60. package/dist/engine-floor.d.ts +11 -1
  61. package/dist/engine-floor.d.ts.map +1 -1
  62. package/dist/engine-floor.js +49 -16
  63. package/dist/engine-floor.js.map +1 -1
  64. package/dist/functions-deno-types.d.ts +9 -0
  65. package/dist/functions-deno-types.d.ts.map +1 -1
  66. package/dist/functions-deno-types.js +45 -1
  67. package/dist/functions-deno-types.js.map +1 -1
  68. package/dist/functions-router-gen.d.ts.map +1 -1
  69. package/dist/functions-router-gen.js +113 -81
  70. package/dist/functions-router-gen.js.map +1 -1
  71. package/dist/kong-config.d.ts +1 -1
  72. package/dist/model-cache.d.ts +44 -0
  73. package/dist/model-cache.d.ts.map +1 -0
  74. package/dist/model-cache.js +106 -0
  75. package/dist/model-cache.js.map +1 -0
  76. package/dist/model-hooks.d.ts +5 -0
  77. package/dist/model-hooks.d.ts.map +1 -1
  78. package/dist/model-hooks.js +29 -1
  79. package/dist/model-hooks.js.map +1 -1
  80. package/dist/model-versioning.d.ts +80 -0
  81. package/dist/model-versioning.d.ts.map +1 -0
  82. package/dist/model-versioning.js +140 -0
  83. package/dist/model-versioning.js.map +1 -0
  84. package/dist/postgres-ctl.d.ts +55 -0
  85. package/dist/postgres-ctl.d.ts.map +1 -1
  86. package/dist/postgres-ctl.js +84 -8
  87. package/dist/postgres-ctl.js.map +1 -1
  88. package/dist/preview-config-check.d.ts +21 -0
  89. package/dist/preview-config-check.d.ts.map +1 -0
  90. package/dist/preview-config-check.js +47 -0
  91. package/dist/preview-config-check.js.map +1 -0
  92. package/dist/project-config.d.ts +182 -5
  93. package/dist/project-config.d.ts.map +1 -1
  94. package/dist/project-config.js +125 -8
  95. package/dist/project-config.js.map +1 -1
  96. package/dist/resolve-target.d.ts +9 -0
  97. package/dist/resolve-target.d.ts.map +1 -1
  98. package/dist/resolve-target.js.map +1 -1
  99. package/dist/rest-cache-admin.d.ts.map +1 -1
  100. package/dist/rest-cache-admin.js +6 -2
  101. package/dist/rest-cache-admin.js.map +1 -1
  102. package/dist/schema-ast-v2.d.ts +32 -1
  103. package/dist/schema-ast-v2.d.ts.map +1 -1
  104. package/dist/schema-ast-v2.js +14 -1
  105. package/dist/schema-ast-v2.js.map +1 -1
  106. package/dist/self-host-compose.d.ts +20 -1
  107. package/dist/self-host-compose.d.ts.map +1 -1
  108. package/dist/self-host-compose.js +227 -29
  109. package/dist/self-host-compose.js.map +1 -1
  110. package/dist/studio-admin-roles.d.ts +8 -1
  111. package/dist/studio-admin-roles.d.ts.map +1 -1
  112. package/dist/studio-admin-roles.js +16 -2
  113. package/dist/studio-admin-roles.js.map +1 -1
  114. package/dist/type-extractor.d.ts +7 -0
  115. package/dist/type-extractor.d.ts.map +1 -1
  116. package/dist/type-extractor.js +234 -3
  117. package/dist/type-extractor.js.map +1 -1
  118. package/package.json +2 -2
  119. package/src/api-config-cache.ts +142 -0
  120. package/src/cache-identity-scope.ts +92 -0
  121. package/src/cache-provider.ts +61 -0
  122. package/src/cli-version-embedded.ts +1 -1
  123. package/src/commands/add.ts +1 -1
  124. package/src/commands/admin.ts +23 -6
  125. package/src/commands/cache.ts +3 -3
  126. package/src/commands/cloud.ts +2 -1
  127. package/src/commands/deploy.ts +2 -1
  128. package/src/commands/dev.ts +68 -7
  129. package/src/commands/diff.ts +2 -1
  130. package/src/commands/functions.ts +6 -2
  131. package/src/commands/init.ts +13 -9
  132. package/src/commands/keys.ts +112 -7
  133. package/src/commands/migrate.ts +2 -1
  134. package/src/commands/push.ts +68 -1
  135. package/src/compose-services.ts +2 -1
  136. package/src/dev-compose.ts +151 -15
  137. package/src/engine-floor.ts +73 -16
  138. package/src/functions-deno-types.ts +48 -1
  139. package/src/functions-router-gen.ts +113 -81
  140. package/src/kong-config.ts +1 -1
  141. package/src/model-cache.ts +122 -0
  142. package/src/model-hooks.ts +26 -2
  143. package/src/model-versioning.ts +167 -0
  144. package/src/postgres-ctl.ts +103 -9
  145. package/src/preview-config-check.ts +52 -0
  146. package/src/project-config.ts +273 -11
  147. package/src/resolve-target.ts +11 -1
  148. package/src/rest-cache-admin.ts +8 -2
  149. package/src/schema-ast-v2.ts +47 -0
  150. package/src/self-host-compose.ts +250 -29
  151. package/src/studio-admin-roles.ts +16 -2
  152. package/src/type-extractor.ts +281 -2
  153. package/tests/api-config-cache-seed.test.ts +180 -0
  154. package/tests/cache-identity-scope.test.ts +61 -0
  155. package/tests/cache-provider-config.test.ts +132 -0
  156. package/tests/engine-floor.test.ts +43 -0
  157. package/tests/external-database-compose.test.ts +74 -3
  158. package/tests/field-masking-tier.test.ts +3 -3
  159. package/tests/fixtures/identity-scope-corpus.json +42 -0
  160. package/tests/functions-concurrency.test.ts +71 -0
  161. package/tests/init.test.ts +36 -2
  162. package/tests/keys-write.test.ts +148 -0
  163. package/tests/model-cache-declaration.test.ts +229 -0
  164. package/tests/model-cache-manifest.test.ts +99 -0
  165. package/tests/model-hooks.test.ts +55 -0
  166. package/tests/model-versions.test.ts +229 -0
  167. package/tests/native-keyspace.test.ts +130 -0
  168. package/tests/preview-config-check.test.ts +104 -0
  169. package/tests/rest-cache-admin.test.ts +36 -0
  170. package/tests/runtime-contract.test.ts +176 -22
  171. package/tests/studio-admin-roles.test.ts +32 -0
  172. package/tests/type-extractor.test.ts +113 -0
  173. package/tsconfig.tsbuildinfo +1 -1
@@ -16,8 +16,10 @@ import {
16
16
  } from "./project-config.js"
17
17
  import { hasEngineOverride, hasStudioOverride, pinnedVersion, fetchLatestVersion, VERSION_PIN_LOCAL } from "./binary-cache.js"
18
18
  import { buildKongDeclarative } from "./kong-config.js"
19
+ import { keyspaceInPostgres } from "./cache-provider.js"
19
20
  import { readEnvFile } from "./env-file.js"
20
21
  import { fieldMaskingTierFromProject, type FieldMaskingTier } from "./field-masking-tier.js"
22
+ import { projectHasVersionedModels } from "./model-versioning.js"
21
23
 
22
24
  /** Env keys written when `versions` pins exist in supatype.config.ts. */
23
25
  export const COMPOSE_PINNED_IMAGE_ENV_KEYS = [
@@ -34,8 +36,31 @@ export const COMPOSE_IMAGE_ENV_KEYS = [
34
36
  "SUPATYPE_STUDIO_IMAGE",
35
37
  "SUPATYPE_STORAGE_IMAGE",
36
38
  "SUPATYPE_FUNCTIONS_WORKER_IMAGE",
39
+ "SUPATYPE_SEAWEEDFS_IMAGE",
37
40
  ] as const
38
41
 
42
+ /** Pinned by digest, because the registry this replaced proved a tag can be taken away. */
43
+ const SEAWEEDFS_IMAGE =
44
+ "chrislusf/seaweedfs:4.46@sha256:08d516132314207d10c8e37cbffc1f32b147d870169688734cc61c6231625b62"
45
+
46
+ /** Credentials the object store is configured with and the storage service is handed. */
47
+ /**
48
+ * The prefix Kong's ACME plugin stores its certificates under.
49
+ *
50
+ * Not a guess and not configurable: the plugin prefixes every key it writes
51
+ * with this and enforces it in its own `reserved_words.lua`, so it is the one
52
+ * durable thing in a keyspace that is otherwise a cache. Verified on a live
53
+ * instance — both ACME keys survived `kill -9` with only this prefix named
54
+ * durable, and a response-cache key beside them did not.
55
+ */
56
+ const ACME_KEY_PREFIX = "kong_acme:"
57
+
58
+ const OBJECT_STORE_ACCESS_KEY = "supatype"
59
+ const OBJECT_STORE_SECRET_KEY = "supatype-secret"
60
+
61
+ /** Where the rendered SeaweedFS identity file sits, resolved from the project root like kong.yml. */
62
+ const SEAWEED_CONFIG_MOUNT = ".supatype/self-host/s3.json"
63
+
39
64
  type DockerPinComponent = "engine" | "server" | "postgres"
40
65
 
41
66
  /** Map a config version pin to a Docker Hub image reference. */
@@ -142,11 +167,36 @@ export async function schemaEngineImageForPush(
142
167
  return dockerImageRef("engine", version)
143
168
  }
144
169
 
170
+ /**
171
+ * SeaweedFS reads its identities from a file, so the stack's credentials are a generated artifact
172
+ * beside the compose file rather than two env vars inside it.
173
+ *
174
+ * There is deliberately no `anonymous` identity. Declaring one with `Read` would make every bucket
175
+ * world-readable and the per-bucket policy decorative, which is the opposite of what
176
+ * `applyPublicPolicy` is for: a bucket is public because its policy says so.
177
+ */
178
+ export function renderSeaweedIdentities(): string {
179
+ const identities = {
180
+ identities: [
181
+ {
182
+ name: "supatype",
183
+ credentials: [
184
+ { accessKey: OBJECT_STORE_ACCESS_KEY, secretKey: OBJECT_STORE_SECRET_KEY },
185
+ ],
186
+ actions: ["Admin", "Read", "List", "Tagging", "Write"],
187
+ },
188
+ ],
189
+ }
190
+ return `${JSON.stringify(identities, null, 2)}\n`
191
+ }
192
+
145
193
  export interface SelfHostComposePaths {
146
194
  dir: string
147
195
  composePath: string
148
196
  kongPath: string
149
197
  nginxPath: string
198
+ /** SeaweedFS identities, rendered beside the compose file the server reads them through. */
199
+ s3ConfigPath: string
150
200
  }
151
201
 
152
202
  export function selfHostComposePaths(cwd: string): SelfHostComposePaths {
@@ -156,6 +206,7 @@ export function selfHostComposePaths(cwd: string): SelfHostComposePaths {
156
206
  composePath: join(dir, "docker-compose.yml"),
157
207
  kongPath: join(dir, "kong.yml"),
158
208
  nginxPath: join(dir, "nginx.conf"),
209
+ s3ConfigPath: join(dir, "s3.json"),
159
210
  }
160
211
  }
161
212
 
@@ -286,6 +337,14 @@ export interface SelfHostComposeOptions {
286
337
  * rules and for every project on `supatype/postgres`.
287
338
  */
288
339
  fieldMaskingTier?: FieldMaskingTier
340
+ /**
341
+ * Whether any model declares `versions`, which puts the generated `draft` schema on the exposed
342
+ * list.
343
+ *
344
+ * Same reasoning as the tier above: only a caller that has loaded the schema can say, and left
345
+ * unset the list is today's, which is correct for every project with no versioned model.
346
+ */
347
+ drafts?: boolean
289
348
  }
290
349
 
291
350
  export function renderSelfHostCompose(
@@ -296,6 +355,12 @@ export function renderSelfHostCompose(
296
355
  const projectMount = projectMountPath(cwd)
297
356
  const kongMount = kongMountPath(cwd)
298
357
  const external = usesExternalDatabase(config)
358
+ // Both halves come from the caller because both need the schema loaded, which this renderer does
359
+ // not do. See `SelfHostComposeOptions`.
360
+ const restSchemas = apiSchemaList(config, {
361
+ ...(options?.fieldMaskingTier !== undefined && { tier: options.fieldMaskingTier }),
362
+ drafts: options?.drafts === true,
363
+ })
299
364
  const ownerUrl = ownerDatabaseUrl(config)
300
365
  // the auth driver wants the `postgres://` spelling; an external URL is used as given.
301
366
  const authUrl = external ? ownerUrl : ownerDatabaseUrl(config, "postgres")
@@ -337,20 +402,42 @@ ${studioService}
337
402
  - "127.0.0.1:\${SUPATYPE_DEV_DB_PORT:-54329}:5432"
338
403
  `
339
404
  : ` ports:
340
- - "5432:5432"
405
+ - "\${SUPATYPE_DB_PORT:-5432}:5432"
341
406
  `
342
407
  : ""
408
+ // Host ports, every one of them overridable.
409
+ //
410
+ // Kong's has been `${SUPATYPE_KONG_PORT:-18473}` for as long as `supatype dev` has picked a free
411
+ // one per project and written it back to .env. These three were fixed literals, so the second
412
+ // project on a machine could not start: Docker refuses the bind and compose reports only that
413
+ // the stack would not come up, naming no port. Two projects side by side is the normal case
414
+ // here (this repository ships eight examples), so a literal is the wrong default.
415
+ //
416
+ // Defaults are the previous literals, so a project with none of these set behaves exactly as
417
+ // before. Allocating them per project the way the Kong port is allocated is the follow-up; this
418
+ // makes a second stack possible rather than automatic.
343
419
  const serverPorts = devLocal
344
420
  ? ""
345
421
  : ` ports:
346
- - "9999:9999"
422
+ - "\${SUPATYPE_SERVER_PORT:-9999}:9999"
347
423
  `
348
- const minioPorts = devLocal
424
+ const seaweedPorts = devLocal
349
425
  ? ""
350
426
  : ` ports:
351
- - "9000:9000"
352
- - "9001:9001"
427
+ - "\${SUPATYPE_SEAWEEDFS_PORT:-8333}:8333"
353
428
  `
429
+ // One source for the credentials: the server is configured with them and the storage service is
430
+ // handed them, and a mismatch does not fail at start, it fails at the first upload.
431
+ const objectStoreBlock = ` seaweedfs:
432
+ # Identities come from a config file rather than env vars, and the anonymous identity is
433
+ # deliberately absent: a bucket is public because of its policy, never because the server is
434
+ # open. Written beside this file by the same generator, so the two cannot drift.
435
+ image: \${SUPATYPE_SEAWEEDFS_IMAGE:-${SEAWEEDFS_IMAGE}}
436
+ command: server -dir=/data -s3 -s3.port=8333 -s3.config=/etc/seaweedfs/s3.json
437
+ volumes:
438
+ - storage-data:/data
439
+ - ${SEAWEED_CONFIG_MOUNT}:/etc/seaweedfs/s3.json:ro
440
+ ${seaweedPorts}`
354
441
  const kongTlsEnv = tlsEnabled
355
442
  ? ` KONG_PROXY_LISTEN: "0.0.0.0:8000, 0.0.0.0:8443 ssl"
356
443
  KONG_LUA_SSL_TRUSTED_CERTIFICATE: system
@@ -361,7 +448,16 @@ ${studioService}
361
448
  ? ` - "80:8000"
362
449
  - "443:8443"`
363
450
  : ` - "\${SUPATYPE_KONG_PORT:-18473}:8000"`
364
- const valkeyBlock = `
451
+ // The RESP server the response cache and Kong's ACME certificates use.
452
+ // Either a Valkey sidecar, or pg_keyspace inside the Postgres container --
453
+ // one stateful service instead of two.
454
+ const keyspaceInPg = keyspaceInPostgres(config)
455
+ const respHost = keyspaceInPg ? "db" : "valkey"
456
+ const valkeyBlock = keyspaceInPg
457
+ ? ` # No \`valkey\` service: cache.provider is "pg_keyspace", so the RESP keyspace is
458
+ # served by the \`db\` container on :6379. See extensions/pg_keyspace in supatype/postgres.
459
+ `
460
+ : `
365
461
  valkey:
366
462
  image: \${SUPATYPE_VALKEY_IMAGE:-valkey/valkey:8-alpine}
367
463
  command: ["valkey-server", "--appendonly", "yes"]
@@ -370,7 +466,9 @@ ${studioService}
370
466
  volumes:
371
467
  - valkey-data:/data
372
468
  `
373
- const kongValkeyDepends = "\n - valkey"
469
+ // With the keyspace inside Postgres, waiting on `db` is already what every
470
+ // other service does, so Kong needs no extra dependency of its own.
471
+ const kongValkeyDepends = keyspaceInPg ? "" : "\n - valkey"
374
472
  const tlsHintComment = tlsEnabled
375
473
  ? ""
376
474
  : ` # HTTPS is off. To enable automatic TLS (Let's Encrypt) for production, set in supatype.config.ts:
@@ -379,9 +477,8 @@ ${studioService}
379
477
  `
380
478
  // An external database is not ours to declare a volume for.
381
479
  const volumesBlock = `volumes:
382
- ${external ? "" : " db-data:\n"} minio-data:
383
- valkey-data:
384
- `
480
+ ${external ? "" : " db-data:\n"} storage-data:
481
+ ${keyspaceInPg ? "" : " valkey-data:\n"}`
385
482
 
386
483
  // `depends_on` for the services that talk to Postgres. With an external database there is no
387
484
  // container to wait on, so the clause disappears entirely and each service retries on connect
@@ -426,6 +523,89 @@ ${dbDependency}`
426
523
  : ""
427
524
  const realtimeServerEnv = realtime ? " SUPATYPE_REALTIME_URL: http://realtime:4000" : ""
428
525
 
526
+ // pg_keyspace registers background workers and requests shared memory at
527
+ // postmaster start, so it has to be in `shared_preload_libraries` -- there is
528
+ // no runtime toggle. The image turns it on from the environment
529
+ // (SUPATYPE_KEYSPACE_ENABLED) and writes the configuration itself, which is
530
+ // why this does not restate the preload list: the entrypoint appends
531
+ // pg_keyspace to the image's own list, in the right place (before
532
+ // supatype_mask, so the mask stays outermost and the image's
533
+ // pg_keyspace.require_mask = on is satisfied rather than refusing to serve).
534
+ // Restating it here would be a copy that goes stale the first time the image
535
+ // adds a library.
536
+ //
537
+ // Durability is `ephemeral` for everything, with ACME the one exception.
538
+ // A response cache that went through the WAL would put this stack's busiest
539
+ // write on disk to keep a copy of something that expires in seconds; a
540
+ // certificate that did not survive `docker compose restart db` would be
541
+ // re-issued against Let's Encrypt's rate limits, or fail. So the default is
542
+ // the cheap one and the certificates are named.
543
+ //
544
+ // `kong_acme:` is not a guess. Kong's ACME plugin prefixes every key it
545
+ // stores with it and enforces that prefix in its own reserved_words.lua;
546
+ // proven on a live instance, where both ACME keys survived `kill -9` under
547
+ // this exact configuration and the response-cache key did not.
548
+ //
549
+ // It is set whether or not TLS is on today. The override costs nothing when
550
+ // no key matches it, and a stack that turns TLS on later should not need a
551
+ // database restart before its certificates are safe to store.
552
+ const durablePrefixes = config.cache?.durablePrefixes ?? []
553
+ const durabilityOverrides = [ACME_KEY_PREFIX, ...durablePrefixes]
554
+ .map((prefix) => `${prefix}=durable`)
555
+ .join(", ")
556
+
557
+ // Sizing is shared memory, reserved at postmaster start whether or not the
558
+ // cache is used, so the defaults are a self-hosted stack's floor rather than
559
+ // the extension's own (1M keys x 512 B reserves ~689 MiB per worker before
560
+ // rings and row cache). These are measured on a live PG16 build, not derived:
561
+ // this profile reserves ~235 MiB, of which ~16 MiB is a fixed row-cache
562
+ // directory that rowcache_mb does not describe and cannot be declined.
563
+ const keyspaceDbEnv = keyspaceInPg
564
+ ? ` # The RESP keyspace, in place of a Valkey sidecar. The image writes
565
+ # /etc/postgresql-custom/pg_keyspace.conf from these before any server starts.
566
+ SUPATYPE_KEYSPACE_ENABLED: "1"
567
+ # Durable keys are rows in this database; without it they land in \`postgres\`.
568
+ SUPATYPE_KEYSPACE_DATABASE: \${POSTGRES_DB:-supatype}
569
+ SUPATYPE_KEYSPACE_DURABILITY: ephemeral
570
+ SUPATYPE_KEYSPACE_DURABILITY_OVERRIDES: "${durabilityOverrides}"
571
+ SUPATYPE_KEYSPACE_KEYS: "200000"
572
+ SUPATYPE_KEYSPACE_RING_MB: "16"
573
+ SUPATYPE_KEYSPACE_ROWCACHE_MB: "64"
574
+ # Mode B, the row cache itself. The segment above is reserved either way; these two decide
575
+ # whether anything decodes into it or serves from it, and the image refuses readthrough
576
+ # without decode because that pair serves stale rows forever rather than merely wasting
577
+ # memory.
578
+ #
579
+ # From .env rather than a literal, because only a push can answer this: the switches follow
580
+ # whether any model declares \`cache: { rows: true }\`, and \`self-host compose render\` runs
581
+ # with no schema in hand. Default off, so a project that declares no row cache does not pin
582
+ # WAL behind a replication slot it never reads.
583
+ SUPATYPE_KEYSPACE_ROWCACHE_DECODE: "\${SUPATYPE_KEYSPACE_ROWCACHE_DECODE:-0}"
584
+ SUPATYPE_KEYSPACE_ROWCACHE_READTHROUGH: "\${SUPATYPE_KEYSPACE_ROWCACHE_READTHROUGH:-0}"
585
+ # Both decoders this stack runs, because the image's setting REPLACES the allowlist.
586
+ #
587
+ # Turning the row cache on makes the entrypoint write
588
+ # \`output_plugin_libraries = 'supacache_keys'\`, and PostgreSQL then refuses every other
589
+ # plugin. Realtime decodes with wal2json, so a project that declared \`cache: { rows: true }\`
590
+ # silently lost realtime: the service stayed up, answered /health/ready with 200, and logged
591
+ # \`library "wal2json" may not be used as an output plugin\` once a second while every
592
+ # subscription reported SUBSCRIBED and delivered nothing.
593
+ #
594
+ # Named here rather than left to the image because only this file knows both features are in
595
+ # the same stack.
596
+ SUPATYPE_KEYSPACE_OUTPUT_PLUGIN_LIBRARIES: "supacache_keys, wal2json"
597
+ `
598
+ : ""
599
+
600
+ // Not published to the host: the keyspace is reachable to the compose network
601
+ // (server, Kong) and nothing else. A RESP port on a public interface is an
602
+ // unauthenticated read of every cached response.
603
+ const keyspaceExpose = keyspaceInPg
604
+ ? ` expose:
605
+ - "6379"
606
+ `
607
+ : ""
608
+
429
609
  const dbServiceBlock = external
430
610
  ? ` # No \`db\` service: database.external points this stack at a Postgres it does not manage.
431
611
  # Every service reads \${DATABASE_URL} from .env, and \`supatype db check\` reports what that
@@ -439,7 +619,7 @@ ${dbDependency}`
439
619
  POSTGRES_DB: \${POSTGRES_DB:-supatype}
440
620
  # Read by the image's init to password the \`authenticator\` role PostgREST connects as.
441
621
  AUTHENTICATOR_PASSWORD: \${AUTHENTICATOR_PASSWORD:?AUTHENTICATOR_PASSWORD is missing from .env}
442
- ${dbPorts} volumes:
622
+ ${keyspaceDbEnv}${dbPorts}${keyspaceExpose} volumes:
443
623
  - db-data:/var/lib/postgresql/data
444
624
  healthcheck:
445
625
  # -h 127.0.0.1 forces TCP. Without it \`pg_isready\` uses the Unix socket, which the
@@ -477,7 +657,7 @@ ${dbServiceBlock} postgrest:
477
657
  # Derived from schema.pg_schema (or schema.api_schemas). Hardcoding this is why choosing a
478
658
  # non-public pg_schema used to give a correct push and an API that answered PGRST106 for
479
659
  # everything: the engine moved and PostgREST was never told.
480
- PGRST_DB_SCHEMA: "${apiSchemaList(config, options?.fieldMaskingTier)}"
660
+ PGRST_DB_SCHEMA: "${restSchemas}"
481
661
  PGRST_DB_ANON_ROLE: anon
482
662
  PGRST_JWT_SECRET: \${JWT_SECRET:?JWT_SECRET is missing from .env}
483
663
  PGRST_DB_EXTRA_SEARCH_PATH: public,extensions
@@ -491,10 +671,10 @@ ${dbDependency}
491
671
  PORT: 5000
492
672
  DATABASE_URL: "${ownerUrl}"
493
673
  JWT_SECRET: \${JWT_SECRET:?JWT_SECRET is missing from .env}
494
- S3_ENDPOINT: http://minio:9000
674
+ S3_ENDPOINT: http://seaweedfs:8333
495
675
  S3_REGION: us-east-1
496
- S3_ACCESS_KEY: supatype
497
- S3_SECRET_KEY: supatype-secret
676
+ S3_ACCESS_KEY: ${OBJECT_STORE_ACCESS_KEY}
677
+ S3_SECRET_KEY: ${OBJECT_STORE_SECRET_KEY}
498
678
  S3_FORCE_PATH_STYLE: "true"
499
679
  ${dbDependency}
500
680
  functions-worker:
@@ -510,6 +690,10 @@ ${dbDependency}
510
690
  # outside. One worker rather than two: a second container would cost a pod per project on
511
691
  # cloud, for isolation the route boundary already provides.
512
692
  SUPATYPE_HOOKS_ROOT: /project/hooks
693
+ # Every log line the worker writes carries this, and on cloud it is the label Loki indexes
694
+ # each project's logs under. Without it a line is collected and filed under no project, which
695
+ # is the same as losing it.
696
+ SUPATYPE_PROJECT_REF: ${JSON.stringify(config.project.name)}
513
697
  PORT: "8001"
514
698
  # In-compose loopback to Kong (not API_EXTERNAL_URL / localhost, unreachable from this container).
515
699
  SUPATYPE_URL: http://kong:8000
@@ -555,7 +739,17 @@ ${dbDependency}
555
739
  # stays visible instead of hiding in a crash loop.
556
740
  restart: on-failure:5
557
741
  ${serverPorts} volumes:
742
+ # The project is read-only: the server reads the schema, the manifest and the functions, and
743
+ # has no business editing any of them.
558
744
  - ${projectMount}:/project:ro
745
+ # .supatype is the exception, and only because one file in it is not project source.
746
+ # api-config.json is the operator's runtime state: which tables have caching switched on,
747
+ # the project TTL, max_rows. PATCH /admin/v1/config/rest writes it, which is what Studio's
748
+ # cache panel and the CLI's cache commands call. Under the read-only mount alone that PATCH
749
+ # fails with "read-only file system", so a cache a model declares can be read back as
750
+ # declared and never actually switched on. A narrower bind than making the whole project
751
+ # writable, because the rest of the tree keeps the guarantee.
752
+ - ${projectMount}/.supatype:/project/.supatype
559
753
  working_dir: /project
560
754
  environment:
561
755
  SUPATYPE_MODE: ${devLocal ? "dev" : "standalone"}
@@ -573,7 +767,7 @@ ${serverPorts} volumes:
573
767
  SUPATYPE_FUNCTIONS_WORKER_URL: http://functions-worker:8001
574
768
  ${realtimeServerEnv}
575
769
  SUPATYPE_CONTROL_PLANE_URL: http://control-plane:8080
576
- SUPATYPE_VALKEY_ADDR: valkey:6379
770
+ SUPATYPE_VALKEY_ADDR: ${respHost}:6379
577
771
  ${appEnv}
578
772
  SUPATYPE_API_HOST: 0.0.0.0
579
773
  SUPATYPE_API_PORT: 9999
@@ -606,9 +800,7 @@ ${appEnv}
606
800
  SUPATYPE_DISABLE_SIGNUP: \${DISABLE_SIGNUP:-false}
607
801
  ${devLocal ? " STUDIO_OPEN_DEV: \"1\"\n" : ""}
608
802
  depends_on:
609
- ${dbDependencyClause} valkey:
610
- condition: service_started
611
- postgrest:
803
+ ${dbDependencyClause}${keyspaceInPg ? "" : " valkey:\n condition: service_started\n"} postgrest:
612
804
  condition: service_started
613
805
  storage:
614
806
  condition: service_started
@@ -617,15 +809,7 @@ ${dbDependencyClause} valkey:
617
809
  ${realtimeDependency} control-plane:
618
810
  condition: service_started
619
811
 
620
- minio:
621
- image: minio/minio:RELEASE.2024-11-07T00-52-20Z
622
- command: server /data --console-address ":9001"
623
- environment:
624
- MINIO_ROOT_USER: supatype
625
- MINIO_ROOT_PASSWORD: supatype-secret
626
- ${minioPorts} volumes:
627
- - minio-data:/data
628
-
812
+ ${objectStoreBlock}
629
813
  schema-engine:
630
814
  image: \${SUPATYPE_ENGINE_IMAGE:-supatype/schema-engine:latest}
631
815
  profiles: ["tools"]
@@ -642,6 +826,16 @@ ${dbDependency}${studioBlock}${valkeyBlock}${tlsHintComment} kong:
642
826
  KONG_ADMIN_ACCESS_LOG: /dev/stdout
643
827
  KONG_PROXY_ERROR_LOG: /dev/stderr
644
828
  KONG_ADMIN_ERROR_LOG: /dev/stderr
829
+ # Cookies are not scoped by port, so every project a developer runs on localhost shares one
830
+ # cookie jar and every request through this gateway carries the lot, which overruns nginx's
831
+ # default buffers and answers "Request header or cookie too large" about cookies belonging to
832
+ # something else entirely.
833
+ #
834
+ # Kept equal to large_client_header_buffers in packages/studio/nginx.conf, where the reason
835
+ # for the two numbers is written down. Both hops need it and they need the same value: this
836
+ # one carries API calls, that one the page, and a request that clears one and fails the other
837
+ # is worse to diagnose than one that fails outright.
838
+ KONG_NGINX_HTTP_LARGE_CLIENT_HEADER_BUFFERS: "4 32k"
645
839
  ${kongTlsEnv} volumes:
646
840
  - ${kongMount}:/etc/kong/kong.yml:ro
647
841
  ports:
@@ -821,6 +1015,21 @@ function resolveFieldMaskingTier(
821
1015
  return fieldMaskingTierFromProject(cwd, config)
822
1016
  }
823
1017
 
1018
+ /**
1019
+ * Whether this project has a versioned model, unless the caller already knows.
1020
+ *
1021
+ * Same seam as the tier above, and the same reason: the renderer does not load the schema, so a
1022
+ * caller that has can say, and `writeSelfHostCompose` answers for the callers that have not.
1023
+ */
1024
+ function resolveDrafts(
1025
+ cwd: string,
1026
+ config: SupatypeProjectConfig,
1027
+ options?: SelfHostComposeOptions,
1028
+ ): boolean {
1029
+ if (options?.drafts !== undefined) return options.drafts
1030
+ return projectHasVersionedModels(cwd, config)
1031
+ }
1032
+
824
1033
  export function writeSelfHostCompose(
825
1034
  cwd: string,
826
1035
  config: SupatypeProjectConfig,
@@ -831,6 +1040,7 @@ export function writeSelfHostCompose(
831
1040
  const resolved: SelfHostComposeOptions = {
832
1041
  ...options,
833
1042
  ...(tier !== undefined && { fieldMaskingTier: tier }),
1043
+ drafts: resolveDrafts(cwd, config, options),
834
1044
  }
835
1045
  assertExternalUrlReachableFromContainers(config)
836
1046
  const paths = selfHostComposePaths(cwd)
@@ -838,6 +1048,7 @@ export function writeSelfHostCompose(
838
1048
  ensureProjectFunctionsDir(cwd, config)
839
1049
  ensureComposeManifest(cwd, config)
840
1050
  writeFileSync(paths.composePath, renderSelfHostCompose(config, cwd, resolved), "utf8")
1051
+ writeFileSync(paths.s3ConfigPath, renderSeaweedIdentities(), "utf8")
841
1052
  const studioHostDev = options?.devLocal === true && hasStudioOverride(config)
842
1053
  const tlsEnabled = selfHostTlsEnabled(config, options?.devLocal === true)
843
1054
  const domain = config.server.domain?.trim()
@@ -850,8 +1061,18 @@ export function writeSelfHostCompose(
850
1061
  studioServiceUrl: COMPOSE_STUDIO_HOST_URL,
851
1062
  studioStripPath: false,
852
1063
  }),
1064
+ // Kong stores its certificates wherever the RESP server is. With
1065
+ // pg_keyspace that is the database container, and they are durable
1066
+ // there because the keyspace defaults to durable and nothing names
1067
+ // Kong's keys as an exception.
853
1068
  ...(tlsEnabled && domain && acmeEmail
854
- ? { acme: { email: acmeEmail, domain, redisHost: "valkey" } }
1069
+ ? {
1070
+ acme: {
1071
+ email: acmeEmail,
1072
+ domain,
1073
+ redisHost: keyspaceInPostgres(config) ? "db" : "valkey",
1074
+ },
1075
+ }
855
1076
  : {}),
856
1077
  }),
857
1078
  "utf8",
@@ -9,8 +9,22 @@ export function studioAdminRoles(cfg: SupatypeProjectConfig): string[] {
9
9
  return [...DEFAULT_STUDIO_ADMIN_ROLES]
10
10
  }
11
11
 
12
- /** Merge `adminRoles` into engine admin-config JSON for Studio and supatype-server. */
12
+ /**
13
+ * Merge the project's own settings into engine admin-config JSON for Studio and supatype-server.
14
+ *
15
+ * The engine derives everything it can from the schema. These two cannot be derived from it: which
16
+ * JWT roles may open Studio, and where the project's front end renders each model. Both are facts
17
+ * about a deployment rather than about a shape, so they live in `supatype.config.ts` and are folded
18
+ * in here, at the one place the file is written.
19
+ */
13
20
  export function withAdminRoles(admin: unknown, cfg: SupatypeProjectConfig): Record<string, unknown> {
14
21
  const base = typeof admin === "object" && admin !== null ? (admin as Record<string, unknown>) : {}
15
- return { ...base, adminRoles: studioAdminRoles(cfg) }
22
+ const livePreview = cfg.admin?.livePreview
23
+ return {
24
+ ...base,
25
+ adminRoles: studioAdminRoles(cfg),
26
+ // Omitted rather than written empty: Studio treats an absent entry as "this project has not
27
+ // said", which is what drives the message asking for it.
28
+ ...(livePreview !== undefined && Object.keys(livePreview).length > 0 && { livePreview }),
29
+ }
16
30
  }