From d941b40133f93d933f716fd535364b8d37882428 Mon Sep 17 00:00:00 2001 From: Suleiman Shahbari Date: Sat, 4 Jul 2026 00:30:43 +0300 Subject: [PATCH 1/3] feat(framework): opt-in vike-auth composition framing (#186, Tier 1) Adds a vike-auth-composer persona (ai-autopilot) that tells the agent to compose vike-auth for authentication instead of hand-rolling sessions, cookies, and login pages, plus a vikeExtensionPersonas set that keeps the Prisma data persona but swaps in auth composition. Framework opts in via `--compose-extensions` / RunFrameworkOptions.composeExtensions; default framing is unchanged and publish-safe (no vike-auth), since the extensions currently resolve only inside the vike-data workspace. Offline-verified: persona content + opt-in wiring tests; ai-autopilot 261, framework 57; typecheck 20/20; --fake --compose-extensions runs clean. No changeset yet: not publish-ready until the live in-workspace proof lands and the vike-* publish story is decided. --- packages/ai-autopilot/src/index.ts | 2 + packages/ai-autopilot/src/personas/index.ts | 2 + .../ai-autopilot/src/personas/library.test.ts | 26 ++++++++ packages/ai-autopilot/src/personas/library.ts | 65 +++++++++++++++++++ packages/framework/src/cli.ts | 9 +++ packages/framework/src/run.test.ts | 42 ++++++++++++ packages/framework/src/run.ts | 14 +++- 7 files changed, 159 insertions(+), 1 deletion(-) create mode 100644 packages/ai-autopilot/src/personas/library.test.ts diff --git a/packages/ai-autopilot/src/index.ts b/packages/ai-autopilot/src/index.ts index c82a4dc..698f0c5 100644 --- a/packages/ai-autopilot/src/index.ts +++ b/packages/ai-autopilot/src/index.ts @@ -108,7 +108,9 @@ export { nextPageBuilder, dataModeler, uiIntentDesigner, + vikeAuthComposer, sharedPersonas, + vikeExtensionPersonas, stackPersonas, type Persona, type PersonaSpec, diff --git a/packages/ai-autopilot/src/personas/index.ts b/packages/ai-autopilot/src/personas/index.ts index 32e495c..16796f7 100644 --- a/packages/ai-autopilot/src/personas/index.ts +++ b/packages/ai-autopilot/src/personas/index.ts @@ -21,7 +21,9 @@ export { nextPageBuilder, dataModeler, uiIntentDesigner, + vikeAuthComposer, sharedPersonas, + vikeExtensionPersonas, stackPersonas, } from './library.js' export type { Persona, PersonaSpec } from './types.js' diff --git a/packages/ai-autopilot/src/personas/library.test.ts b/packages/ai-autopilot/src/personas/library.test.ts new file mode 100644 index 0000000..e8491cb --- /dev/null +++ b/packages/ai-autopilot/src/personas/library.test.ts @@ -0,0 +1,26 @@ +import { strict as assert } from 'node:assert' +import { describe, it } from 'node:test' +import { personaInstructions } from './compose.js' +import { dataModeler, uiIntentDesigner, vikeAuthComposer, vikeExtensionPersonas } from './library.js' + +describe('vike extension personas', () => { + it('vikeAuthComposer teaches composing vike-auth instead of hand-rolling auth', () => { + assert.equal(vikeAuthComposer.name, 'vike-auth-composer') + const text = personaInstructions(vikeAuthComposer) + // Real install + wiring, so the agent composes rather than reinventing. + assert.match(text, /npm install vike-auth/) + assert.match(text, /vike-auth\/react/) + assert.match(text, /setAdapter\(createMemoryAdapter\(\)\)/) + assert.match(text, /useUser\(\)/) + // And it must forbid re-modeling users/sessions in the app's own ORM. + assert.match(text, /Do NOT write auth UI/) + assert.match(text, /do NOT model users or\s*\n?\s*sessions/i) + }) + + it('vikeExtensionPersonas keeps the ORM data persona + adds auth', () => { + const names = vikeExtensionPersonas.map(p => p.name) + assert.deepEqual(names, ['data-modeler', 'vike-auth-composer', 'ui-intent-designer']) + assert.ok(vikeExtensionPersonas.includes(dataModeler)) + assert.ok(vikeExtensionPersonas.includes(uiIntentDesigner)) + }) +}) diff --git a/packages/ai-autopilot/src/personas/library.ts b/packages/ai-autopilot/src/personas/library.ts index e9a82f0..372ee89 100644 --- a/packages/ai-autopilot/src/personas/library.ts +++ b/packages/ai-autopilot/src/personas/library.ts @@ -121,6 +121,59 @@ Your output should read as a declaration of what the UI is, leaving how it looks to the decoupled implementation.`, }) +/** + * Composes `vike-auth` for authentication instead of hand-rolling it. Auth is a + * solved, security-sensitive concern; the live-build failure mode is an agent + * reinventing sessions/cookies/CSRF (and getting them wrong). This persona hands + * that whole surface to the extension. Opt-in: vike-auth is Vike-specific, and + * (today) resolves only inside the vike-data workspace — see `vikeExtensionPersonas`. + */ +export const vikeAuthComposer: Persona = definePersona({ + name: 'vike-auth-composer', + role: 'Composes vike-auth for auth instead of hand-rolling sessions, cookies, and login pages', + appliesTo: ['vike-auth'], + systemPrompt: `You compose vike-auth for authentication instead of hand-rolling it. Auth is a +solved, security-sensitive concern: a hand-rolled version (sessions, cookies, +password hashing, CSRF, rate limiting) is where apps get it wrong. vike-auth owns it. + +What vike-auth gives you (passwordless, email magic-link): +- Its own tables: \`users\`, \`sessions\`, \`login_tokens\`. Do NOT model users or + sessions in your app's ORM, and do NOT write login / logout / session code. +- The \`/login\` and \`/account\` pages, shipped by the extension. Do NOT write auth UI. +- The current user on \`pageContext.user\` (server) and via \`useUser()\` (React). + +Install and wire it (React + Vike): +- Add vike-auth and an ORM adapter to the app (vike-auth pulls the rest of its + stack transitively): + \`npm install vike-auth @universal-orm/core @universal-orm/memory\` +- In \`pages/+config.js\`, extend the React auth entry — this ONE line brings the + server tier AND the \`/login\` + \`/account\` pages: + \`\`\`js + import vikeReact from 'vike-react/config' + import authExt from 'vike-auth/react' + export default { extends: [vikeReact, authExt] } // loginRedirect: '/admin' to land signed-in users there + \`\`\` +- Register ONE universal-orm adapter, in \`pages/+onCreateGlobalContext.js\`, so + vike-auth's tables persist (memory is fine for dev; swap for a real DB later): + \`\`\`js + import { setAdapter, getAdapter } from '@universal-orm/core' + import { createMemoryAdapter } from '@universal-orm/memory' + export default async function onCreateGlobalContext() { + if (getAdapter()) return + setAdapter(createMemoryAdapter()) + } + \`\`\` + +Use it: +- Read the user server-side from \`pageContext.user\`; in a React component use + \`useUser()\` from \`vike-auth/react/hooks\` (returns \`{ id, email, name } | null\`). +- Protect a page by checking \`pageContext.user\` in a \`+guard\` (redirect to + \`/login\` when absent), or reuse vike-auth's own guard. + +Your app's OWN domain data (posts, comments, etc.) still goes through the data +persona's ORM. vike-auth owns only identity and sessions — do not duplicate them.`, +}) + /** * The framework-neutral personas shared by every preset — the data layer and the * intent-based UI guardrail apply the same whether the app is on Vike or Next. @@ -131,6 +184,18 @@ export const sharedPersonas: readonly Persona[] = Object.freeze([ uiIntentDesigner, ]) +/** + * The opt-in vike-extension stack: keep the ORM data persona for the app's own + * domain data, but compose `vike-auth` for authentication instead of hand-rolling + * it. Swap this in for {@link sharedPersonas} when composing extensions (Vike + * only; the extensions currently resolve inside the vike-data workspace). + */ +export const vikeExtensionPersonas: readonly Persona[] = Object.freeze([ + dataModeler, + vikeAuthComposer, + uiIntentDesigner, +]) + /** All built-in stack personas (the Vike stack), in a stable order. */ export const stackPersonas: readonly Persona[] = Object.freeze([ vikePageBuilder, diff --git a/packages/framework/src/cli.ts b/packages/framework/src/cli.ts index cc699e0..be778e3 100644 --- a/packages/framework/src/cli.ts +++ b/packages/framework/src/cli.ts @@ -34,6 +34,9 @@ Options: --cwd Workspace the agent builds in (default: current directory). --model Model to pass through to the wrapped agent. --scope How much app to build (default: full). + --compose-extensions Compose the vike-* extensions (vike-auth for auth) instead + of hand-rolling them. Vike-only; extensions resolve in the + vike-data workspace (default: off, hand-rolled + Prisma). --max-passes Full-fledged loop pass budget (default: 5). --permission-mode Claude Code permission mode: default | acceptEdits | bypassPermissions | plan (default: acceptEdits). @@ -85,6 +88,7 @@ export interface CliOptions { servePath?: string | undefined port?: number dashboard: boolean + composeExtensions: boolean sessionLink?: string | undefined permissionMode?: PermissionMode | undefined skipPermissions: boolean @@ -102,6 +106,7 @@ export function parseArgs(argv: string[]): CliOptions { intent: '', scope: 'full', dashboard: true, + composeExtensions: false, skipPermissions: false, } const PERMISSION_MODES: PermissionMode[] = ['default', 'acceptEdits', 'bypassPermissions', 'plan'] @@ -123,6 +128,9 @@ export function parseArgs(argv: string[]): CliOptions { case '--no-dashboard': opts.dashboard = false break + case '--compose-extensions': + opts.composeExtensions = true + break case '--skip-preflight': opts.skipPreflight = true break @@ -321,6 +329,7 @@ export async function runCli(argv: string[], io: CliIO = defaultIO): Promise string } { + const fd = fakeDriver() + let captured = '' + // Name it 'fake' so the workspace-verify stays off (no fs access in this unit test). + const driver: Driver = { + name: 'fake', + start: opts => { + captured = opts.system ?? '' + return fd.start(opts) + }, + } + return { driver, system: () => captured } +} + test('runFramework drives the whole flow through the driver, offline, to production-grade', async () => { const events: FrameworkEvent[] = [] const { result, detection } = await runFramework({ @@ -97,6 +113,32 @@ test('runFramework shows a literal session link immediately (no template)', asyn assert.equal(session.sessionLink, 'https://code.example.com/live') }) +test('--compose-extensions frames the agent with vike-auth, not hand-rolled auth (#186)', async () => { + const { driver, system } = recordingDriver() + await runFramework({ + intent: FAKE_INTENT, + driver, + cwd: '/tmp/ws', + signals: FAKE_SIGNALS, + composeExtensions: true, + onEvent: () => {}, + }) + assert.match(system(), /vike-auth/) + assert.match(system(), /npm install vike-auth/) +}) + +test('without --compose-extensions the default framing has no vike-auth (publish-safe)', async () => { + const { driver, system } = recordingDriver() + await runFramework({ + intent: FAKE_INTENT, + driver, + cwd: '/tmp/ws', + signals: FAKE_SIGNALS, + onEvent: () => {}, + }) + assert.doesNotMatch(system(), /vike-auth/) +}) + test('the default pass budget is raised for from-scratch builds (#182)', () => { // 3 was too low: the first passes go to bootstrapping an empty workspace. assert.equal(DEFAULT_MAX_PASSES, 5) diff --git a/packages/framework/src/run.ts b/packages/framework/src/run.ts index 254dcf6..b578571 100644 --- a/packages/framework/src/run.ts +++ b/packages/framework/src/run.ts @@ -7,6 +7,7 @@ import { personaInstructions, presetPersonas, serveCheck, + vikeExtensionPersonas, type BootstrapEvent, type BootstrapResult, type BootstrapScope, @@ -75,6 +76,13 @@ export interface RunFrameworkOptions { model?: string /** Signals for preset detection (deps/files). Default: none, so the flagship preset wins. */ signals?: FrameworkSignals + /** + * Compose the vike-* extensions (currently: vike-auth for authentication) + * instead of hand-rolling them. Frames the agent with the extension personas. + * Opt-in and Vike-only: the extensions resolve inside the vike-data workspace, + * so the default (hand-rolled + Prisma) path stays publish-safe. + */ + composeExtensions?: boolean /** Max full-fledged passes. Default {@link DEFAULT_MAX_PASSES} (5). */ maxPasses?: number /** A deploy decision to narrate at the end. Omit to skip the deploy phase. */ @@ -152,8 +160,12 @@ export async function runFramework(opts: RunFrameworkOptions): Promise Date: Sat, 4 Jul 2026 00:58:52 +0300 Subject: [PATCH 2/3] feat(framework): compose path uses the universal-orm data layer, not Prisma (#186) The vike-* compose path was still pointing domain data at Prisma, so a live run burned time on Prisma install/config/migrations for data that can ride the one adapter vike-auth already registers. Adds a vike-data-modeler persona (schema via @vike-data/vike-schema, repository via @universal-orm/core over the registered adapter, no ORM install / no migrations) and makes vikeExtensionPersonas [vikeDataModeler, vikeAuthComposer, uiIntentDesigner]. Default path stays on Prisma (publish-safe). ai-autopilot 262, framework 57. --- packages/ai-autopilot/src/index.ts | 1 + packages/ai-autopilot/src/personas/index.ts | 1 + .../ai-autopilot/src/personas/library.test.ts | 18 ++++-- packages/ai-autopilot/src/personas/library.ts | 61 +++++++++++++++++-- 4 files changed, 72 insertions(+), 9 deletions(-) diff --git a/packages/ai-autopilot/src/index.ts b/packages/ai-autopilot/src/index.ts index 698f0c5..58580ea 100644 --- a/packages/ai-autopilot/src/index.ts +++ b/packages/ai-autopilot/src/index.ts @@ -109,6 +109,7 @@ export { dataModeler, uiIntentDesigner, vikeAuthComposer, + vikeDataModeler, sharedPersonas, vikeExtensionPersonas, stackPersonas, diff --git a/packages/ai-autopilot/src/personas/index.ts b/packages/ai-autopilot/src/personas/index.ts index 16796f7..d3f5bb1 100644 --- a/packages/ai-autopilot/src/personas/index.ts +++ b/packages/ai-autopilot/src/personas/index.ts @@ -22,6 +22,7 @@ export { dataModeler, uiIntentDesigner, vikeAuthComposer, + vikeDataModeler, sharedPersonas, vikeExtensionPersonas, stackPersonas, diff --git a/packages/ai-autopilot/src/personas/library.test.ts b/packages/ai-autopilot/src/personas/library.test.ts index e8491cb..481fadd 100644 --- a/packages/ai-autopilot/src/personas/library.test.ts +++ b/packages/ai-autopilot/src/personas/library.test.ts @@ -1,7 +1,7 @@ import { strict as assert } from 'node:assert' import { describe, it } from 'node:test' import { personaInstructions } from './compose.js' -import { dataModeler, uiIntentDesigner, vikeAuthComposer, vikeExtensionPersonas } from './library.js' +import { uiIntentDesigner, vikeAuthComposer, vikeDataModeler, vikeExtensionPersonas } from './library.js' describe('vike extension personas', () => { it('vikeAuthComposer teaches composing vike-auth instead of hand-rolling auth', () => { @@ -17,10 +17,20 @@ describe('vike extension personas', () => { assert.match(text, /do NOT model users or\s*\n?\s*sessions/i) }) - it('vikeExtensionPersonas keeps the ORM data persona + adds auth', () => { + it('vikeDataModeler teaches the universal-orm data layer, not a hand-installed ORM', () => { + assert.equal(vikeDataModeler.name, 'vike-data-modeler') + const text = personaInstructions(vikeDataModeler) + assert.match(text, /@universal-orm\/core/) + assert.match(text, /defineSchema/) + assert.match(text, /createRepository/) + assert.match(text, /getAdapter\(\)/) + // The whole point: do NOT reach for Prisma/Drizzle/SQLite/migrations. + assert.match(text, /Do NOT add Prisma, Drizzle,\s*\n?\s*SQLite/i) + }) + + it('vikeExtensionPersonas composes the data layer + auth (no Prisma, no hand-rolled auth)', () => { const names = vikeExtensionPersonas.map(p => p.name) - assert.deepEqual(names, ['data-modeler', 'vike-auth-composer', 'ui-intent-designer']) - assert.ok(vikeExtensionPersonas.includes(dataModeler)) + assert.deepEqual(names, ['vike-data-modeler', 'vike-auth-composer', 'ui-intent-designer']) assert.ok(vikeExtensionPersonas.includes(uiIntentDesigner)) }) }) diff --git a/packages/ai-autopilot/src/personas/library.ts b/packages/ai-autopilot/src/personas/library.ts index 372ee89..d4549a6 100644 --- a/packages/ai-autopilot/src/personas/library.ts +++ b/packages/ai-autopilot/src/personas/library.ts @@ -174,6 +174,56 @@ Your app's OWN domain data (posts, comments, etc.) still goes through the data persona's ORM. vike-auth owns only identity and sessions — do not duplicate them.`, }) +/** + * Models domain data on the universal-orm data layer (the same one vike-auth + * uses) instead of a hand-installed ORM. The live-build failure mode is an agent + * burning time on ORM install/config/migrations (e.g. Prisma) for data that could + * ride the one adapter the app already registered. Opt-in, in-workspace only + * (the packages resolve inside the vike-data workspace). + */ +export const vikeDataModeler: Persona = definePersona({ + name: 'vike-data-modeler', + role: 'Models domain data on the universal-orm data layer — one registered adapter, no ORM install', + appliesTo: ['@universal-orm/core', '@vike-data/vike-schema', '@vike-data/universal-schema'], + systemPrompt: `You model the app's domain data on the universal-orm data layer — the same layer +vike-auth uses — NOT on a hand-installed ORM. The app registers ONE adapter at +startup (memory in dev), and every table rides it, so there is nothing to install, +no database to provision, and no migrations to run. Do NOT add Prisma, Drizzle, +SQLite, or any ORM: that churn is exactly what this layer removes. + +Define tables and build a repository over the already-registered adapter: +\`\`\`js +import { defineSchema } from '@vike-data/vike-schema/schema' +import { mergeSchemas } from '@vike-data/universal-schema' +import { createRepository, getAdapter } from '@universal-orm/core' + +const posts = defineSchema('posts', (t) => { + t.integer('id').primary() + t.string('title') + t.text('content') + t.string('created_at') +}) +let repo +// getAdapter() returns the adapter the app registered in +onCreateGlobalContext. +export const db = () => (repo ??= createRepository(mergeSchemas([posts]), getAdapter())) +\`\`\` + +Read and write through the narrow repository: +- \`db().posts\`: \`insert(row)\`, \`find(filter, opts)\`, \`findOne(filter)\`, + \`upsert(row, { onConflict })\`, \`update(filter, patch)\`, \`delete(filter)\`. +- Filters are equality (\`{ post_id: 5 }\`) or membership (\`{ id: { in: [1, 2] } }\`) + ONLY — there are no joins or aggregates. For "a post with its comments", do two + finds and combine them in JS. \`opts\` is \`{ limit, offset, orderBy }\`. +- The memory adapter does NOT auto-assign ids — mint them yourself (a counter or uuid). +- Do NOT call \`setAdapter\` again; the app already registered one. + +Column types: \`uuid\` / \`string\` / \`text\` / \`integer\` / \`boolean\` / \`timestamp\`, +each chainable with \`.nullable()\` / \`.unique()\` / \`.primary()\` / +\`.references('table.col', { onDelete })\`. Read data in Vike \`+data\` hooks on the +server. Swapping the dev memory adapter for a real database is a one-line adapter +change at startup; your schema and queries do not change.`, +}) + /** * The framework-neutral personas shared by every preset — the data layer and the * intent-based UI guardrail apply the same whether the app is on Vike or Next. @@ -185,13 +235,14 @@ export const sharedPersonas: readonly Persona[] = Object.freeze([ ]) /** - * The opt-in vike-extension stack: keep the ORM data persona for the app's own - * domain data, but compose `vike-auth` for authentication instead of hand-rolling - * it. Swap this in for {@link sharedPersonas} when composing extensions (Vike - * only; the extensions currently resolve inside the vike-data workspace). + * The opt-in vike-extension stack: compose `vike-auth` for authentication AND the + * universal-orm data layer for domain data (both ride one registered adapter), + * instead of hand-rolling auth or hand-installing an ORM. Swap this in for + * {@link sharedPersonas} when composing extensions (Vike only; the extensions + * currently resolve inside the vike-data workspace). */ export const vikeExtensionPersonas: readonly Persona[] = Object.freeze([ - dataModeler, + vikeDataModeler, vikeAuthComposer, uiIntentDesigner, ]) From 5fc947bff11f4085f5eca21332a15ec2af8d6e92 Mon Sep 17 00:00:00 2001 From: Suleiman Shahbari Date: Sat, 4 Jul 2026 01:42:51 +0300 Subject: [PATCH 3/3] feat(framework): teach compose personas real-DB persistence (drizzle + pglite) (#187) The composed vike-auth + universal-orm stack runs on the memory adapter, which resets on restart, so accounts and posts vanish on reboot. Teach the data and auth personas the opt-in swap to the Drizzle adapter over an embedded pglite Postgres: register it instead of the memory adapter, add vikeSchema() to codegen the drizzle schema, and derive/apply migrations with drizzle-kit. Schema and queries do not change, and because auth and data ride the same one adapter, the single swap makes both durable at once. Memory stays the zero-config dev default. Part of #186. Closes #187. --- .../compose-drizzle-pglite-persistence.md | 7 ++++ .../ai-autopilot/src/personas/library.test.ts | 9 ++++- packages/ai-autopilot/src/personas/library.ts | 35 +++++++++++++++++-- 3 files changed, 48 insertions(+), 3 deletions(-) create mode 100644 .changeset/compose-drizzle-pglite-persistence.md diff --git a/.changeset/compose-drizzle-pglite-persistence.md b/.changeset/compose-drizzle-pglite-persistence.md new file mode 100644 index 0000000..e8aaeab --- /dev/null +++ b/.changeset/compose-drizzle-pglite-persistence.md @@ -0,0 +1,7 @@ +--- +'@gemstack/ai-autopilot': minor +--- + +Teach the compose personas the opt-in real-persistence path (drizzle + pglite) + +The composed stack (vike-auth + the universal-orm data layer) runs on the memory adapter, which resets on every server restart, so accounts and posts vanish on reboot. The `vike-data-modeler` persona now teaches the "make it real" swap: register the Drizzle adapter over an embedded pglite Postgres instead of the memory adapter, add the `vikeSchema()` Vite plugin to codegen `drizzle/schema.generated.ts`, and derive/apply migrations with drizzle-kit. Because auth and domain data ride the same one adapter, that single swap makes both durable at once; `defineSchema` tables and `db()` queries do not change. The `vike-auth-composer` persona points at the same step, and the memory adapter stays the zero-config dev default. Reference: the proven `examples/drizzle-pglite` twin. Part of #186. Closes #187. diff --git a/packages/ai-autopilot/src/personas/library.test.ts b/packages/ai-autopilot/src/personas/library.test.ts index 481fadd..b2bd789 100644 --- a/packages/ai-autopilot/src/personas/library.test.ts +++ b/packages/ai-autopilot/src/personas/library.test.ts @@ -24,8 +24,15 @@ describe('vike extension personas', () => { assert.match(text, /defineSchema/) assert.match(text, /createRepository/) assert.match(text, /getAdapter\(\)/) - // The whole point: do NOT reach for Prisma/Drizzle/SQLite/migrations. + // The dev default: do NOT reach for Prisma/Drizzle/SQLite/migrations. assert.match(text, /Do NOT add Prisma, Drizzle,\s*\n?\s*SQLite/i) + // But it must also teach the opt-in real-persistence path (drizzle + pglite), + // so a composed app can survive a restart without changing schema/queries. + assert.match(text, /Make it real/) + assert.match(text, /registerDrizzle/) + assert.match(text, /pglite/) + assert.match(text, /vikeSchema\(\)/) + assert.match(text, /drizzle-kit generate/) }) it('vikeExtensionPersonas composes the data layer + auth (no Prisma, no hand-rolled auth)', () => { diff --git a/packages/ai-autopilot/src/personas/library.ts b/packages/ai-autopilot/src/personas/library.ts index d4549a6..fc2d170 100644 --- a/packages/ai-autopilot/src/personas/library.ts +++ b/packages/ai-autopilot/src/personas/library.ts @@ -163,6 +163,10 @@ Install and wire it (React + Vike): setAdapter(createMemoryAdapter()) } \`\`\` + Memory resets on restart, so accounts vanish on reboot. To persist accounts for + real, swap this ONE adapter for the Drizzle + pglite backend — see the data + persona's "Make it real" steps. Because auth AND your domain data ride this same + adapter, that single swap makes both durable at once. Use it: - Read the user server-side from \`pageContext.user\`; in a React component use @@ -220,8 +224,35 @@ Read and write through the narrow repository: Column types: \`uuid\` / \`string\` / \`text\` / \`integer\` / \`boolean\` / \`timestamp\`, each chainable with \`.nullable()\` / \`.unique()\` / \`.primary()\` / \`.references('table.col', { onDelete })\`. Read data in Vike \`+data\` hooks on the -server. Swapping the dev memory adapter for a real database is a one-line adapter -change at startup; your schema and queries do not change.`, +server. + +Make it real (opt-in persistence). The memory adapter resets on every restart. To +persist for real, swap the ONE adapter the app registers in +\`pages/+onCreateGlobalContext.js\` from memory to the Drizzle adapter over an embedded +pglite Postgres (real Postgres as wasm; no server to run). Your \`defineSchema\` tables +and every \`db().posts\` query stay identical, and because vike-auth rides the SAME +adapter, this one swap makes accounts AND domain data survive a restart. This is NOT +"add an ORM to model with" — you still model with \`defineSchema\`; Drizzle is only the +persistence backend. The steps: +1. Install: \`npm install vike-drizzle @universal-orm/drizzle drizzle-orm @electric-sql/pglite\` + (and \`drizzle-kit\` as a dev dep). +2. In \`vite.config.js\`, add the \`vikeSchema()\` plugin (\`@vike-data/vike-schema/plugin\`) + AFTER \`vike()\`: it generates \`drizzle/schema.generated.ts\` from every installed + extension's tables (your posts/comments AND vike-auth's users/sessions). Also add + \`ssr: { external: ['@electric-sql/pglite', 'drizzle-orm'] }\` to keep pglite's wasm + out of the client bundle. +3. Add \`drizzle.config.js\` (\`{ schema: './drizzle/schema.generated.ts', out: + './drizzle/migrations', dialect: 'postgresql' }\`) and run \`drizzle-kit generate\` to + derive the SQL migrations from that generated schema. +4. In \`pages/+onCreateGlobalContext.js\`, guard the DB setup with + \`if (!import.meta.env.SSR) return\`, open pglite, \`migrate(db, { migrationsFolder: + 'drizzle/migrations' })\`, then \`registerDrizzle(db, schema)\` from \`vike-drizzle\` + instead of \`setAdapter(createMemoryAdapter())\`. +Reference: the proven \`examples/drizzle-pglite\` twin in the vike-data monorepo. Note: +\`integer('id').primary()\` is NOT auto-incremented (same as memory) — keep minting ids +yourself, or use \`uuid('id').primary()\` for collision-free ids on a persistent store, +and never re-insert fixed-id seed rows on every boot (that duplicates or crashes on a +real DB).`, }) /**