@supatype/cli 0.2.1 → 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.
Files changed (162) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-test.log +180 -143
  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/cache.d.ts +1 -1
  20. package/dist/commands/cache.js +3 -3
  21. package/dist/commands/cache.js.map +1 -1
  22. package/dist/commands/cloud.d.ts.map +1 -1
  23. package/dist/commands/cloud.js +2 -1
  24. package/dist/commands/cloud.js.map +1 -1
  25. package/dist/commands/deploy.d.ts.map +1 -1
  26. package/dist/commands/deploy.js +2 -1
  27. package/dist/commands/deploy.js.map +1 -1
  28. package/dist/commands/dev.d.ts.map +1 -1
  29. package/dist/commands/dev.js +61 -8
  30. package/dist/commands/dev.js.map +1 -1
  31. package/dist/commands/diff.d.ts.map +1 -1
  32. package/dist/commands/diff.js +2 -1
  33. package/dist/commands/diff.js.map +1 -1
  34. package/dist/commands/functions.d.ts.map +1 -1
  35. package/dist/commands/functions.js +6 -2
  36. package/dist/commands/functions.js.map +1 -1
  37. package/dist/commands/init.d.ts.map +1 -1
  38. package/dist/commands/init.js +13 -9
  39. package/dist/commands/init.js.map +1 -1
  40. package/dist/commands/migrate.d.ts.map +1 -1
  41. package/dist/commands/migrate.js +2 -1
  42. package/dist/commands/migrate.js.map +1 -1
  43. package/dist/commands/push.d.ts.map +1 -1
  44. package/dist/commands/push.js +52 -1
  45. package/dist/commands/push.js.map +1 -1
  46. package/dist/compose-services.d.ts.map +1 -1
  47. package/dist/compose-services.js +2 -1
  48. package/dist/compose-services.js.map +1 -1
  49. package/dist/dev-compose.d.ts.map +1 -1
  50. package/dist/dev-compose.js +18 -7
  51. package/dist/dev-compose.js.map +1 -1
  52. package/dist/engine-floor.d.ts +11 -1
  53. package/dist/engine-floor.d.ts.map +1 -1
  54. package/dist/engine-floor.js +49 -16
  55. package/dist/engine-floor.js.map +1 -1
  56. package/dist/functions-deno-types.d.ts +9 -0
  57. package/dist/functions-deno-types.d.ts.map +1 -1
  58. package/dist/functions-deno-types.js +45 -1
  59. package/dist/functions-deno-types.js.map +1 -1
  60. package/dist/functions-router-gen.d.ts.map +1 -1
  61. package/dist/functions-router-gen.js +113 -81
  62. package/dist/functions-router-gen.js.map +1 -1
  63. package/dist/kong-config.d.ts +1 -1
  64. package/dist/model-cache.d.ts +17 -0
  65. package/dist/model-cache.d.ts.map +1 -0
  66. package/dist/model-cache.js +45 -0
  67. package/dist/model-cache.js.map +1 -0
  68. package/dist/model-hooks.d.ts +5 -0
  69. package/dist/model-hooks.d.ts.map +1 -1
  70. package/dist/model-hooks.js +29 -1
  71. package/dist/model-hooks.js.map +1 -1
  72. package/dist/model-versioning.d.ts +80 -0
  73. package/dist/model-versioning.d.ts.map +1 -0
  74. package/dist/model-versioning.js +140 -0
  75. package/dist/model-versioning.js.map +1 -0
  76. package/dist/postgres-ctl.d.ts +55 -0
  77. package/dist/postgres-ctl.d.ts.map +1 -1
  78. package/dist/postgres-ctl.js +84 -8
  79. package/dist/postgres-ctl.js.map +1 -1
  80. package/dist/preview-config-check.d.ts +21 -0
  81. package/dist/preview-config-check.d.ts.map +1 -0
  82. package/dist/preview-config-check.js +47 -0
  83. package/dist/preview-config-check.js.map +1 -0
  84. package/dist/project-config.d.ts +182 -5
  85. package/dist/project-config.d.ts.map +1 -1
  86. package/dist/project-config.js +125 -8
  87. package/dist/project-config.js.map +1 -1
  88. package/dist/resolve-target.d.ts +9 -0
  89. package/dist/resolve-target.d.ts.map +1 -1
  90. package/dist/resolve-target.js.map +1 -1
  91. package/dist/rest-cache-admin.d.ts.map +1 -1
  92. package/dist/rest-cache-admin.js +6 -2
  93. package/dist/rest-cache-admin.js.map +1 -1
  94. package/dist/schema-ast-v2.d.ts +32 -1
  95. package/dist/schema-ast-v2.d.ts.map +1 -1
  96. package/dist/schema-ast-v2.js +14 -1
  97. package/dist/schema-ast-v2.js.map +1 -1
  98. package/dist/self-host-compose.d.ts +20 -1
  99. package/dist/self-host-compose.d.ts.map +1 -1
  100. package/dist/self-host-compose.js +181 -27
  101. package/dist/self-host-compose.js.map +1 -1
  102. package/dist/studio-admin-roles.d.ts +8 -1
  103. package/dist/studio-admin-roles.d.ts.map +1 -1
  104. package/dist/studio-admin-roles.js +16 -2
  105. package/dist/studio-admin-roles.js.map +1 -1
  106. package/dist/type-extractor.d.ts +7 -0
  107. package/dist/type-extractor.d.ts.map +1 -1
  108. package/dist/type-extractor.js +234 -3
  109. package/dist/type-extractor.js.map +1 -1
  110. package/package.json +2 -2
  111. package/src/api-config-cache.ts +142 -0
  112. package/src/cache-identity-scope.ts +92 -0
  113. package/src/cache-provider.ts +61 -0
  114. package/src/cli-version-embedded.ts +1 -1
  115. package/src/commands/add.ts +1 -1
  116. package/src/commands/cache.ts +3 -3
  117. package/src/commands/cloud.ts +2 -1
  118. package/src/commands/deploy.ts +2 -1
  119. package/src/commands/dev.ts +68 -7
  120. package/src/commands/diff.ts +2 -1
  121. package/src/commands/functions.ts +6 -2
  122. package/src/commands/init.ts +13 -9
  123. package/src/commands/migrate.ts +2 -1
  124. package/src/commands/push.ts +57 -1
  125. package/src/compose-services.ts +2 -1
  126. package/src/dev-compose.ts +18 -7
  127. package/src/engine-floor.ts +73 -16
  128. package/src/functions-deno-types.ts +48 -1
  129. package/src/functions-router-gen.ts +113 -81
  130. package/src/kong-config.ts +1 -1
  131. package/src/model-cache.ts +74 -0
  132. package/src/model-hooks.ts +26 -2
  133. package/src/model-versioning.ts +167 -0
  134. package/src/postgres-ctl.ts +103 -9
  135. package/src/preview-config-check.ts +52 -0
  136. package/src/project-config.ts +273 -11
  137. package/src/resolve-target.ts +11 -1
  138. package/src/rest-cache-admin.ts +8 -2
  139. package/src/schema-ast-v2.ts +47 -0
  140. package/src/self-host-compose.ts +204 -27
  141. package/src/studio-admin-roles.ts +16 -2
  142. package/src/type-extractor.ts +281 -2
  143. package/tests/api-config-cache-seed.test.ts +180 -0
  144. package/tests/cache-identity-scope.test.ts +61 -0
  145. package/tests/cache-provider-config.test.ts +132 -0
  146. package/tests/engine-floor.test.ts +43 -0
  147. package/tests/external-database-compose.test.ts +74 -3
  148. package/tests/field-masking-tier.test.ts +3 -3
  149. package/tests/fixtures/identity-scope-corpus.json +42 -0
  150. package/tests/functions-concurrency.test.ts +71 -0
  151. package/tests/init.test.ts +36 -2
  152. package/tests/model-cache-declaration.test.ts +229 -0
  153. package/tests/model-cache-manifest.test.ts +99 -0
  154. package/tests/model-hooks.test.ts +55 -0
  155. package/tests/model-versions.test.ts +229 -0
  156. package/tests/native-keyspace.test.ts +130 -0
  157. package/tests/preview-config-check.test.ts +104 -0
  158. package/tests/rest-cache-admin.test.ts +36 -0
  159. package/tests/runtime-contract.test.ts +176 -22
  160. package/tests/studio-admin-roles.test.ts +32 -0
  161. package/tests/type-extractor.test.ts +113 -0
  162. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Whether an access rule's outcome varies by *who* is asking.
