Skip to content

Commit 9f38cdc

Browse files
os-zhuangclaude
andauthored
docs: implementation-accuracy audit of the #4212-family affected docs (#4312)
* docs: checkpoint the docs-accuracy audit pass over 18 hand-written docs WIP checkpoint — the adversarial verification pass is still running and will add repairs on top of this commit. Recorded now so the work survives. An audit agent read each doc, located the backing implementation under packages/, and applied evidence-backed fixes in place (158 across 18 docs). Every fix carries file:line evidence in the run's fix log. The heaviest: - kernel/cluster.mdx (22): documented an `eventBus.emit(...)` API that exists nowhere in the codebase, a `ctx.cluster` PluginContext property that does not exist, `defineStack({ cluster })` that stack.zod.ts silently strips, fabricated `postgres`/`nats` drivers, the wrong env var (real one is OS_CLUSTER_DRIVER), and an at-least-once delivery promise neither shipped driver provides. - protocol/kernel/plugin-spec.mdx (20): `plugin.manifest.ts` as the manifest filename — grep-empty across the implementation, the real one is objectstack.plugin.json; `context.plugins.isInstalled()` which does not exist; compound semver ranges ('>=1.0.0 <2.0.0') that make the resolver's anchored parse throw; init described as registration order when bootstrap() runs the dependency-topological order. - data-modeling/external-datasources.mdx (3): credentialsRef samples used a `secret:` prefix the shipped SecretBinder cannot dereference (`sys_secret:`), so copying the sample fails closed at connect. - deployment/migration-from-objectql.mdx (2): a `^3.1.0` pin that can never resolve to the current 16.1.0 line. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HrRNgrWaRtggzmrHpbomyh * docs: implementation-accuracy audit of the #4212-family affected docs Ran the repo's docs-accuracy-audit workflow scoped to the drift-check advisory from #4242 (14 docs) plus 4 the retired-surface grep implicated. 158 evidence-backed fixes across 18 docs, each cited to file:line. The largest classes: - kernel/cluster.mdx (22): a documented `eventBus.emit(...)` that exists nowhere in packages/, a `ctx.cluster` PluginContext property that does not exist, `defineStack({ cluster })` that stack.zod.ts silently strips, `postgres`/`nats` listed as shipped drivers when neither package exists, `REDIS_URL` for the real `OS_CLUSTER_DRIVER`, and an at-least-once delivery promise no shipped driver provides. - protocol/kernel/plugin-spec.mdx (20): `plugin.manifest.ts` as the manifest filename — grep-empty across packages/, apps/ and examples/; the real one is `objectstack.plugin.json`. Plus `context.plugins.isInstalled()` (no such member), compound semver ranges (`'>=1.0.0 <2.0.0'`) that make the resolver throw, and init ordering described as registration order when bootstrap() runs a dependency topological sort. - data-modeling/external-datasources.mdx: `credentialsRef: 'secret:…'` — the shipped binder only resolves `sys_secret:<id>`, so the sample as written fails closed at connect. - deployment/migration-from-objectql.mdx: a `^3.1.0` pin that can never resolve to the current 16.1.0 line. VERIFICATION STATUS — 7 of 18 docs carry an adversarial-verifier pass; the other 11 do not (the run hit the session limit mid-phase). The verifier earned its keep on the 7 it reached: 8 repairs, including a fabricated `enforceNetworkAccess` (real: `enforceNetworkRequest`, plugin-permission-enforcer.ts:212) and an over-broad retirement claim that would have contradicted the live app-bundle `onEnable`. The remaining 11 are committed unverified and MUST NOT ship until their verifier pass runs — the PR stays draft until then. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HrRNgrWaRtggzmrHpbomyh * chore(lint): ratchet the role-word baseline down after the docs audit The audit pass removed a "role" occurrence from kernel/events.mdx (now clean) and one from kernel/services-checklist.mdx (4 → 3). check-role-word is a ratchet, so an improvement fails the gate until the baseline records it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HrRNgrWaRtggzmrHpbomyh * docs: apply adversarial-verifier repairs (metadata-lifecycle, protocol/kernel/index) Verifier pass for two more of the 18 audited docs completed: 27 of the audit's fixes confirmed against code, 5 repaired. Checkpoint commit — the remaining verifier passes are still running and will land separately. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HrRNgrWaRtggzmrHpbomyh * docs: apply the remaining adversarial-verifier repairs Completes the verifier phase for all 18 audited docs (36/36 agents, 0 errors). Across the run: 158 audit fixes, 29 verifier repairs, 91 residual items recorded rather than guessed at. The repairs are why this phase exists — the audit agents were confident and sometimes wrong. Representative catches: - `enforceNetworkAccess` invented for the real `enforceNetworkRequest` (plugin-permission-enforcer.ts:212). - A retirement callout widened to "None of it existed" over a list containing `onEnable`, which would have contradicted the live app-bundle contract AppPlugin.start() dispatches (app-plugin.ts:574). - `cluster.lock.withLock(key, fn)` attributed to service-job's cron scheduler, which actually does `lock.acquire(...)` + manual release (cron-job-adapter.ts:139-147). - A "(planned)" marker dropped from the webhook dispatcher, which filed an already-shipping partitioned dispatcher under "Not started". Gates: check-doc-authoring 215 files clean; check-role-word OK (44 baselined); docs build compiles all pages. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HrRNgrWaRtggzmrHpbomyh --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 7967133 commit 9f38cdc

