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
18 changes: 18 additions & 0 deletions .changeset/cli-dev-sqlite-seed-admin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
"@objectstack/cli": minor
---

`objectstack dev` now defaults to SQLite and auto-seeds an admin.

- **Default driver → SQLite.** With no `OS_DATABASE_URL`/`OS_DATABASE_DRIVER`,
dev now prefers `SqlDriver(sqlite, :memory:)` over the pure-JS `InMemoryDriver`
for production-like SQL semantics. It probes by opening a connection (knex
loads `better-sqlite3` lazily at first query) and falls back to
`InMemoryDriver` **with a warning** if the native binary is unavailable —
closing a hole where the surrounding silent catch could leave the kernel with
no driver.
- **`--seed-admin` defaults ON in dev.** Idempotent and non-destructive: POSTs
the public sign-up endpoint, creating `admin@objectos.ai` only on an empty DB
(then promoted to platform admin) and skipping when the email already exists
(422/400), so a custom password is never overwritten. Disable with
`--no-seed-admin`.
21 changes: 14 additions & 7 deletions packages/cli/src/commands/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export default class Dev extends Command {
default: false,
}),
'seed-admin': Flags.boolean({
description: 'After the server is ready, POST /api/v1/auth/sign-up/email to seed an admin account. Default: on when --fresh.',
description: 'After the server is ready, POST /api/v1/auth/sign-up/email to seed an admin account. Default: on (idempotent — creates the admin only on an empty DB, skips if the email already exists). Disable with --no-seed-admin.',
allowNo: true,
}),
'admin-email': Flags.string({
Expand Down Expand Up @@ -206,12 +206,19 @@ export default class Dev extends Command {
);

// ── Seed admin account after the server is ready ────────────────
// `--seed-admin` defaults to ON when `--fresh` is set; otherwise
// OFF (so we don't surprise long-lived dev DBs with extra users).
// Uses better-auth's public sign-up endpoint, which the dev-mode
// bootstrap bypass (auth-manager.ts) lets through even when
// OS_DISABLE_SIGNUP=true for the very first user.
const seedAdmin = flags['seed-admin'] ?? flags.fresh;
// `--seed-admin` defaults to ON in dev. The seed is idempotent and
// NON-DESTRUCTIVE: it POSTs the public sign-up endpoint, which creates
// `admin@objectos.ai` only on an EMPTY DB (then `bootstrapPlatformAdmin`
// in @objectstack/plugin-security promotes that first user to platform
// admin). When the email already exists — i.e. a persistent dev DB you
// signed up against before — better-auth returns 422/400 and we skip
// (see the `r.status === 422 || 400` branch below), so a custom password
// is never overwritten. There's therefore no need to gate on ephemeral
// vs persistent: seeding an empty DB is exactly what you want, and a
// populated DB is left untouched.
// The dev-mode bootstrap bypass (auth-manager.ts) lets the very first
// sign-up through even when OS_DISABLE_SIGNUP=true.
const seedAdmin = flags['seed-admin'] ?? true;
if (seedAdmin) {
void this.seedAdminAccount({
port: port ?? '3000',
Expand Down
52 changes: 46 additions & 6 deletions packages/cli/src/commands/serve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -593,12 +593,52 @@ export default class Serve extends Command {
resolvedDriverLabel = 'SqlDriver(mysql2)';
resolvedDatabaseUrl = databaseUrl;
} else if (isDev) {
// Default in dev: in-memory driver
const { InMemoryDriver } = await import('@objectstack/driver-memory');
await kernel.use(new DriverPlugin(new InMemoryDriver()));
trackPlugin('MemoryDriver');
resolvedDriverLabel = 'InMemoryDriver';
resolvedDatabaseUrl = '(in-memory)';
// Default in dev: prefer SQLite for production-like SQL semantics,
// falling back to the pure-JS in-memory driver (mingo) only when the
// native `better-sqlite3` binary is unavailable — not built, ABI
// mismatch after a Node upgrade, or a blocked prebuild download.
//
// knex loads its client lazily (at first query, not at construction),
// so the only reliable signal inside this registration window is to
// actually open a connection: connect() runs `SELECT 1`, which forces
// better-sqlite3 to load. If that throws we drop to the in-memory
// driver instead of letting the failure surface much later — as a
// missing-module crash on the first real query — or be swallowed by
// the silent catch below, leaving the kernel with no driver at all.
let sqliteDriver: any;
let sqliteOk = false;
try {
const { SqlDriver } = await import('@objectstack/driver-sql');
sqliteDriver = new SqlDriver({
client: 'better-sqlite3',
connection: { filename: ':memory:' },
useNullAsDefault: true,
});
await sqliteDriver.connect();
sqliteOk = true;
} catch {
sqliteOk = false;
if (sqliteDriver?.disconnect) {
try { await sqliteDriver.disconnect(); } catch { /* ignore */ }
}
}

if (sqliteOk) {
await kernel.use(new DriverPlugin(sqliteDriver));
trackPlugin('SqlDriver');
resolvedDriverLabel = 'SqlDriver(sqlite)';
resolvedDatabaseUrl = ':memory:';
} else {
const { InMemoryDriver } = await import('@objectstack/driver-memory');
await kernel.use(new DriverPlugin(new InMemoryDriver()));
trackPlugin('MemoryDriver');
resolvedDriverLabel = 'InMemoryDriver';
resolvedDatabaseUrl = '(in-memory)';
console.warn(chalk.yellow(
' ⚠ better-sqlite3 unavailable — dev falling back to InMemoryDriver (mingo, not real SQL).\n' +
' Rebuild better-sqlite3, or set OS_DATABASE_URL / OS_DATABASE_DRIVER for SQL fidelity.'
));
}
}
} catch (e: any) {
// silent
Expand Down