3
+ *
4
+ * This decides whether a model may declare `cache: { public: true }`. A public cache entry is
5
+ * global — one response served to every caller — so on a table whose read rule varies by caller it
6
+ * serves one user's rows to another.
7
+ *
8
+ * **Row-dependence is not the same question, and getting that wrong in either direction is a bug.**
9
+ * `Lte<"published_at", Now>` varies by row and by time but not by caller, so a shared entry is
10
+ * exactly right for it: that is the case public caching exists to serve. Refusing it would make the
11
+ * feature useless. Allowing `Owner<"user_id">` would make it a data leak.
12
+ *
13
+ * ## This is the second implementation of one rule
14
+ *
15
+ * supatype-server already answers this at runtime, in
16
+ * `internal/studiobootstrap.IsIdentityDependent`, to decide whether a response may be cached under
17
+ * a shared scope. This one answers it at *push* time, where the error can name the model and the
18
+ * rule instead of a request quietly not being cached.
19
+ *
20
+ * Two implementations of one rule drift, and the drift is silent: the push says "shareable" and the
21
+ * runtime says "not", or worse the other way round. `tests/fixtures/identity-scope-corpus.json` is
22
+ * the defence — a shared corpus of rules and expected answers that both sides run against. Change
23
+ * this file and you change that corpus; the Go side reads the same cases.
24
+ */
25
+
26
+ /** A rule node, in the shape the extractor emits and the server parses. */
27
+ export interface AccessRuleNode {
28
+ type?: string
29
+ left?: AccessOperand
30
+ right?: AccessOperand
31
+ operand?: AccessOperand
32
+ rules?: AccessRuleNode[]
33
+ rule?: AccessRuleNode
34
+ }
35
+
36
+ export interface AccessOperand {
37
+ kind?: string
38
+ }
39
+
40
+ /**
41
+ * The operands whose value differs per caller. Columns, literals and the temporal operands
42
+ * (`Now` and friends) are the same for everyone, so they are not here.
43
+ */
44
+ const IDENTITY_OPERANDS = new Set(["authUid", "authRole", "claim"])
45
+
46
+ function operandIsIdentityDependent(o: AccessOperand | undefined): boolean {
47
+ if (!o) return false
48
+ return IDENTITY_OPERANDS.has(o.kind ?? "")
49
+ }
50
+
51
+ export function isIdentityDependent(rule: AccessRuleNode | undefined | null): boolean {
52
+ // No rule at all is not a caller-varying rule. An absent `access.read` means the table's
53
+ // readability is decided elsewhere, and treating that as identity-dependent would refuse public
54
+ // caching on every model that never wrote one.
55
+ if (!rule) return false
56
+
57
+ switch (rule.type) {
58
+ case "public":
59
+ case "private":
60
+ return false
61
+
62
+ // "Any authenticated user" still distinguishes signed-in from anonymous, so a shared entry
63
+ // could serve a member's view to a stranger.
64
+ case "authenticated":
65
+ case "role":
66
+ case "owner":
67
+ return true
68
+
69
+ // Membership is per caller by construction.
70
+ case "in":
71
+ case "exists":
72
+ return true
73
+
74
+ case "compare":
75
+ return operandIsIdentityDependent(rule.left) || operandIsIdentityDependent(rule.right)
76
+
77
+ case "nullCheck":
78
+ return operandIsIdentityDependent(rule.operand)
79
+
80
+ case "any":
81
+ case "all":
82
+ return (rule.rules ?? []).some(isIdentityDependent)
83
+
84
+ case "not":
85
+ return rule.rule ? isIdentityDependent(rule.rule) : false
86
+
87
+ default:
88
+ // An unrecognised rule is assumed to depend on identity. Guessing the other way would share
89
+ // a cache entry across callers on the strength of not recognising something.
90
+ return true
91
+ }
92
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Which RESP server this stack runs: one decision, in one place.
3
+ *
4
+ * The response cache and Kong's ACME certificates share a RESP server. Since P1 that can be
5
+ * pg_keyspace inside the Postgres container — one stateful service instead of two, no second
6
+ * image to keep patched, and a cache whose memory is accounted for in the database's own
7
+ * reservation. That is now the default rather than the option.
8
+ *
9
+ * **Valkey stays selectable**, and stays the answer for `database.external`: pg_keyspace is loaded
10
+ * through `shared_preload_libraries`, which is a property of a Postgres this stack starts. Against
11
+ * a database Supatype does not manage there is nothing to configure, so the fallback is not a
12
+ * preference — it is the only thing that can run.
13
+ *
14
+ * A resolver rather than a default written into the config type, because the answer depends on
15
+ * more than one field: the same absent `cache.provider` means pg_keyspace beside a managed
16
+ * database and Valkey beside an external one. A default baked into the parser would have to be
17
+ * either wrong for one of them or applied twice, and applied twice is how the compose file and the
18
+ * native path end up disagreeing about which server is running — a cache that silently never hits
19
+ * and, with TLS on, a Kong that cannot store its certificate.
20
+ */
21
+ import { pinnedVersion } from "./binary-cache.js"
22
+ import type { SupatypeProjectConfig } from "./project-config.js"
23
+
24
+ export type CacheProvider = "valkey" | "pg_keyspace"
25
+
26
+ /**
27
+ * The provider this project runs, explicit or resolved.
28
+ *
29
+ * An explicit `cache.provider` is honoured exactly — including `"valkey"`, which is how a project
30
+ * keeps the sidecar it has. `validateProjectConfig` has already refused the one explicit
31
+ * combination that cannot run (pg_keyspace against `database.external`), so this never overrides
32
+ * what someone wrote.
33
+ *
34
+ * The two things that resolve an absent one to Valkey are both cases where this cannot know that a
35
+ * keyspace would work:
36
+ *
37
+ * - **`database.external`**: pg_keyspace is loaded through `shared_preload_libraries`, and there
38
+ * is no container here to set that on.
39
+ * - **A pinned `versions.postgres`**: the keyspace is served by the image, and the entrypoint
40
+ * honours `SUPATYPE_KEYSPACE_ENABLED` only from the release that introduced it. An older image
41
+ * ignores the variable and starts with no RESP listener, which is not an error anywhere — the
42
+ * stack comes up and the cache never hits. A pin says this project's image is fixed, and a
43
+ * default must not assume something about a fixed image it cannot check. Unpinned tracks
44
+ * latest, which carries the toggle by definition.
45
+ *
46
+ * A project on a pin that *does* carry the toggle says `cache: { provider: "pg_keyspace" }` and
47
+ * gets it — which is also the only instruction anyone needs, and is better than a version floor
48
+ * here that would have to be right about a release number in another repository.
49
+ */
50
+ export function cacheProvider(config: SupatypeProjectConfig): CacheProvider {
51
+ const declared = config.cache?.provider
52
+ if (declared === "valkey" || declared === "pg_keyspace") return declared
53
+ if (config.database?.external) return "valkey"
54
+ if (pinnedVersion("postgres", config) !== undefined) return "valkey"
55
+ return "pg_keyspace"
56
+ }
57
+
58
+ /** Whether the RESP keyspace is served by the Postgres this stack starts. */
59
+ export function keyspaceInPostgres(config: SupatypeProjectConfig): boolean {
60
+ return cacheProvider(config) === "pg_keyspace"
61
+ }
@@ -9,4 +9,4 @@
9
9
  // Annotated `string`, not left to inference: once a release stamps a value here, the
10
10
  // inferred literal type makes the `!== ""` test in cliPackageVersion() a compile error
11
11
  // (TS2367, no overlap), so the build would only fail during a release.
12
- export const EMBEDDED_CLI_VERSION: string = "0.2.1"
12
+ export const EMBEDDED_CLI_VERSION: string = "0.3.0"
@@ -101,5 +101,5 @@ function printDomainNextSteps(cwd: string, domain: string): void {
101
101
  `Platform URL: https://${domain}`,
102
102
  ])
