Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
3c7f0b7
wip(spec): one driver vocabulary table; mongo→mongodb; turso contract
claude Aug 9, 2026
6e708b7
wip(cli,runtime): both hosts read the shared driver vocabulary; fork-…
claude Aug 9, 2026
e777611
wip(service-datasource): mongodb rename, turso arm, exhaustive factor…
claude Aug 9, 2026
b1ea158
test(cli): cross-host driver vocabulary parity pin + 8-cell fork-2 ma…
claude Aug 9, 2026
35022d7
test(cli): update the two pins fork 1/2 deliberately flip
claude Aug 9, 2026
c7368c9
test(spec): turso contract, mongo→mongodb migration proof, rename pins
claude Aug 9, 2026
4bdafa2
chore(spec): regenerate api-surface, spec-changes, upgrade guide; liv…
claude Aug 9, 2026
970712c
chore: changesets for the driver-vocabulary convergence
claude Aug 9, 2026
9a16e2a
chore: register the mongo→mongodb conversion on the hosts changeset
claude Aug 9, 2026
dfad526
test(cli): parity REFUSE cases run dev AND prod — reverse verificatio…
claude Aug 9, 2026
e0fe76c
test(service-datasource): rename-consequence pins for mongodb
claude Aug 9, 2026
70cbcc0
chore(spec): turso schema strictness ledger row, TursoTransportMode a…
claude Aug 9, 2026
1d0a347
Merge remote-tracking branch 'origin/main' into claude/issue-6345-dri…
claude Aug 9, 2026
c0bfb65
chore(spec): regenerate spec-changes/upgrade-guide from the MERGED so…
claude Aug 9, 2026
7bb035d
chore(spec): regenerate api-surface/docs/skill-refs from the MERGED s…
claude Aug 9, 2026
b19f169
fix(spec): three convention breaks in the new turso schema — docs-lin…
claude Aug 9, 2026
0fa19d2
Merge remote-tracking branch 'origin/main' into claude/issue-6345-dri…
claude Aug 9, 2026
cc2447b
Merge remote-tracking branch 'origin/main' into claude/issue-6345-dri…
claude Aug 9, 2026
6a71bd8
Merge remote-tracking branch 'origin/main' into claude/issue-6345-dri…
claude Aug 9, 2026
8640f39
fix(cli): keep the #6860 allowlist pin's oracle honest — an unrecogni…
claude Aug 9, 2026
dd63b18
Merge remote-tracking branch 'origin/main' into claude/issue-6345-dri…
claude Aug 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions .changeset/driver-vocabulary-single-table-hosts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
---
"@objectstack/cli": major
"@objectstack/runtime": major
---

