Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
31 changes: 31 additions & 0 deletions .changeset/cli-libsql-url-inference.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
"@objectstack/cli": minor
---

feat(cli): `libsql://` URLs boot a Turso driver — via an optional package, never a silent SQLite fallback (#5602)

`os start --database libsql://my-db.turso.io --database-auth-token $TURSO_TOKEN`
是 `os start --help` 自己列出的 example,但在此之前它必然 `exit 1`:CLI 的
URL → driver 推断认得 `libsql://`,却当场抛 `UnsupportedDriverError` —— 而 runtime 的
环境 provisioning 把 turso 排在偏好第一位。两处口径相反的原因(driver 不在开源分发里)
已随 #4645 把 `@objectstack/driver-turso` 迁回本仓而消失。

现在这条 example 成真:

- **识别即构造。** `libsql://` / `*.turso.io` 解析为 `turso` datasource 定义,
`--database-auth-token`(`OS_DATABASE_AUTH_TOKEN`,回落到 vendor 自己的
`TURSO_AUTH_TOKEN`)进入 driver 配置 —— 该 flag 此前只被转发进子进程环境、无人读取。
- **可选依赖 + 动态 import。** `@objectstack/driver-turso` 声明为 CLI 的
**optional peer**(它会拖入 `@libsql/client`),默认安装体积不变;只有真正选了 libSQL
的启动才会动态 import 它,并通过 `DefaultDatasourcePlugin` 既有的 host-factory 接缝注入。
连接路径、`bootCritical` 失败裁决、`OS_ALLOW_DRIVER_CONNECT_FAILURE` 逃生舱与
Setup → Datasources 的状态留存因此与其他 driver 完全一致(#3826)。
- **包缺席时响亮失败。** 抛 `MissingDriverPackageError`,消息给出精确安装命令
(`npm install @objectstack/driver-turso`)、说明它是 optional peer,并说明为什么
⛔ 不回退 SQLite:静默降级会让服务器对着一个空的本地库启动,而你的 libSQL 数据原封不动,
每一次写入都落在错误的数据库里(#3276 的教训)。
- **仍然拒收的形状。** `--database-driver turso` 但没有任何 URL —— libSQL 没有可猜的默认值,
这条继续抛 `UnsupportedDriverError`,而不是悄悄用 SQLite 默认值顶上。

`os start` 的 example 加了「需安装 driver 包」注记,Drivers / Self-hosting /
Environment variables / CLI 四处文档同步为「可选包支持」口径。
21 changes: 14 additions & 7 deletions content/docs/data-modeling/drivers.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -47,18 +47,24 @@ Drivers can be selected in two ways:
| `mongodb://…`, `mongodb+srv://…` | MongoDB | `@objectstack/driver-mongodb` |
| `postgres://…`, `postgresql://…` | PostgreSQL (Knex `pg`) | `@objectstack/driver-sql` + `pg` |
| `mysql://…`, `mysql2://…` | MySQL (Knex `mysql2`) | `@objectstack/driver-sql` + `mysql2` |
| `libsql://…`, `http(s)://*.turso.…` | Turso / libSQL | `@objectstack/driver-turso` (**optional** — install it yourself) |
| `wasm-sqlite://…`, `*.wasm.db` | SQLite (pure-JS WASM) | `@objectstack/driver-sqlite-wasm` |
| `file:…`, `sqlite:…`, `:memory:`, `*.db` / `*.sqlite` | SQLite (Knex `better-sqlite3`) | `@objectstack/driver-sql` + `better-sqlite3` |
| _(unset, dev mode)_ | SQLite (native, falling back to WASM, then in-memory) | `@objectstack/driver-sql` / `-sqlite-wasm` / `-memory` |

<Callout type="info">
Turso / libSQL (`libsql://`, `*.turso.io`) is **not** inferred from a URL.
`@objectstack/driver-turso` does live in this repo (`packages/drivers/driver-turso`)
and you can register it yourself — a datasource with `driver: 'turso'` and
`config: { url, authToken }` — but the CLI's URL → driver resolver does not
construct it. A `libsql://` URL therefore fails loudly at boot rather than
silently degrading to SQLite. Whether the inference table should construct it
is tracked in issue #5602.
**Turso / libSQL needs one extra install.** `libsql://` and `*.turso.io` URLs *are*
inferred, but `@objectstack/driver-turso` is an **optional peer dependency** of the
CLI — it pulls in `@libsql/client`, so it is not part of a default install:

```bash
npm install @objectstack/driver-turso
```

Without it the boot **fails loudly** with that exact command; it never degrades to
SQLite, which would start the server against an empty local database while your
libSQL data stayed untouched. Pass the token with `--database-auth-token`
(`OS_DATABASE_AUTH_TOKEN`, or the vendor's own `TURSO_AUTH_TOKEN`).
</Callout>

## Supported Drivers
Expand All @@ -70,6 +76,7 @@ is tracked in issue #5602.
| **SQLite** | `@objectstack/driver-sql` (peer: `better-sqlite3`) | `SqlDriver` | `sqlite` \| `sql` |
| **SQLite (WASM)** | `@objectstack/driver-sqlite-wasm` | `SqliteWasmDriver` | `sqlite-wasm` \| `wasm-sqlite` \| `wasm` |
| **MongoDB** | `@objectstack/driver-mongodb` | `MongoDBDriver` | `mongodb` \| `mongo` (single-tenant only — see [below](#multi-tenancy-not-supported)) |
| **Turso / libSQL** | `@objectstack/driver-turso` (optional peer of the CLI) | `TursoDriver` | `turso` \| `libsql` |
| **Memory** | `@objectstack/driver-memory` | `InMemoryDriver` | `memory` |

> All SQL flavours (PostgreSQL / MySQL / SQLite) are served by a single
Expand Down
3 changes: 2 additions & 1 deletion content/docs/deployment/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@ os start \
--port 8080

# Remote artifact + Turso/libSQL backing store
# (needs the optional driver package: npm install @objectstack/driver-turso)
os start \
--artifact https://cdn.example.com/app.json \
--database libsql://my-db.turso.io \
Expand Down Expand Up @@ -351,7 +352,7 @@ The fall-through applies to the **conventional** locations only. Remote
**What it boots:**
- Reads the artifact's `manifest`, `objects`, `views`, `flows`, …
- Auto-registers the platform services declared in `requires: [...]` (e.g. `ai`, `automation`, `analytics`, `auth`, `ui`). Declaring a **service** capability (`automation`, `analytics`, `ai`, `audit`, …) is a *requirement*: if its provider package isn't installed, boot **fails fast** with a clear error instead of silently starting without a capability you asked for. (`auth` and `ui` are tier-gated with their own opt-in rules — `auth`'s secret-gated skip is described below.)
- Auto-detects the driver from the database URL scheme (`memory://` → in-memory, `libsql://`/`https://` → Turso, `postgres[ql]://`/`pg://` → pg, `mongodb[+srv]://` → MongoDB, otherwise sqlite)
- Auto-detects the driver from the database URL scheme (`memory://` → in-memory, `libsql://`/`https://*.turso.*` → Turso — via the optional `@objectstack/driver-turso` package, and a loud failure with the install command when it is missing rather than a fallback to sqlite —, `postgres[ql]://`/`pg://` → pg, `mongodb[+srv]://` → MongoDB, otherwise sqlite)
- Runs standalone boot mode with one active environment.

**Authentication:**
Expand Down
5 changes: 3 additions & 2 deletions content/docs/deployment/environment-variables.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,9 @@ read at startup unless noted otherwise. Boolean variables accept `true` / `false

| Variable | Type | Default | Description |
|:---|:---|:---|:---|
| `OS_DATABASE_URL` | url | — | Database connection string (e.g. `file:./data.sqlite`, `postgres://…`, `mongodb://…`, `memory://`). `libsql://` (Turso) is not inferred — register `@objectstack/driver-turso` explicitly. |
| `OS_DATABASE_DRIVER` | enum | inferred | Force a specific driver when the URL is ambiguous. `memory` \| `sqlite` \| `sqlite-wasm` \| `postgres` \| `mongodb`. |
| `OS_DATABASE_URL` | url | — | Database connection string (e.g. `file:./data.sqlite`, `postgres://…`, `mongodb://…`, `memory://`). `libsql://` / `*.turso.io` (Turso) is inferred too, but its driver is an **optional** package: `npm install @objectstack/driver-turso`, otherwise the boot fails loudly with that command — it never falls back to SQLite. |
| `OS_DATABASE_AUTH_TOKEN` | string | — | Auth token for a libSQL/Turso connection (`--database-auth-token`). The vendor's own `TURSO_AUTH_TOKEN` is read as a fallback and is **not** renamed (see the third-party names note above). Ignored by every other driver — their credentials live in the URL. |
| `OS_DATABASE_DRIVER` | enum | inferred | Force a specific driver when the URL is ambiguous. `memory` \| `sqlite` \| `sqlite-wasm` \| `postgres` \| `mysql` \| `mongodb` \| `turso`. |
| `OS_DATABASE_SQLITE_JOURNAL_MODE` | enum | `wal` | Journal mode for **file-backed** SQLite. `wal` (default) lets a dev server and CLI commands share one file without blocking each other, and is what makes the `os migrate` occupancy check reliable. Set to `delete` for SQLite's rollback journal — required when the database lives on a **network filesystem** (NFS/SMB), where WAL cannot work. The setting is applied, not merely skipped: `delete` converts a database that already adopted WAL back. Ignored for `:memory:`, for the WASM SQLite driver, and for non-SQLite drivers. A per-datasource `sqliteJournalMode` in driver config outranks it. See [Journal mode](/docs/data-modeling/drivers#journal-mode-wal-and-cross-process-access). |
| `OS_ALLOW_DRIVER_CONNECT_FAILURE` | boolean | `false` | Escape hatch for the driver-connect boot guard. By default a data driver that fails to connect at startup **refuses the boot** — a server that cannot reach its database must not report itself started and then fail every request. The same guard covers a **declared datasource** that objects bind to via `datasource: '…'`, or an `external` one with `validation.onMismatch: 'fail'`: those objects have no fallback datasource, so an unconnected one means they are all dead. Set to `1` to boot anyway, in an explicitly degraded state logged loudly at startup. There is **no reconnection**: whatever failed stays dead for the process lifetime and every query and schema sync routed to it fails. |
| `OS_STORAGE_LOCAL_ROOT` | path | `./.objectstack/data/uploads` | Root directory for the local file storage adapter, relative to the process cwd (used by `os serve`'s default `storage` capability wiring). This is the same value as **Setup → Settings → File Storage → Root directory**; setting it here pins that field (it shows as locked-by-env). Renamed from `OS_STORAGE_ROOT` — see below. |
Expand Down
2 changes: 1 addition & 1 deletion content/docs/deployment/self-hosting.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ workable default:

| Variable | Why it must be set |
|:---|:---|
| `OS_DATABASE_URL` | Without it, data lands in a SQLite file under the ObjectStack home directory (`~/.objectstack`, or `<cwd>/.objectstack` next to a project config) — fine for one box, wrong for containers. Use `postgres://…`, `mongodb://…`, or a mounted `file:…` path (`libsql://` / Turso is **not** inferred from the URL — `@objectstack/driver-turso` is in-repo but must be registered explicitly in your stack config; see [Drivers](/docs/data-modeling/drivers)). `mongodb://…` is **single-tenant only**: the MongoDB driver has no row-level tenant isolation and refuses to boot unless the tenancy posture is `single` — see [Drivers → Multi-tenancy](/docs/data-modeling/drivers#multi-tenancy-not-supported). |
| `OS_DATABASE_URL` | Without it, data lands in a SQLite file under the ObjectStack home directory (`~/.objectstack`, or `<cwd>/.objectstack` next to a project config) — fine for one box, wrong for containers. Use `postgres://…`, `mongodb://…`, `libsql://…`, or a mounted `file:…` path (`libsql://` / Turso is inferred, but its driver is an **optional** package — `npm install @objectstack/driver-turso`, or the boot fails loudly rather than degrading to SQLite; see [Drivers](/docs/data-modeling/drivers)). `mongodb://…` is **single-tenant only**: the MongoDB driver has no row-level tenant isolation and refuses to boot unless the tenancy posture is `single` — see [Drivers → Multi-tenancy](/docs/data-modeling/drivers#multi-tenancy-not-supported). |
| `OS_AUTH_SECRET` | Session secret for the auth plugin (`AUTH_SECRET` is the legacy alias). Without it, `/api/v1/auth/*` is **silently skipped** — the server runs unauthenticated. |
| `OS_SECRET_KEY` | 32-byte master key encrypting every stored secret (`openssl rand -hex 32`). On a container's ephemeral filesystem the auto-minted key is **lost on restart**, making previously-encrypted secrets undecryptable. |
| `OS_PORT` | `os start` **fails loudly** if the port is busy (it never auto-shifts like `os dev`). Pin it and keep your reverse-proxy upstream in sync. |
Expand Down
9 changes: 9 additions & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,19 @@
"yaml": "^2.9.0",
"zod": "^4.4.3"
},
"peerDependencies": {
"@objectstack/driver-turso": "workspace:^"
},
"peerDependenciesMeta": {
"@objectstack/driver-turso": {
"optional": true
}
},
"optionalDependencies": {
"better-sqlite3": "^13.0.2"
},
"devDependencies": {
"@objectstack/driver-turso": "workspace:*",
"@oclif/plugin-help": "^6.2.55",
"@oclif/plugin-plugins": "^5.4.86",
"@types/better-sqlite3": "^7.6.13",
Expand Down
43 changes: 36 additions & 7 deletions packages/cli/src/commands/serve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,14 @@ import { bundleRequire } from 'bundle-require';
import { loadConfig, BUNDLE_REQUIRE_EXTERNALS } from '../utils/config.js';
import { mergeBootConfig } from '../utils/merge-boot-config.js';
import { isHostConfig, shouldBootWithLibrary } from '../utils/plugin-detection.js';
import { resolveDriverType, resolveStorageDefinition, UnsupportedDriverError } from '../utils/storage-driver.js';
import {
resolveDriverType,
resolveStorageDefinition,
loadTursoDriverFactory,
isTursoDriverId,
MissingDriverPackageError,
UnsupportedDriverError,
} from '../utils/storage-driver.js';
// [ADR-0105 D1] `resolveMultiOrgEnabled` is deliberately NOT imported here: the
// posture is the authoritative knob and `resolveTenancyPosture()` already folds
// the legacy boolean in as its unset-fallback. serve's last direct reader of the
Expand Down Expand Up @@ -1027,6 +1034,11 @@ export default class Serve extends Command {
if (!hasDriver && config.objects) {
const databaseUrl = process.env.OS_DATABASE_URL;
const driverType = resolveDriverType(process.env.OS_DATABASE_DRIVER, databaseUrl);
// libSQL/Turso's credential is the only one that does NOT ride inside the
// URL (`--database-auth-token`, forwarded by `os start` / `os dev` as
// OS_DATABASE_AUTH_TOKEN; TURSO_AUTH_TOKEN is the vendor's own name, kept
// as-is per Prime Directive #9's third-party exceptions).
const databaseAuthToken = process.env.OS_DATABASE_AUTH_TOKEN || process.env.TURSO_AUTH_TOKEN;

try {
// #3826: the fallback no longer constructs a driver — it declares
Expand All @@ -1039,11 +1051,22 @@ export default class Serve extends Command {
// the loosen-only self-heal (#2186, via config.autoMigrate) now run
// inside the factory at connect.
const { DriverPlugin, DefaultDatasourcePlugin } = await import('@objectstack/runtime');
const resolution = resolveStorageDefinition(driverType, { databaseUrl, isDev });
const resolution = resolveStorageDefinition(driverType, { databaseUrl, isDev, authToken: databaseAuthToken });
if (resolution) {
// #5602: libSQL/Turso is the one kind the shared open-core factory
// cannot build — `@objectstack/driver-turso` is an OPTIONAL peer, so
// the CLI loads it here and injects it through the plugin's documented
// host-factory seam. Everything else about the boot is unchanged: same
// connect path, same bootCritical verdict, same escape hatch. A missing
// package throws MissingDriverPackageError BEFORE the plugin exists, so
// the operator sees the install command rather than a connect failure —
// and never a silent SQLite fallback.
const hostFactory = isTursoDriverId(resolution.driverId)
? await loadTursoDriverFactory()
: undefined;
await kernel.use(new DefaultDatasourcePlugin(
{ driver: resolution.driverId, config: resolution.config },
{ dev: isDev },
{ dev: isDev, ...(hostFactory ? { factory: hostFactory } : {}) },
));
trackPlugin(resolution.trackName);
resolvedDriverLabel = resolution.label;
Expand Down Expand Up @@ -1097,14 +1120,20 @@ export default class Serve extends Command {
}
}
} catch (e: any) {
// "declared ≠ enforced" guard (#3276-class): a driver that is
// RECOGNIZED but the CLI's resolver does not construct — currently
// `turso`/libSQL, in-repo since #4645 but unwired — must fail LOUDLY, never silently
// fall through to the SQLite default and ignore the selected engine.
// "declared ≠ enforced" guard (#3276-class): a selection the CLI
// RECOGNIZED but cannot honour must fail LOUDLY, never silently fall
// through to the SQLite default and ignore the engine that was asked for.
// Re-throw so run()'s fatal handler restores output, prints the
// actionable message, and exits 1 (in dev AND prod). All OTHER driver
// construction errors keep the prior best-effort silent behavior.
// • UnsupportedDriverError — recognized kind, no usable definition
// (`--database-driver turso` with no URL to connect to).
// • MissingDriverPackageError (#5602) — the optional driver package for
// a `libsql://` selection is not installed. Fatal for the same reason
// and with the same remedy shape: the message carries the exact
// install command, and there is deliberately no SQLite fallback.
if (e instanceof UnsupportedDriverError) throw e;
if (e instanceof MissingDriverPackageError) throw e;
// Same class of fatal (#3724): a driver that refuses to run in this
// deployment's tenancy mode — driver-mongodb has no row-level tenant
// isolation and rejects a non-`single` posture. Swallowing it would
Expand Down
11 changes: 10 additions & 1 deletion packages/cli/src/commands/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,16 @@ export default class Start extends Command {
'<%= config.bin %> start --artifact https://cdn.example.com/app.json --port 8080',
'<%= config.bin %> start --database file:./data/prod.db',
'<%= config.bin %> start --database postgres://user:pass@host:5432/mydb',
'<%= config.bin %> start --database libsql://my-db.turso.io --database-auth-token $TURSO_TOKEN',
{
// #5602: `libsql://` IS inferred and built — through the OPTIONAL package
// `@objectstack/driver-turso`, which the CLI does not bundle (it drags
// `@libsql/client`). Without it installed the boot fails loudly with this
// exact install command; it never degrades to SQLite. The note belongs in
// the example because copy-pasting this line is precisely how an operator
// meets the requirement.
command: '<%= config.bin %> start --database libsql://my-db.turso.io --database-auth-token $TURSO_TOKEN',
description: 'Turso / libSQL — requires the optional driver package: npm install @objectstack/driver-turso',
},
'<%= config.bin %> start --no-ui',
];

Expand Down
Loading
Loading