103
103
  plain(" App, REST, Auth, Storage, Realtime, Functions, and Studio, one HTTPS domain.")
104
- plain(" Certificates persist in the valkey-data volume.\n")
104
+ plain(" Certificates outlive restarts — in the keyspace's durable store, or the valkey-data volume.\n")
105
105
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * supatype cache: list and clean cached component binaries + REST Valkey cache.
2
+ * supatype cache: list and clean cached component binaries + the REST response cache.
3
3
  */
4
4
 
5
5
  import type { Command } from "commander"
@@ -135,7 +135,7 @@ export function registerCache(program: Command): void {
135
135
 
136
136
  const rest = cache
137
137
  .command("rest")
138
- .description("Manage REST API response cache in Valkey")
138
+ .description("Manage the server-side REST API response cache")
139
139
 
140
140
  rest
141
141
  .command("list")
@@ -170,7 +170,7 @@ export function registerCache(program: Command): void {
170
170
 
171
171
  rest
172
172
  .command("get <key>")
173
- .description("Show one cache entry (full Valkey key)")
173
+ .description("Show one cache entry (full key)")
174
174
  .option("--json", "Output JSON")
175
175
  .action(async (key: string, opts: { json?: boolean }) => {
176
176
  const cwd = process.cwd()
@@ -1,3 +1,4 @@
1
+ import { withPublishing } from "../model-versioning.js"
1
2
  import type { Command } from "commander"
2
3
  import { readFileSync, writeFileSync, existsSync } from "node:fs"
3
4
  import { resolve } from "node:path"
@@ -91,7 +92,7 @@ export async function pushSchemaToLinkedProject(
91
92
  process.exit(1)
92
93
  }
93
94
 
94
- const ast = loadSchemaAst(schemaPathFromProject(config, cwd), cwd)
95
+ const ast = withPublishing(loadSchemaAst(schemaPathFromProject(config, cwd), cwd), config)
95
96
  info(`Pushing schema to ${target.mode} project ${target.projectRef} (${target.environment})...`)
96
97
 
97
98
  const result = await targetSchemaPush(target, ast, {
@@ -11,6 +11,7 @@
11
11
  * supatype deploy logs <version>, show build logs
12
12
  */
13
13
 
14
+ import { withPublishing } from "../model-versioning.js"
14
15
  import type { Command } from "commander"
15
16
  import { existsSync, readdirSync, statSync, createReadStream, mkdirSync, cpSync } from "node:fs"
16
17
  import { join, relative } from "node:path"
@@ -74,7 +75,7 @@ export function registerDeploy(program: Command): void {
74
75
 
75
76
  // Step 1: Schema push (unless --app-only or --skip-build, or already done via cloud.json)
76
77
  if (!opts.appOnly && !opts.skipBuild && !schemaDone) {
77
- const ast = loadSchemaAst(schemaPathFromProject(config, cwd), cwd)
78
+ const ast = withPublishing(loadSchemaAst(schemaPathFromProject(config, cwd), cwd), config)
78
79
 
79
80
  if (opts.local) {
80
81
  step("Schema Push (local)")
@@ -10,6 +10,7 @@
10
10
  * Deno is not provisioned by the CLI on those paths.
11
11
  */
12
12
 
13
+ import { keyspaceInPostgres } from "../cache-provider.js"
13
14
  import type { Command } from "commander"
14
15
  import { spawnSync } from "node:child_process"
15
16
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
@@ -26,6 +27,7 @@ import {
26
27
  } from "../project-config.js"
27
28
  import { discoverTsFunctionsInDir, writeDevFunctionsRouter } from "../functions-router-gen.js"
28
29
  import { fieldMaskingTierFromProject } from "../field-masking-tier.js"
30
+ import { projectHasVersionedModels } from "../model-versioning.js"
29
31
  import { signJwt } from "../jwt.js"
30
32
  import {
31
33
  devAuthenticatorPassword,
@@ -55,9 +57,13 @@ import { registerDevShutdown } from "../dev-shutdown.js"
55
57
  import { patchRouteManifest } from "../route-manifest.js"
56
58
  import { resolveRealtimeLaunch } from "../realtime-launch.js"
57
59
  import { writeAppViteEnv } from "../app-vite-env.js"
58
- import { ensureValkeySidecar, stopValkeySidecar } from "../valkey-sidecar.js"
60
+ import { ensureValkeySidecar, probeTcp, stopValkeySidecar } from "../valkey-sidecar.js"
59
61
  import {
62
+ firstFreePort,
60
63
  initdb,
64
+ KEYSPACE_PORT_BASE,
65
+ KEYSPACE_PORT_SPAN,
66
+ nativeKeyspaceLibraryPresent,
61
67
  nativeMaskLibraryPresent,
62
68
  start as pgStart,
63
69
  stop as pgStop,
@@ -193,13 +199,46 @@ export function registerDev(program: Command): void {
193
199
  // pgBinDir is set on the native path and used to add DLL search path for
194
200
  // PostgREST on Windows (PostgREST links against libpq + SSL from MinGW).
195
201
  let pgBinDir: string | null = null
202
+ // The port the native Postgres serves RESP on, or null when this archive
203
+ // has no pg_keyspace and the Valkey sidecar is what the cache talks to.
204
+ let keyspacePort: number | null = null
196
205
 
197
206
  {
198
207
  // native: resolve pg bin dir and manage with pg_ctl
199
208
  pgBinDir = await resolvePgBinDir(config)
200
209
  const dataDir = config.database.data_dir ?? join(stateRoot, "data")
201
210
  mkdirSync(dataDir, { recursive: true })
202
- const pgOpts = { pgBinDir, dataDir, port: pgPort, logPath: join(logsDir, "postgres.log") }
211
+ // The RESP keyspace, when this archive carries it: one process instead
212
+ // of a Postgres and a Valkey container. The port is chosen rather than
213
+ // fixed because 6379 is the first thing a developer's own Redis or a
214
+ // leftover sidecar takes, and a keyspace that cannot bind its port
215
+ // fails inside the postmaster log where nobody is looking.
216
+ //
217
+ // `cache.provider: "valkey"` is honoured here too, and this is the only place it could
218
+ // be: the native path asks the archive what it carries rather than the config what it
219
+ // wants, so a project that deliberately kept the sidecar would otherwise find Postgres
220
+ // serving RESP anyway and the sidecar never started.
221
+ const wantsPgKeyspace = keyspaceInPostgres(config)
222
+ keyspacePort =
223
+ wantsPgKeyspace && nativeKeyspaceLibraryPresent(pgBinDir)
224
+ ? await firstFreePort(KEYSPACE_PORT_BASE, KEYSPACE_PORT_SPAN)
225
+ : null
226
+ if (wantsPgKeyspace && nativeKeyspaceLibraryPresent(pgBinDir) && keyspacePort === null) {
227
+ console.warn(
228
+ `[supatype] ⚠ No free port in ${KEYSPACE_PORT_BASE}-${KEYSPACE_PORT_BASE + KEYSPACE_PORT_SPAN - 1} ` +
229
+ "for the Postgres keyspace — starting without it.",
230
+ )
231
+ }
232
+
233
+ const pgOpts = {
234
+ pgBinDir,
235
+ dataDir,
236
+ port: pgPort,
237
+ logPath: join(logsDir, "postgres.log"),
238
+ ...(keyspacePort !== null
239
+ ? { keyspacePort, keyspaceDatabase: projectName }
240
+ : {}),
241
+ }
203
242
 
204
243
  console.log("[supatype] Initialising Postgres data directory...")
205
244
  initdb(pgOpts)
@@ -399,7 +438,27 @@ ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO authenticate
399
438
  ? "ses"
400
439
  : "smtp"
401
440
 
402
- const valkeySidecar = ensureValkeySidecar(projectName)
441
+ // Postgres serves RESP itself when the archive had pg_keyspace and the
442
+ // port came up. The probe is not ceremony: shared memory limits, a
443
+ // preload ordering mistake or a port taken between the check and the
444
+ // start all leave a Postgres that is running and a keyspace that is not,
445
+ // and the difference is a cache that silently never hits.
446
+ const nativeKeyspaceAddr =
447
+ keyspacePort !== null && (await probeTcp("127.0.0.1", keyspacePort))
448
+ ? `127.0.0.1:${keyspacePort}`
449
+ : null
450
+ if (keyspacePort !== null && nativeKeyspaceAddr === null) {
451
+ console.warn(
452
+ `[supatype] ⚠ Postgres has pg_keyspace but is not serving RESP on :${keyspacePort} — ` +
453
+ "see logs/postgres.log. Falling back to the Valkey sidecar.",
454
+ )
455
+ }
456
+ if (nativeKeyspaceAddr) {
457
+ console.log(`[supatype] Keyspace served by Postgres (${nativeKeyspaceAddr}).`)
458
+ }
459
+ const valkeySidecar = nativeKeyspaceAddr
460
+ ? { addr: nativeKeyspaceAddr, containerName: null, started: false }
461
+ : ensureValkeySidecar(projectName)
403
462
 
404
463
  const serverEnv: Record<string, string> = {
405
464
  // supatype-server outer layer
@@ -513,12 +572,14 @@ ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO authenticate
513
572
  // a non-default pg_schema pushed correctly and then answered PGRST106 on every request.
514
573
  // Native dev has no masking extension either, so a schema with field rules is served from
515
574
  // `api` here too: the case an "is the database external" rule would have got wrong.
516
- PGRST_DB_SCHEMA: apiSchemaList(
517
- config,
575
+ PGRST_DB_SCHEMA: apiSchemaList(config, {
518
576
  // The installed archive decides: one downloaded before the masking library was bundled
519
577
  // has no extension, so its field rules are enforced by views instead.
520
- fieldMaskingTierFromProject(cwd, config, nativeMaskLibraryPresent(pgBinDir)),
521
- ),
578
+ tier: fieldMaskingTierFromProject(cwd, config, nativeMaskLibraryPresent(pgBinDir)),
579
+ // A versioned model adds the generated `draft` schema, or `.draft()` would answer
580
+ // PGRST106 against a stack that had migrated the views into place.
581
+ drafts: projectHasVersionedModels(cwd, config),
582
+ }),
522
583
  // Parity with self-host, which has always set this. Unqualified names in column defaults
523
584
  // (`uuid_generate_v4()`) resolve here, and with a non-public pg_schema the request schema
524
585
  // alone does not reach them.
@@ -1,3 +1,4 @@
1
+ import { withPublishing } from "../model-versioning.js"
1
2
  import type { Command } from "commander"
2
3
  import { loadConfig, loadSchemaAst } from "../config.js"
3
4
  import { resolveRuntimeProvider, schemaPathFromProject } from "../project-config.js"
@@ -19,7 +20,7 @@ export function registerDiff(program: Command): void {
19
20
  const pgSchema = schemaPgSchema(cwd)
20
21
 
21
22
  const ast = await withSpinner("Loading schema", async () =>
22
- loadSchemaAst(schemaPathFromProject(config, cwd), cwd),
23
+ withPublishing(loadSchemaAst(schemaPathFromProject(config, cwd), cwd), config),
23
24
  )
24
25
 
25
26
  const linked = loadProjectLink(cwd)
@@ -141,9 +141,13 @@ function scaffoldFunction(cwd: string, name: string): void {
141
141
  const indexContent = `// ${name}, Supatype Edge Function
142
142
  // Docs: https://supatype.com/docs/edge-functions
143
143
 
144
- export default async function handler(req: Request): Promise<Response> {
144
+ import type { FunctionContext } from "../_shared/context.ts"
145
+
146
+ export default async function handler(req: Request, ctx: FunctionContext): Promise<Response> {
145
147
  const { method } = req
146
- const url = Deno.env.get("SUPATYPE_URL")
148
+ // From the context, not the environment. Per-invocation values are passed in, which is what lets
149
+ // the worker run your functions concurrently instead of one at a time.
150
+ const { url } = ctx
147
151
 
148
152
  // Example: read request body for POST requests
149
153
  if (method === "POST") {
@@ -70,7 +70,7 @@ const STORAGE_PROVIDER_OPTIONS: {
70
70
  label: string
71
71
  hint: string
72
72
  }[] = [
73
- { value: "local", label: "Local", hint: "storage you host yourself (MinIO)" },
73
+ { value: "local", label: "Local", hint: "storage you host yourself (SeaweedFS)" },
74
74
  { value: "s3", label: "S3", hint: "external bucket (AWS S3 or compatible)" },
75
75
  ]
76
76
 
@@ -1164,7 +1164,7 @@ function storageConfigLines(
1164
1164
  if (storageProduction === "s3" && storageLocal !== "s3") {
1165
1165
  lines.push(` // Production storage: external S3 bucket, set production S3_* in .env`)
1166
1166
  } else if (storageProduction === "local" && storageLocal === "s3") {
1167
- lines.push(` // Production storage: MinIO on your server (included in self-host compose)`)
1167
+ lines.push(` // Production storage: SeaweedFS on your server (included in self-host compose)`)
1168
1168
  }
1169
1169
  return lines
1170
1170
  }
@@ -1305,7 +1305,7 @@ S3_SECRET_KEY=`
1305
1305
  }
1306
1306
  return `${localStorageEnvSection("local")}
1307
1307
 
1308
- # Production storage (MinIO on your server)
1308
+ # Production storage (SeaweedFS on your server)
1309
1309
  # Included in the self-host compose stack, no extra configuration needed.`
1310
1310
  }
1311
1311
 
@@ -1323,8 +1323,8 @@ S3_BUCKET=
1323
1323
  S3_ACCESS_KEY=
1324
1324
  S3_SECRET_KEY=`
1325
1325
  }
1326
- return `# Storage (local development, MinIO)
1327
- S3_ENDPOINT=http://localhost:9000
1326
+ return `# Storage (local development, SeaweedFS)
1327
+ S3_ENDPOINT=http://localhost:8333
1328
1328
  S3_ACCESS_KEY=supatype
1329
1329
  S3_SECRET_KEY=supatype-secret`
1330
1330
  }
@@ -1338,7 +1338,7 @@ S3_BUCKET=
1338
1338
  S3_ACCESS_KEY=
1339
1339
  S3_SECRET_KEY=`
1340
1340
  }
1341
- return `# Storage (production, MinIO on your server)
1341
+ return `# Storage (production, SeaweedFS on your server)
1342
1342
  # Included in the self-host compose stack, no extra configuration needed.`
1343
1343
  }
1344
1344
 
@@ -1372,9 +1372,13 @@ function helloFunctionTemplate(): string {
1372
1372
  return `// hello: Supatype Edge Function
1373
1373
  // Docs: https://supatype.com/docs/edge-functions
1374
1374
 
1375
- export default async function handler(req: Request): Promise<Response> {
1375
+ import type { FunctionContext } from "../_shared/context.ts"
1376
+
1377
+ export default async function handler(req: Request, ctx: FunctionContext): Promise<Response> {
1376
1378
  const { method } = req
1377
- const url = Deno.env.get("SUPATYPE_URL")
1379
+ // From the context, not the environment. Per-invocation values are passed in, which is what lets
1380
+ // the worker run your functions concurrently instead of one at a time.
1381
+ const { url } = ctx
1378
1382
 
1379
1383
  if (method === "POST") {
1380
1384
  const body = await req.json()
@@ -1507,7 +1511,7 @@ function printNextSteps(args: {
1507
1511
  selfHostSteps.push("supatype self-host compose up -d # Kong provisions HTTPS automatically")
1508
1512
  selfHostSteps.push(`Your Supatype platform goes live at https://${domain}`)
1509
1513
  selfHostSteps.push(
1510
- "Your app, REST, Auth, Storage, Realtime, Functions, and Studio, all behind one HTTPS domain (certs persist in valkey-data)",
1514
+ "Your app, REST, Auth, Storage, Realtime, Functions, and Studio, all behind one HTTPS domain (certificates outlive restarts)",
1511
1515
  )
1512
1516
  } else {
1513
1517
  selfHostSteps.push(
@@ -1,3 +1,4 @@
1
+ import { withPublishing } from "../model-versioning.js"
1
2
  import type { Command } from "commander"
2
3
  import { join } from "node:path"
3
4
  import { loadConfig, loadSchemaAst } from "../config.js"
@@ -149,7 +150,7 @@ export function registerMigrate(program: Command): void {
149
150
  const cwd = process.cwd()
150
151
 
151
152
  await ensureEngine()
152
- const ast = loadSchemaAst(schemaPathFromProject(config, cwd), cwd)
153
+ const ast = withPublishing(loadSchemaAst(schemaPathFromProject(config, cwd), cwd), config)
153
154
  const result = await engineRequest<{ message?: string }>("/push", {
154
155
  ast,
155
156
  database_url: connection,
@@ -1,3 +1,4 @@
1
+ import { withPublishing } from "../model-versioning.js"
1
2
  import type { Command } from "commander"
2
3
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
3
4
  import { join } from "node:path"
@@ -19,6 +20,10 @@ import {
19
20
  } from "../project-config.js"
20
21
  import { ensureEngine, engineRequest, type DiffResult } from "../engine-client.js"
21
22
  import { assertEngineSupportsSchema } from "../engine-floor.js"
23
+ import {
24
+ modelsWithUnresolvablePreview,
25
+ unresolvablePreviewMessage,
26
+ } from "../preview-config-check.js"
22
27
  import { pinnedVersion } from "../binary-cache.js"
23
28
  import { printDiffOperations, printDiffWarnings } from "../diff-output.js"
24
29
  import { signJwt } from "../jwt.js"
@@ -27,6 +32,7 @@ import type { ExtractedSchemaAstV2 } from "../schema-ast-v2.js"
27
32
  import { ensureFirstAdminUser } from "./admin.js"
28
33
  import { withAdminRoles } from "../studio-admin-roles.js"
29
34
  import { restoreSystemRelationTargets } from "../restore-system-relation-targets.js"
35
+ import { freeTierCacheNote, seedApiConfigCache } from "../api-config-cache.js"
30
36
  import type { SupatypeProjectConfig } from "../project-config.js"
31
37
  import {
32
38
  resolveTarget,
@@ -71,10 +77,11 @@ export function registerPush(program: Command): void {
71
77
  const cwd = process.cwd()
72
78
  const config = loadConfig(cwd)
73
79
  const pgSchema = schemaPgSchema(cwd)
74
- const ast = loadSchemaAst(schemaPathFromProject(config, cwd), cwd)
80
+ const ast = withPublishing(loadSchemaAst(schemaPathFromProject(config, cwd), cwd), config)
75
81
  assertModelHooksResolve(cwd, config, ast)
76
82
  assertServiceRoleGrantsResolve(cwd, config)
77
83
  assertEngineSupportsSchema(ast, pinnedVersion("engine", config))
84
+ assertPreviewAddressesResolve(config)
78
85
 
79
86
  const linked = loadProjectLink(cwd)
80
87
  const useDirect = opts.direct || opts.local || Boolean(opts.connection)
@@ -175,6 +182,7 @@ async function pushViaTarget(
175
182
  }
176
183
  await generateTypesLocal(ast, config)
177
184
  await provisionLocalStorage(ast, config)
185
+ reportCacheSeeding(cwd, ast)
178
186
 
179
187
  // Local Studio only: a cloud/self-host push must not advertise the local
180
188
  // gateway URL from config, which may not even be running.
@@ -186,6 +194,12 @@ async function pushViaTarget(
186
194
  }
187
195
 
188
196
  info(`Pushed to ${target.mode} (${target.environment}).`)
197
+ // Cloud only. A self-host control plane has tiers too and they mean nothing there, so the same
198
+ // sentence would be telling a self-hosted project to upgrade something it already has.
199
+ if (target.mode === "cloud") {
200
+ const note = freeTierCacheNote((pushResult as { cache?: { tables?: string[]; honoured?: boolean } }).cache)
201
+ if (note) info(note)
202
+ }
189
203
  const envUrl = target.link?.environments?.[target.environment]?.apiUrl?.replace(/\/$/, "")
190
204
  if (envUrl) {
191
205
  plain(`\nProject API: ${envUrl}`)
@@ -239,6 +253,33 @@ async function deployHooksToTarget(
239
253
  ).then(() => info(`Deployed ${upload.handlers.length} hook(s)`))
240
254
  }
241
255
 
256
+ /**
257
+ * Switch on what the schema newly declares, and say what is still in the way.
258
+ *
259
+ * Two things have to be true for a local `.cache({ server: true })` to be served: the schema
260
+ * declares the table, and the runtime allowlist has it on. A push writes the first, so without
261
+ * this the second is a trip to Studio that nothing in the output asks for — the symptom being a
262
+ * BYPASS header and a declaration that appears to do nothing.
263
+ *
264
+ * Never an error, and never a rewrite of an entry that already exists. See `seedApiConfigCache`.
265
+ */
266
+ function reportCacheSeeding(cwd: string, ast: unknown): void {
267
+ const result = seedApiConfigCache(cwd, ast)
268
+ if (result === null) return
269
+
270
+ if (result.seeded.length > 0) {
271
+ info(`Server cache enabled for ${result.seeded.join(", ")} in .supatype/api-config.json`)
272
+ }
273
+ if (result.ttlIsOff) {
274
+ // A note rather than a fix. Zero is an off switch someone may have chosen, and a push that
275
+ // turned caching on project-wide would be overriding a decision rather than filling in a blank.
276
+ info(
277
+ `${result.declared.length} table(s) declare a cache, but cache_max_ttl is 0 — nothing is ` +
278
+ `cached until it is set, under API → REST → Settings or in .supatype/api-config.json.`,
279
+ )
280
+ }
281
+ }
282
+
242
283
  /** The generated adapter, flattened to the name handlers were rewritten to import. */
243
284
  function readGeneratedAdapter(hooksDir: string): { name: string; source: string } | null {
244
285
  const file = join(hooksDir, "_supatype", "hooks.ts")
@@ -295,6 +336,21 @@ async function writeLocalAdminConfig(ast: unknown, config: SupatypeProjectConfig
295
336
  writeFileSync(join(dir, "admin-config.json"), `${JSON.stringify(admin, null, 2)}\n`)
296
337
  }
297
338
 
339
+ /**
340
+ * Stop the push when a preview address could never open.
341
+ *
342
+ * Refused here rather than left to fail later, because later means in front of whoever was sent the
343
+ * link, not in front of whoever configured it.
344
+ */
345
+ function assertPreviewAddressesResolve(config: SupatypeProjectConfig): void {
346
+ const models = modelsWithUnresolvablePreview(config)
347
+ if (models.length === 0) return
348
+ fatalError(unresolvablePreviewMessage(models), [
349
+ 'Give an absolute URL: urlPattern: "https://example.com/preview/{slug}"',
350
+ 'Or set app.mode to "static" or "proxy", if this deployment should serve the app.',
351
+ ])
352
+ }
353
+
298
354
  /**
299
355
  * Stop the push when a declared hook names a function that is not there.
300
356
  *
@@ -4,7 +4,8 @@
4
4
  * `status` and `logs` each used to carry a hardcoded list of container names.
5
5
  * Both lists were wrong: they named containers the compose file does not create
6
6
  * (`supatype-postgres`, `supatype-kong`), omitted every service it does create
7
- * (`server`, `storage`, `functions-worker`, `schema-engine`, `valkey`), and used
7
+ * (`server`, `storage`, `functions-worker`, `schema-engine`, and `valkey` when the
8
+ * stack runs one), and used
8
9
  * a naming scheme the compose file never applies, since it sets no
9
10
  * `container_name` and Docker therefore names containers
10
11
  * `<project>-<service>-<n>`.