fix(cli,runtime)!: `os start` and `os migrate` finally read the same driver vocabulary (#6345)

One environment variable had two answers. Measured on `main` by driving the real
entry points — `resolveDriverType` + `resolveStorageDefinition` for the `os start`
side, `resolveStandaloneDatabase` for the `os migrate` side — **10 of 21
spellings disagreed**:

```
OS_DATABASE_DRIVER=pg OS_DATABASE_URL=postgres://… os start → boots
OS_DATABASE_DRIVER=pg OS_DATABASE_URL=postgres://… os migrate plan → refused by name
```

`sql`, `wasm`, `wasm-sqlite`, `postgresql`, `pg`, `mysql2`, `mongo`, `mingo`,
`in-memory` and `libsql` were accepted by the CLI and refused by the standalone
stack. Both sides were separately correct and separately pinned; the missing test
was the CROSS-host one, and it now exists
(`packages/cli/src/utils/driver-vocabulary-parity.test.ts` — the only place that
can import both).

**Both hosts now resolve through `@objectstack/spec`'s one driver table.** The
CLI's hand-written `driverType === 'pg' || driverType === 'postgresql'` chains
and the standalone stack's canonical-only `z.enum` are both gone; a driver added
to the spec table appears on both hosts at once, which is the only shape in which
this fork cannot re-open. The standalone `databaseDriver` CONFIG key accepts the
same aliases as `OS_DATABASE_DRIVER`, so the fork cannot relocate to inside one
host either.

**BREAKING ① — selecting a driver whose database lives elsewhere, without saying
where, now refuses.** Four kinds have no local default (`postgres`, `mysql`,
`mongodb`, `turso`), and before this change each side guessed, differently:

| selection, no URL | `os start` before | `os migrate` before | now, both |
| :-- | :-- | :-- | :-- |
| `postgres` | `config.url === undefined` → `pg` connects to ITS localhost:5432 | `file:<state>/data/objectstack.db` | typed refusal |
| `mysql` | `config.url === undefined` | `file:…objectstack.db` | typed refusal |
| `mongodb` | invented `mongodb://localhost:27017/objectstack` | `file:…objectstack.db` | typed refusal |
| `turso` | typed refusal (#5602) | `file:…objectstack.db` | typed refusal |

Eight cells, seven of them wrong in one of two ways: connect the operator to a
database they never named, or hand a server driver a `file:` DSN and let it fail
two layers from the cause. `turso` already said the right sentence; this
generalizes it rather than leaving one kind honest and three guessing. Only the
FALLBACK rungs are refused — a URL from `--database`, `OS_DATABASE_URL`,
`DATABASE_URL`, `TURSO_DATABASE_URL` or the project's declared default datasource
is a statement about where the database is, and is honoured as before, `file:`
DSN included.

**BREAKING ② — an explicitly-named unknown driver refuses on the CLI side too.**
`os dev --database-driver sqlite3` used to fall through to the dev SQLite default
and boot in silence, while `os migrate` refused the same value by name (#6344
killed the silent fallback on that side only). `''` (nobody chose) keeps its old
answer — dev default, `null` in production; a non-empty value can only have come
from an operator, since URL inference yields a canonical id or `''`. The refusal
enumerates the spellings that actually work, from the shared table.

**Widened, not narrowed:** every spelling either host accepted before is accepted
by both now. `sqlite3` / `better-sqlite3` / `mariadb` / `inmemory` stay out of the
selection face on both — neither host ever accepted them as a boot selection, and
converging two hosts is not a licence to widen the flag. They keep resolving a
config CONTRACT, so a stored `driver: 'sqlite3'` datasource is unaffected.

**Why `major` on both.** ① and ② each turn a boot that started into a boot that
refuses. A deployment that really did run postgres on localhost with trust auth,
or that relied on `mongodb://localhost:27017/objectstack`, was working by
accident and now gets a message telling it what to set — but it was working, and
calling that a `patch` because the old behaviour was a bug would let the change
arrive unannounced in a changelog. The alias widening on its own would be
`minor`; the refusals are what price this at `major`.

**Migration.** The stored half of this change is the `mongo` → `mongodb`
canonical-id rename, which both hosts now resolve through the shared table; it is
registered as the ADR-0087 D2 conversion `datasource-driver-mongo-to-mongodb`
and needs no action from anyone — `migrate meta` converges the rows and `mongo`
stays accepted meanwhile. The two refusals have no stored form and no codemod:
they prescribe an operator action (set the database URL, or fix the driver
value) whose correct answer is a fact only the operator has, which is why the
messages name the variable, show the target shape, and say what booting anyway
would have cost.

<!-- adr-0087: registered datasource-driver-mongo-to-mongodb -->
57 changes: 57 additions & 0 deletions .changeset/driver-vocabulary-single-table-service-datasource.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
---
"@objectstack/service-datasource": major
---

feat(service-datasource)!: `DRIVER_CATALOG` publishes `mongodb`, and the factory can no longer fall through to `memory` (#6345)

**BREAKING — `DRIVER_CATALOG`'s MongoDB entry publishes `id: 'mongodb'`.** That
field is documented as "used as `datasource.driver`" and it is literally what the
Studio connection form writes into a datasource row, so this is the face of
#6345's `mongo` → `mongodb` rename that reaches stored data. Rows written before
the rename carry `mongo`; the ADR-0087 D2 conversion
`datasource-driver-mongo-to-mongodb` converges them at every rehydration seam,
and `mongo` remains an accepted alias so a deployment that skipped the migration
still connects. The factory's dispatch arm renames with it (`kind === 'mongodb'`).

**A `turso` construction arm — which the rename made mandatory, not optional.**
`createDefaultDatasourceDriverFactory().supports()` is
`resolveDriverId(id) !== undefined`, so the moment `turso` gained a config
contract in `@objectstack/spec` this factory began claiming it. Before this arm,
that claim was answered by `create()`'s trailing `memory` fall-through: a libSQL
datasource would have been built as an ephemeral in-process store that accepts
writes, reports success and loses everything — the #3276 silent-wrong-engine
class with a new spelling. The arm is the same shape `mongodb` and `sqlite-wasm`
already use (lazy import, typed not-installed error), because all three ride in
optional packages and being an optional INSTALL has never meant lacking a
contract.

The CLI and standalone stack still inject their own turso factory for the
`default` datasource (#5602's host-factory seam), and an injected factory
replaces this one — so this arm serves every OTHER door: a runtime datasource
created in Setup, `testConnection`, a declared non-default datasource. Those
doors previously got `supports() === false` and degraded; they now build.

**The fall-through itself is gone.** `memory` was the last arm's *implicit*
position — no `if`, just the end of the function — so any `BuiltinDriverId` the
switch did not handle silently became an in-memory store. It is now an explicit
`kind === 'memory'` arm followed by an exhaustiveness stop typed `never`: adding
a builtin without an arm is a compile error, and if a stale published
`@objectstack/spec` ever reaches a newer consumer at run time, the result is a
named refusal rather than a different engine. This is the trap the next driver
would have inherited; turso is simply the one that found it.

**Why `major`.** The published `DRIVER_CATALOG[].id` value changes. Any consumer
that compares a stored `datasource.driver` against the catalog id — a form
pre-selecting the current driver, a grouped list, an equality filter — stops
matching pre-rename rows until the conversion has run. Nothing throws, which is
precisely why this is not a `minor`: the failure is a dropdown that silently
shows no selection, and a bump that lets it arrive unannounced would be the same
class of quiet as the defect the rename fixes.

**Not renamed, deliberately:** `SqlDialect`'s `'mongo'` member
(`data/type-compat.ts`). That is a different vocabulary — it names the type
system of an EXTERNAL schema being introspected, alongside `snowflake` and
`bigquery`, and is never a `datasource.driver`. Renaming it would have been
sympathetic magic on a matching string.

<!-- adr-0087: registered datasource-driver-mongo-to-mongodb -->
92 changes: 92 additions & 0 deletions .changeset/driver-vocabulary-single-table-spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
---
"@objectstack/spec": major
---

feat(spec)!: one driver vocabulary — `mongo` → `mongodb`, `turso` gets a config contract (#6345)

`packages/spec` has owned the driver alias table since #4410, for one reason
stated in its own module comment: two tables would let the id that SELECTS a
driver and the id that selects that driver's CONFIG CONTRACT disagree. That
argument was right and the table was right; it just never reached the two boot
hosts. Measured on `main` before this change, driving the real entry points:

| | `os start` | `os migrate` |
| :-- | :-- | :-- |
| `OS_DATABASE_DRIVER=pg` | accepted (`postgres`) | **refused by name** |
| `OS_DATABASE_DRIVER=libsql` | accepted (`turso`) | **refused by name** |

**10 of 21 spellings disagreed.** Three prior cards (#3276, #5820, #6265) each
fixed one spelling on one side, each with a green pin — and every pin drove
exactly one host, which is why the fork survived all three.

**What this changeset changes in `@objectstack/spec`.**

The flat `Record<string, BuiltinDriverId>` becomes one table with a row per
driver carrying `id`, `aliases`, `contractOnlyAliases` and `hasLocalDefault`.
`BUILTIN_DRIVER_IDS`, `DRIVER_ID_ALIASES` and `resolveDriverId` are projections
of it — `BUILTIN_DRIVER_IDS` keeps its exact tuple type, so the api-surface delta
for this PR is purely additive (10 new exports, nothing removed or renamed).

Three faces are new, and they are what the two hosts consume:
`resolveDatabaseDriverId()` (the selection face), `driverHasLocalDefault()` (does
this driver have anything to fall back on with no URL) and
`DATABASE_DRIVER_SELECTION_ALIASES` (what a refusal message enumerates).

**BREAKING — the canonical mongo id is `mongodb`.** `resolveDriverId('mongo')`
now returns `'mongodb'`; `BuiltinDriverId` no longer includes `'mongo'`;
`DRIVER_CONFIG_SCHEMAS` and `MongoDriverSpec.id` follow. The old canon was the
one string on the platform that said `mongo` while both hosts, the npm package
(`@objectstack/driver-mongodb`) and every URL scheme said `mongodb`, and the
maintainer's ruling renames it rather than adding a mapping layer, so that
selection canon and contract canon are one string.

`mongo` **stays an accepted alias**, deliberately: nothing that authored it
breaks, and a deployment that never replays the conversion still resolves the
same contract and builds the same driver. What needs migrating is the STORED
value, because the canonical id is published as `DRIVER_CATALOG.id` — what Studio
writes into `datasource.driver` — so after the rename the form emits `mongodb`
while older rows carry `mongo`, and a reader matching stored rows against the
catalog id silently misses them. The ADR-0087 D2 conversion
`datasource-driver-mongo-to-mongodb` converges them at every rehydration seam.

**`turso`/libSQL becomes a complete builtin.** It was the mirror image of the
mongo problem: both hosts dispatched it while spec shipped no contract, so
`validateDriverConfig('turso', …)` answered `{ known: false }` and a libSQL
`config` was the one connection block on the platform with no gate — `{ token }`
(the wrong key; it is `authToken`) was accepted in silence and the connection
attempted unauthenticated. `TursoConfigSchema` closes that. The keys are drawn
from what `TursoDriverConfig` actually READS, not from what libSQL supports, so
the fix does not open a new inert slot: `client` (a live object, unauthorable),
`pool` and `schemaMode`/`readOnly` (datasource-level) are deliberately absent.

**Consumers of the `{ known: false }` answer, and what the flip does to each** —
established before making it, since a consumer depending on the negative answer
would have been a stop condition:

1. `DatasourceSchema`'s `reportDriverConfigIssues` — was a no-op for turso, now
parses. An authored turso `config` gains a real verdict.
2. `service-datasource`'s `assertValidConfig` (the Setup wizard's door) — same
flip, same reason.
3. `DRIVER_CATALOG` — turso is deliberately NOT curated into the connection form,
the same call `sqlite-wasm` has carried since #4410. No visible change.
4. `driverReadsDeclaredPool` — answers `true` for turso before AND after (via the
unknown-id branch before, the not-rejected branch now). Verdict unchanged.

**`sql` and `wasm` join the selection face; `sqlite3`, `better-sqlite3`,
`mariadb` and `inmemory` do not.** The ruling fixes the selection face as the
union of what the two hosts accepted, and those four were accepted by neither —
so they stay `contractOnlyAliases`: they keep resolving a config contract
(dropping that would silently un-validate a stored `driver: 'sqlite3'` row) while
`resolveDatabaseDriverId` refuses them, because converging two hosts is not a
licence to widen a boot flag on no ruling. That distinction is the thing the flat
`Record` could not express and is why the table has two alias columns.

**Why `major` and not `minor`.** The alias widening alone would be `minor` — it
only accepts more. The rename is what forces `major`: `BuiltinDriverId` loses a
member, so every TypeScript consumer that switches on it or types a variable as
it fails to compile, and `DRIVER_CONFIG_SCHEMAS['mongo']` is gone. That is a
compile-time break even though the runtime behaviour is compatible, and pricing
it as `minor` because "nothing breaks at run time" would be exactly the
half-truth a consumer discovers at build time.

<!-- adr-0087: registered datasource-driver-mongo-to-mongodb -->
92 changes: 92 additions & 0 deletions content/docs/references/data/driver-turso.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
---
title: Driver Turso
description: Driver Turso protocol schemas
---

{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}

Turso / libSQL Driver Protocol (#6345).

## Why this arrives late, and what it closes

`turso` was the one connection block on the platform with NO gate. #4410 gave
every built-in driver's `datasource.config` a contract and made
`DatasourceSchema` parse against it, but turso was not a builtin: its driver
ships in an OPTIONAL package (`@objectstack/driver-turso`, #5602), so
`resolveDriverId('turso')` returned `undefined` and `validateDriverConfig`
answered `{ known: false }` — "nothing to check against". Meanwhile both boot
hosts dispatched `turso` for real. So a libSQL datasource could carry
`{ token: … }` (the wrong key — it is `authToken`) and be accepted in silence,
then connect unauthenticated, which is precisely the failure #4410 exists to
end, surviving in the one driver #4410 could not see.

The maintainer's #6345 ruling closes it by making turso a complete builtin
rather than a permanent exception. Optionality of the PACKAGE is orthogonal to
existence of the CONTRACT — `mongodb` and `sqlite-wasm` are optional installs
too, and both have had a contract since #4410.

## What is declared here, and what is deliberately not

The keys below are exactly the `TursoDriverConfig` fields the driver reads and
that an author can express as data. Three are deliberately absent:

- `client` (a pre-constructed `@libsql/client` instance) — a live object, not
authorable metadata; declaring it would promise a JSON slot that can never
be filled from a `sys_metadata` row.
- `pool` — connection pooling is the datasource's own block, not driver
config, exactly as on postgres/mysql/mongo.
- `schemaMode` / `readOnly` — datasource-level, same as every other driver.

ADR-0049 (enforce-or-remove) is why the list is drawn from what the driver
READS rather than from what libSQL supports: a key declared here that no
driver consults would be a new inert slot, and this file exists to close one.

<Callout type="info">
**Source:** `packages/spec/src/data/driver/turso.zod.ts`
</Callout>

## TypeScript Usage

```typescript
import { TursoConfigSchema, TursoTransportModeSchema } from '@objectstack/spec/data';
import type { TursoConfig, TursoTransportMode } from '@objectstack/spec/data';

// Validate data
const result = TursoConfigSchema.parse(data);
```

---

## TursoConfig

Turso / libSQL Connection Configuration

### Properties

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **url** | `string` | ✅ | libSQL endpoint or local file: a remote libsql/https Turso URL, a file path, or :memory: |
| **authToken** | `string` | optional | JWT auth token for a remote libSQL database (prefer external.credentialsRef) |
| **encryptionKey** | `string` | optional | AES-256 encryption key for the local database file (local/replica modes) |
| **concurrency** | `integer` | optional | Maximum concurrent requests to the remote database |
| **syncUrl** | `string` | optional | Remote sync URL for embedded-replica mode: a libsql or https Turso endpoint |
| **sync** | `{ intervalSeconds?: integer; onConnect?: boolean }` | optional | Embedded-replica sync configuration (requires `syncUrl`) |
| **timeout** | `integer` | optional | Operation timeout in milliseconds for remote operations |
| **mode** | `Enum<'local' \| 'replica' \| 'remote'>` | optional | Force a transport mode instead of inferring it from `url` |


---

## TursoTransportMode

Force a transport mode instead of inferring it from `url`

### Allowed Values

* `local`
* `replica`
* `remote`


---

1 change: 1 addition & 0 deletions content/docs/references/data/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ This section contains all protocol schemas for the data layer of ObjectStack.
<Card href="/docs/references/data/driver-postgres" title="Driver Postgres" description="Source: packages/spec/src/data/driver/postgres.zod.ts" />
<Card href="/docs/references/data/driver-sql" title="Driver Sql" description="Source: packages/spec/src/data/driver-sql.zod.ts" />
<Card href="/docs/references/data/driver-sqlite" title="Driver Sqlite" description="Source: packages/spec/src/data/driver/sqlite.zod.ts" />
<Card href="/docs/references/data/driver-turso" title="Driver Turso" description="Source: packages/spec/src/data/driver/turso.zod.ts" />
<Card href="/docs/references/data/external-catalog" title="External Catalog" description="Source: packages/spec/src/data/external-catalog.zod.ts" />
<Card href="/docs/references/data/external-lookup" title="External Lookup" description="Source: packages/spec/src/data/external-lookup.zod.ts" />
<Card href="/docs/references/data/feed" title="Feed" description="Source: packages/spec/src/data/feed.zod.ts" />
Expand Down
1 change: 1 addition & 0 deletions content/docs/references/data/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"driver-mysql",
"driver-postgres",
"driver-sqlite",
"driver-turso",
"field-value"
]
}
Loading
Loading