20 files changed

Lines changed: 1005 additions & 531 deletions
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
---
3+
4+
docs: implementation-accuracy audit of the #4212-family affected docs. Fixes
5+
fabricated APIs, wrong manifest filenames, unresolvable version pins and
6+
credential-ref formats. Releases nothing.

content/docs/concepts/metadata-lifecycle.mdx

Lines changed: 54 additions & 29 deletions
Large diffs are not rendered by default.

content/docs/data-modeling/external-datasources.mdx

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ export const Warehouse = defineDatasource({
3737
schemaMode: 'external', // ObjectStack never runs DDL here
3838
config: { host: 'db.internal', port: 5432, database: 'analytics', user: 'readonly' },
3939
external: {
40-
allowWrites: false, // read-only (the default)
41-
credentialsRef: 'secret:warehouse/password', // resolved from the secret store
40+
allowWrites: false, // read-only (the default)
41+
credentialsRef: 'sys_secret:9f2c…', // opaque handle minted by the secret store
4242
validation: { onMismatch: 'fail', checkOnBoot: true },
4343
},
4444
active: true,
@@ -90,11 +90,12 @@ That's it. `GET /api/v1/data/ext_customer` now returns live rows from the remote
9090
`customers` table; filters (`?region=EU`) push down to the remote query.
9191

9292
<Callout type="warn">
93-
**Do not set `field.columnName` on an external object.** For federated objects the
94-
driver's query pipeline ignores `field.columnName`; `external.columnMap`
95-
(`remoteColumn → localField`) is the single, authoritative column mapping.
96-
`os build` / `os validate` rejects `field.columnName` on an external object with a
97-
clear error (ADR-0062 D7). Managed objects are unaffected.
93+
**`field.columnName` no longer exists.** It was removed from `FieldSchema` in the
94+
16.x line (#2377, ADR-0049) — the SQL driver always used the field key as the
95+
physical column, so a custom column name was silently ignored — and the
96+
ADR-0062 D7 lint that rejected it on external objects was removed with it.
97+
`external.columnMap` (`remoteColumn → localField`) is the single, authoritative
98+
column mapping for a federated object.
9899
</Callout>
99100

100101
## 3. Auto-connect — no `onEnable` needed
@@ -121,9 +122,10 @@ from external configuration. Auto-connect is idempotent with it (whichever
121122
registers the datasource name first wins), so the two never conflict.
122123
</Callout>
123124

124-
A connected external datasource is also visible in **Setup → Datasources** (stamped
125-
`origin: code`, read-only in the UI) and via `GET /api/v1/datasources` and
126-
`GET /api/v1/meta/datasource`, where an admin can run the "Sync objects" wizard.
125+
A connected external datasource is also visible in **Setup → Integrations →
126+
Datasources** (stamped `origin: code`, read-only in the UI) and via
127+
`GET /api/v1/datasources` and `GET /api/v1/meta/datasource`, where an admin can
128+
run the "Sync objects" wizard.
127129

128130
### When auto-connect fails
129131

@@ -160,8 +162,8 @@ Every connect attempt's verdict is retained, so a datasource that is down no
160162
longer has to be diagnosed by restarting the server and re-reading boot logs
161163
([#3827](https://github.com/objectstack-ai/objectstack/issues/3827)).
162164

163-
**Setup → Datasources** and `GET /api/v1/datasources` report a `status` per
164-
datasource:
165+
**Setup → Integrations → Datasources** and `GET /api/v1/datasources` report a
166+
`status` per datasource:
165167

166168
| `status` | Meaning |
167169
|:---|:---|
@@ -202,7 +204,7 @@ const policy: DatasourceConnectPolicy = {
202204
? { allow: true }
203205
: {
204206
allow: false,
205-
// operator-facing: logs + Setup → Datasources only
207+
// operator-facing: logs + the Setup datasource list only
206208
reason: `egress allow-list miss for ${ds.name} (${tenant.id}, plan=${tenant.plan})`,
207209
// tenant-facing: appended to the query-time error
208210
publicReason: 'External datasources require the Scale plan.',
@@ -217,6 +219,12 @@ secret in the secret store (the same `SecretBinder` / `ICryptoProvider` the
217219
runtime-admin "Add Datasource" wizard uses). The credential is resolved to
218220
cleartext **at connect, before the driver is built**.
219221

222+
The shipped binder encrypts the cleartext into a `sys_secret` row and mints an
223+
**opaque** handle — `sys_secret:<id>` — as the `credentialsRef`; that is the only
224+
form it resolves, so a hand-written path like `secret:warehouse/password` will
225+
not dereference. A host that wants a different scheme (a vault path, say)
226+
injects its own resolver via `new DatasourceAdminServicePlugin({ secrets })`.
227+
220228
Resolution is **fail-closed**: if a `credentialsRef` is declared but no secret store
221229
is configured, or the secret cannot be resolved/decrypted, the datasource is left
222230
**unconnected with a clear error** — never connected without the credential. An

content/docs/data-modeling/formulas.mdx

Lines changed: 47 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ Every expression in metadata is persisted as the same envelope:
3939
type Expression = {
4040
dialect: 'cel' | 'cron' | 'template';
4141
source?: string; // surface syntax
42-
ast?: unknown; // parsed AST (filled by `objectstack compile`)
42+
ast?: unknown; // parsed AST (filled by `os compile`)
4343
meta?: { rationale?: string; generatedBy?: string };
4444
};
4545
```
@@ -66,11 +66,11 @@ to relearn the syntax across surfaces.
6666
most common mistake (and the root cause of issue #1491) is a condition like
6767
`{record.rating} >= 4`: in CEL, `{…}` is a **map literal**, so it is a parse
6868
error. Write bare CEL: `record.rating >= 4`. Braces belong only in `{{ … }}`
69-
text templates. As of 7.6 a malformed expression no longer silently does
70-
nothing — it fails `objectstack build` and throws at runtime.
69+
text templates. A malformed expression does not silently do nothing — it fails
70+
`os build` and throws at runtime.
7171
</Callout>
7272

73-
**Template formatting (7.6).** A `template` hole is a field path with an optional
73+
**Template formatting.** A `template` hole is a field path with an optional
7474
whitelisted formatter — `{{ path | formatter[:arg] }}` — with defined value→string
7575
semantics (no arbitrary logic; put logic in a CEL field):
7676

@@ -121,9 +121,14 @@ evaluates (`objectql` computes a formula virtual field from its `expression`
121121
only). Do **not** pass `type: 'currency' | 'number' | …` to `Field.formula` — it
122122
is rejected by the typed `FieldInput` and, untyped, would override `type:'formula'`
123123
so the field silently never computes. To declare what the formula returns, set
124-
**`returnType`** (`'number' | 'text' | 'boolean' | 'date'`); it is inferred and
125-
stamped automatically by the AI build path, and consumers (dashboard measures,
126-
formatting) read it instead of re-parsing the expression. The CEL source is the
124+
**`returnType`** (`'number' | 'text' | 'boolean' | 'date'`). You declare it
125+
yourself — nothing back-fills it — but the agent-callable `validate_expression`
126+
tool reports the type it infers from the expression (`inferredType`) so an AI
127+
author can stamp the matching value. Consumers read the declared `returnType`
128+
instead of re-parsing the expression (record-title eligibility, for one: a
129+
formula is title-eligible only when its `returnType` is `'text'`, which is what
130+
lets it be *derived* as the record title — an explicit `nameField` pointer is
131+
honored either way). The CEL source is the
127132
**`expression`** key — it is the only key the schema and runtime read for a
128133
formula's source; there is no separate `formula` source key.
129134
</Callout>
@@ -161,7 +166,7 @@ Cheat-sheet of what you'll write daily:
161166

162167
Registered automatically into every `Environment` by `@objectstack/formula`.
163168
All functions are pure given a pinned `now`, which is what makes
164-
`objectstack build` artifacts byte-stable across runs.
169+
`os build` artifacts byte-stable across runs.
165170

166171
| Function | Returns | Description |
167172
|:---|:---|:---|
@@ -197,20 +202,21 @@ Keep them pure, dependency-free, and AI-readable.
197202
| `record` | the row being evaluated | formulas, validation, sharing, visibility |
198203
| `previous` | row before update | hooks, validation on update |
199204
| `input` | hook payload | hooks |
200-
| `os.user` | install / request user | seed, predicates with identity |
205+
| `current_user` | the authenticated subject — the canonical binding (ADR-0068). `user`, `ctx.user` and `os.user` are aliases of the **same** object | predicates with identity |
206+
| `os.user` | alias of `current_user` | seed, predicates with identity |
201207
| `os.org` | active organization | seed, predicates |
202208
| `os.env` | install env (`prod`, `dev`, `test`) | seed, predicates |
203209

204210
---
205211

206212
## Build-time validation
207213

208-
`objectstack build` type-checks every expression against the object schema, so
214+
`os build` type-checks every expression against the object schema, so
209215
a mistake that would silently evaluate to `null` at runtime is caught before it
210216
ships. The same shared validator also runs when a flow is registered
211217
(`registerFlow`), so a flow authored dynamically gets the same verdict. Findings
212218
come at two severities: **errors** fail the build; **warnings** are advisory
213-
(surfaced, not fatal — `objectstack validate --strict` promotes them to errors).
219+
(surfaced, not fatal — `os validate --strict` promotes them to errors).
214220

215221
| Check | Example | Severity |
216222
|:---|:---|:---|
@@ -330,10 +336,19 @@ invert the test (here `amount <= 0` rejects non-positive amounts).
330336
object: 'case',
331337
events: ['afterUpdate'],
332338
condition: P`previous.status != 'escalated' && record.status == 'escalated'`,
333-
body: { language: 'js', source: 'await ctx.notifyOpsTeam(record)' },
339+
body: {
340+
language: 'js',
341+
source: "ctx.log.warn('case escalated', { id: ctx.input.id });",
342+
capabilities: ['log'],
343+
},
334344
}
335345
```
336346

347+
The `condition` is the CEL part — it binds `record` / `previous` directly. The
348+
JS `body` is a different surface: it is wrapped as `new AsyncFunction('ctx', source)`,
349+
so inside it the record is `ctx.input` (there is no bare `record`), and every
350+
`ctx` API it touches must be covered by a declared capability.
351+
337352
---
338353

339354
## Formula patterns
@@ -375,24 +390,32 @@ Field.formula({
375390
Field.formula({
376391
label: 'Days to Close',
377392
returnType: 'number',
378-
expression: 'daysBetween(record.close_date, today())',
393+
expression: 'daysBetween(today(), record.close_date)',
379394
})
380395

381396
// Contract end in 30 days
382397
Field.formula({
383398
label: 'Expiring Soon',
384399
returnType: 'boolean',
385-
expression: 'record.status == "active" && daysBetween(record.end_date, today()) <= 30',
400+
expression: 'record.status == "active" && daysBetween(today(), record.end_date) <= 30',
386401
})
387402

388403
// Age in days
389404
Field.formula({
390405
label: 'Age (Days)',
391406
returnType: 'number',
392-
expression: 'daysBetween(today(), record.created_date)',
407+
expression: 'daysBetween(record.created_date, today())',
393408
})
394409
```
395410

411+
<Callout type="warn">
412+
**Argument order matters.** `daysBetween(a, b)` is `b − a`, so it counts
413+
*forward from `a` to `b`* and goes negative when `b` is earlier. "Days
414+
remaining" is `daysBetween(today(), record.due_date)`; "age so far" is
415+
`daysBetween(record.created_date, today())`. Swapping the two operands is the
416+
easiest way to ship a formula whose sign is inverted on every row.
417+
</Callout>
418+
396419
<Callout type="info">
397420
**`dateField == today()` now matches (#3183).** A `date` field reads back as a
398421
`YYYY-MM-DD` string, and CEL treats a string and a timestamp as unequal — so the
@@ -479,18 +502,20 @@ Field.formula({
479502
**DON'T:**
480503
- Create circular references
481504
- Use formulas for frequently changing data
482-
- Nest too many IF statements
505+
- Nest ternaries so deeply the expression stops being readable
483506
- Ignore null handling
484507

485508
---
486509

487510
## Determinism contract
488511

489-
Two consecutive `objectstack build` runs with no source changes must produce
512+
Two consecutive `os build` runs with no source changes must produce
490513
**byte-identical** `dist/objectstack.json`. CEL plus pinned `now` is what
491514
makes this possible — there are zero compile-time-evaluated `Date.now()`
492-
calls in seed metadata. CI enforces this via SHA-1 comparison; if you add a
493-
new dialect or stdlib helper, ensure it preserves determinism.
515+
calls in seed metadata: a CEL seed value travels into the artifact as
516+
unevaluated source and only resolves at install. This is a design contract
517+
held by review, not a gate — no CI job currently diffs two builds — so if you
518+
add a new dialect or stdlib helper, ensure it preserves determinism.
494519

495520
---
496521

@@ -524,7 +549,7 @@ Salesforce semantics rewrite their formulas in CEL.
524549
```ts
525550
import { ExpressionEngine } from '@objectstack/formula';
526551

527-
// Compile + cache
552+
// Compile — parse + type-check, returning the engine-native AST as `value`
528553
const compiled = ExpressionEngine.compile({ dialect: 'cel', source: 'record.x * 2' });
529554

530555
// Evaluate
@@ -536,11 +561,11 @@ const result = ExpressionEngine.evaluate(
536561
```
537562

538563
The low-level engine never throws — `evaluate()` returns `{ ok: false, error }`.
539-
But **call sites must not silently swallow that** (ADR-0032): `objectstack build`
564+
But **call sites must not silently swallow that** (ADR-0032): `os build`
540565
**fails** on an invalid expression (with a located, schema-aware message), and at
541566
runtime the flow/rule engines **throw** a loud, attributed error instead of
542567
treating a bad expression as `false`/`null`. The same `validateExpression`
543-
validator backs `objectstack build` and metadata registration (and the
568+
validator backs `os build` and metadata registration (and the
544569
agent-callable `validate_expression` tool), so an expression is checked before it ships.
545570

546571
---

content/docs/deployment/migration-from-objectql.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ description: Step-by-step guide for migrating from @objectql/core to @objectstac
55

66
# Migrating from `@objectql/core` to `@objectstack/objectql`
77

8-
Starting with `@objectstack/objectql` v3.1, all core functionality previously provided by `@objectql/core` is now available upstream. This guide walks you through migrating your project so that `@objectql/core` can be removed.
8+
The core functionality previously provided by `@objectql/core` — the introspection types, the utility functions, and the kernel factory — was upstreamed into `@objectstack/objectql` in **v3.0.4** and has shipped in every release since (the current release line is **v16.x**). This guide walks you through migrating your project so that `@objectql/core` can be removed.
99

1010
## Why Migrate?
1111

@@ -26,7 +26,7 @@ Starting with `@objectstack/objectql` v3.1, all core functionality previously pr
2626
"dependencies": {
2727
- "@objectql/core": "^4.x",
2828
- "@objectql/types": "^4.x",
29-
+ "@objectstack/objectql": "^3.1.0"
29+
+ "@objectstack/objectql": "^16.1.0"
3030
}
3131
}
3232
```

content/docs/deployment/vercel.mdx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ description: Deploy ObjectStack applications to Vercel with the Hono server adap
55

66
# Deploy to Vercel
77

8-
ObjectStack 11 deploys to Vercel in **server mode** — serverless functions running the **Hono** adapter (`@objectstack/hono`). The published ObjectStack Studio/console ships as a static Vite SPA that points at a separate ObjectStack server (via `VITE_SERVER_URL`).
8+
ObjectStack deploys to Vercel in **server mode** — serverless functions running the **Hono** adapter (`@objectstack/hono`). The Console SPA can also be deployed on its own as a static Vite build that points at a separate ObjectStack server (via `VITE_SERVER_URL`) — but that build comes from the Console source repo ([objectui](https://github.com/objectstack-ai/objectui)), not from the prebuilt `@objectstack/console` package.
99

1010
<Callout type="warn">
1111
**Removed in 11.** The in-browser MSW mode (`@objectstack/plugin-msw`) and the Next.js adapter (`@objectstack/nextjs`) are no longer published — use the Hono server path below.
@@ -43,7 +43,7 @@ In Server mode, ObjectStack runs inside Vercel Serverless Functions. API request
4343
This is a valid self-contained pattern when you want to ship a Vite SPA *and* its API from a single Vercel project: the SPA is served as static assets, and a Hono-based serverless function handles `/api/*` requests.
4444

4545
<Callout type="info">
46-
The published ObjectStack Studio/console (`@objectstack/console`) does **not** use this topology. It deploys as a pure static SPA with no `api/` function — it is meant to be embedded in, or pointed at, a separate ObjectStack server via `VITE_SERVER_URL`. For that separate server, prefer the CLI (`objectstack serve`) over a hand-rolled Hono function.
46+
The framework-vendored Console (`@objectstack/console`) does **not** use this topology. It is a **prebuilt, dist-only** package — no source and no build step, just static assets — that an ObjectStack server mounts at `/_console` (`os serve --ui`, on by default). Its bundle is baked same-origin, and `VITE_SERVER_URL` is a build-time Vite flag, so aiming a Console at a *different* origin means building it yourself from the Console source repo ([objectui](https://github.com/objectstack-ai/objectui)). For the server itself, prefer the CLI (`os serve`) over a hand-rolled Hono function.
4747
</Callout>
4848

4949
**1. Create the kernel singleton** (`api/_kernel.ts` — prefixed with `_` to prevent Vercel from creating a route):
@@ -82,7 +82,10 @@ export async function ensureApp(): Promise<Hono> {
8282
`InMemoryDriver` keeps this snippet self-contained, but on serverless **all data
8383
is lost on every cold start** — fine for a demo, wrong for anything real. Before
8484
going past a proof of concept, swap the driver for a real database (see
85-
[Database Drivers](/docs/data-modeling/drivers)) and point `OS_DATABASE_URL` at it.
85+
[Database Drivers](/docs/data-modeling/drivers)). A hand-built kernel does **not**
86+
resolve `OS_DATABASE_URL` on its own — that precedence lives in the CLI and in
87+
`createStandaloneStack()` — so read the connection string yourself, e.g.
88+
`new SqlDriver({ client: 'pg', connection: process.env.OS_DATABASE_URL })`.
8689
</Callout>
8790

8891
**2. Create the API entrypoint** (`api/index.ts`):
@@ -134,7 +137,7 @@ Setting `VITE_SERVER_URL` to empty string tells the client SDK to use same-origi
134137

135138
Configure these in Vercel Project Settings → Environment Variables:
136139

137-
### Studio / SPA build
140+
### Console / SPA build
138141

139142
| Variable | Description |
140143
| :--- | :--- |
@@ -179,7 +182,7 @@ storage for artifacts.
179182
- [ ] `api/_kernel.ts` boots the kernel with the correct driver
180183
- [ ] `vercel.json` sets `VITE_USE_MOCK_SERVER=false` and `VITE_SERVER_URL=` (empty)
181184
- [ ] Rewrite rule routes `/api/*` to `/api` and excludes `/api/` from SPA rewrite
182-
- [ ] `OS_DATABASE_URL` is configured in Vercel environment variables (for production drivers)
185+
- [ ] The production connection string is set in Vercel environment variables **and** read explicitly by the driver you construct in `api/_kernel.ts` (`OS_DATABASE_URL` is resolved by the CLI / `createStandaloneStack()`, not by a hand-built kernel)
183186
- [ ] CORS is configured if frontend and API are on different origins
184187

185188
---

0 commit comments

Comments
 (0)