@webjsdev/cli 0.10.17 → 0.10.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11,7 +11,7 @@ companion and reach for docs.webjs.com whenever you need more detail.
11
11
 
12
12
  This project was created with `webjs create`. The files you see right
13
13
  now (`app/page.ts` printing "Hello from {{APP_NAME}}", the example `User`
14
- model in `prisma/schema.prisma`, the `theme-toggle` component, the
14
+ model in `db/schema.server.ts`, the `theme-toggle` component, the
15
15
  example users module in api/saas templates) are **starting-point
16
16
  references, not the final product**. Your job is to replace them with
17
17
  the app the user actually asked for. That includes adapting
@@ -30,11 +30,11 @@ user asked for, never leftover scaffold code.
30
30
 
31
31
  **Non-negotiables for every webjs app:**
32
32
 
33
- 1. **Use Prisma + SQLite for persistence.** It's already wired up
34
- (`prisma/schema.prisma`, `lib/prisma.server.ts`, `npm run db:migrate`,
35
- `predev` hook running `prisma generate`). For any data the app
36
- stores (todos, posts, messages, products, comments, anything),
37
- define a Prisma model and persist there.
33
+ 1. **Use Drizzle + SQLite for persistence.** It's already wired up
34
+ (`db/schema.server.ts`, `db/connection.server.ts`, `npm run db:generate`
35
+ + `npm run db:migrate`). For any data the app stores (todos, posts,
36
+ messages, products, comments, anything), define a Drizzle table and
37
+ persist there.
38
38
  - **NEVER** store app data in JSON files (`data/todos.json`,
39
39
  `db.json`, …). It resets on reload and cannot scale. This is a project convention,
40
40
  and the user's prompt explicitly forbids it.
@@ -47,10 +47,10 @@ user asked for, never leftover scaffold code.
47
47
  `full-stack` (default), `--template api`, `--template saas`. Don't
48
48
  reach for a `--template blog` / `--template todo` / `--template
49
49
  ecommerce`. They don't exist and the CLI will reject them.
50
- 3. **First step after scaffolding:** edit `prisma/schema.prisma` to the
50
+ 3. **First step after scaffolding:** edit `db/schema.server.ts` to the
51
51
  app's real domain models (delete the example `User` model unless the
52
- app actually needs users), run `webjs db migrate <name>`, then build
53
- pages / actions / queries against those models.
52
+ app actually needs users), run `webjs db generate` then
53
+ `webjs db migrate`, then build pages / actions / queries against them.
54
54
 
55
55
  **Picking the right scaffold from the user's prompt** (you do this BEFORE
56
56
  running `webjs create`; if you're reading this you've already scaffolded.
@@ -202,10 +202,10 @@ Pure functions that return Tailwind class strings. You apply them to
202
202
  import {
203
203
  cardClass, cardHeaderClass, cardTitleClass,
204
204
  cardContentClass, cardFooterClass,
205
- } from '../../components/ui/card.ts';
206
- import { inputClass } from '../../components/ui/input.ts';
207
- import { labelClass } from '../../components/ui/label.ts';
208
- import { buttonClass } from '../../components/ui/button.ts';
205
+ } from '#components/ui/card.ts';
206
+ import { inputClass } from '#components/ui/input.ts';
207
+ import { labelClass } from '#components/ui/label.ts';
208
+ import { buttonClass } from '#components/ui/button.ts';
209
209
 
210
210
  return html`
211
211
  <div class=${cardClass()}>
@@ -237,13 +237,13 @@ elements. Import them once (typically in `app/layout.ts`) and use
237
237
 
238
238
  ```ts
239
239
  // app/layout.ts (registers the custom elements for every page)
240
- import '../components/ui/dialog.ts';
241
- import '../components/ui/tabs.ts';
240
+ import '#components/ui/dialog.ts';
241
+ import '#components/ui/tabs.ts';
242
242
  ```
243
243
 
244
244
  ```ts
245
245
  // app/some-page/page.ts (uses the registered elements)
246
- import { buttonClass } from '../../components/ui/button.ts';
246
+ import { buttonClass } from '#components/ui/button.ts';
247
247
 
248
248
  return html`
249
249
  <ui-dialog>
@@ -322,12 +322,15 @@ modules/<feature>/
322
322
  utils/*.ts feature-scoped helpers
323
323
  types.ts feature types
324
324
  lib/
325
- prisma.ts PrismaClient singleton (import from here, never `new PrismaClient()`)
326
- ... other cross-cutting infra (session, auth config, etc.)
327
- prisma/
328
- schema.prisma Prisma schema, SQLite by default, switch provider for Postgres/MySQL
329
- dev.db SQLite file (gitignored); run `npm run db:migrate` to create
330
- migrations/ generated migration SQL
325
+ ... cross-cutting infra (session, auth config, etc.)
326
+ db/
327
+ schema.server.ts Drizzle models + relations (your data layer)
328
+ columns.server.ts column helpers (dialect-specific; the only file to swap for Postgres)
329
+ connection.server.ts opens the driver, exports the \`db\` singleton (import \`db\` from here)
330
+ seed.server.ts optional seed (run via \`webjs db seed\`)
331
+ dev.db SQLite file (gitignored); run \`npm run db:migrate\` to create
332
+ migrations/ generated migration SQL (committed)
333
+ drizzle.config.ts drizzle-kit config (root; SQLite by default, --db postgres to switch)
331
334
  public/ static assets, served at /public/*
332
335
  test/<feature>/ feature-scoped tests, one folder per concern
333
336
  <name>.test.ts node unit / integration test (node --test)
@@ -359,39 +362,44 @@ Run `webjs types` once (and ensure `tsconfig.json` `include` lists
359
362
  startup, so it stays current. Without it, `params` is `Record<string, string>`
360
363
  and `navigate()` accepts any string (non-breaking).
361
364
 
362
- ## Database (Prisma + SQLite by default)
365
+ ## Database (Drizzle + SQLite by default)
363
366
 
364
- Every scaffold includes a Prisma setup pointed at a local SQLite file.
367
+ Every scaffold includes a Drizzle setup pointed at a local SQLite file,
368
+ under a `db/` folder (`schema.server.ts`, `columns.server.ts`,
369
+ `connection.server.ts`). Drizzle has no codegen and no engine binary.
365
370
  First-run workflow:
366
371
 
367
372
  ```sh
368
373
  cp .env.example .env # DATABASE_URL is pre-filled for SQLite
369
- npm run db:migrate # creates prisma/dev.db + migration
370
- npm run dev # webjs dev + prisma generate via predev
374
+ npm run db:generate # schema -> SQL migration (drizzle-kit)
375
+ npm run db:migrate # apply it (creates db/dev.db)
376
+ npm run dev # webjs dev, then serves
371
377
  ```
372
378
 
373
- ### Always `npm run dev` / `npm start`, never `webjs dev` / `webjs start` directly
379
+ ### `npm run dev` / `npm start` and `webjs dev` / `webjs start` behave identically
374
380
 
375
- `webjs dev` and `webjs start` are framework primitives, they only run
376
- the webjs server. They do **not** run `prisma generate`, do **not** run
377
- `prisma migrate deploy`, do **not** spawn the Tailwind watcher, do
378
- **not** run any other per-app process this `package.json` composes.
381
+ `npm run dev` and `npm start` are the documented entrypoints, and they
382
+ are thin aliases for `webjs dev` / `webjs start`. The start orchestration
383
+ (applying migrations, and any parallel watcher like the Tailwind CLI)
384
+ lives in the `webjs` block of `package.json` and runs INSIDE
385
+ `webjs dev` / `webjs start`:
379
386
 
380
- `npm run dev` and `npm start` are the app-level entrypoints. They run
381
- the webjs server **plus** every other process the app needs, wired
382
- together via `predev` / `prestart` hooks and (where present)
383
- `concurrently` for parallel watchers. Skipping the npm wrapper produces
384
- silent breakage: a stale Prisma client, missing `public/tailwind.css`,
385
- an unmigrated database in production, etc.
387
+ ```jsonc
388
+ "webjs": {
389
+ "start": { "before": ["webjs db migrate"] }
390
+ }
391
+ ```
386
392
 
387
- Same split Rails 7+ uses: `bin/rails server` is the framework
388
- primitive, `bin/dev` is the orchestrator. webjs uses npm scripts +
389
- hooks for the same role, because as a no-build framework Tailwind /
390
- Prisma / etc. cannot be bundler plugins.
393
+ Drizzle has no codegen, so there is no dev `before` step. An app that
394
+ adds the Tailwind CLI puts its `--watch` command under
395
+ `webjs.dev.parallel` and it runs alongside the server, torn down on exit.
396
+ `before` steps run to completion first; a failed `webjs db migrate`
397
+ aborts the boot with a clear message rather than serving a stale schema.
391
398
 
392
- In Docker / Railway, prefer `npm start` (or `node node_modules/.bin/npm
393
- start`) as the CMD over `node ... webjs.js start ...`. The npm form
394
- fires `prestart`; the direct binary form skips it.
399
+ In Docker / Railway, `CMD ["npm", "start"]` and `CMD ["webjs", "start"]`
400
+ are equivalent: `webjs start` runs `webjs.start.before` (`webjs db
401
+ migrate`) in-process before serving, so the migrate no longer depends on
402
+ an npm `prestart` hook.
395
403
 
396
404
  ### Running on Bun instead of Node
397
405
 
@@ -407,14 +415,16 @@ bun --bun run dev # or: bun --bun run start
407
415
  On Node the `.ts` type-stripping is the built-in `module.stripTypeScriptTypes`;
408
416
  on Bun (which has no built-in) it comes from `amaro` automatically, so the same
409
417
  source serves identically. SSR action-result seeding (an internal hydration
410
- optimization) is off on Bun (it needs `module.registerHooks`), which only means
411
- an async-render component re-fetches once on hydration, no behavior change.
418
+ optimization) works on both runtimes: Node installs it via `module.registerHooks`,
419
+ Bun via a `Bun.plugin` `onLoad`, so an async-render component does not re-fetch
420
+ on hydration on either runtime.
412
421
 
413
422
  **Containerized deploy ships with the scaffold.** `Dockerfile`,
414
423
  `compose.yaml`, and `.dockerignore` are scaffolded at the app root. The
415
424
  Dockerfile pins `node:24-alpine` (the same Node major CI uses), installs
416
- deps, runs `prisma generate`, and starts via `npm start` so `prestart`
417
- applies migrations. Run it locally with `docker compose up --build` (the
425
+ deps (no build step, since Drizzle has no codegen), and starts via
426
+ `npm start` (`webjs start` runs `webjs.start.before` = `webjs db migrate`
427
+ before serving). Run it locally with `docker compose up --build` (the
418
428
  app comes up on http://localhost:8080 against a SQLite file on a named
419
429
  volume). For production, point `DATABASE_URL` at managed Postgres and set
420
430
  `AUTH_SECRET`. The `.dockerignore` keeps the `.webjs/vendor/` importmap in
@@ -439,24 +449,28 @@ live DB ping), add an optional `readiness.{js,ts}` at the app root that
439
449
  default-exports an async check; `/__webjs/ready` runs it once warm and reports
440
450
  503 if it returns `false` or throws.
441
451
 
442
- Scripts:
452
+ Scripts (all wrap `drizzle-kit`):
443
453
 
444
- - `npm run db:migrate`: `prisma migrate dev` (dev-time schema changes + migration + generate)
445
- - `npm run db:generate`: `prisma generate` (regenerate client only)
446
- - `npm run db:studio`: `prisma studio` (GUI)
447
- - `predev` hook auto-runs `prisma generate` before `npm run dev`
448
- - `prestart` hook runs `prisma migrate deploy` before `npm start` (idempotent in prod)
454
+ - `npm run db:generate`: `webjs db generate` (schema -> SQL migration)
455
+ - `npm run db:migrate`: `webjs db migrate` (apply pending migrations)
456
+ - `npm run db:push`: `webjs db push` (push the schema straight to the dev DB)
457
+ - `npm run db:studio`: `webjs db studio` (visual DB browser)
458
+ - `npm run db:seed`: `webjs db seed` (run `db/seed.server.ts`)
459
+ - `webjs.start.before` runs `webjs db migrate` inside `webjs start` (idempotent; replaces the old `prestart` hook). No dev `before` step (no codegen).
449
460
 
450
- Always import the client from `lib/prisma.server.ts` (never `new PrismaClient()` directly -
451
- the singleton avoids opening a new connection on every dev-server reload):
461
+ Always import `db` from `db/connection.server.ts` (the globalThis-cached
462
+ singleton avoids opening a new connection on every dev-server reload), and
463
+ the tables from `db/schema.server.ts`:
452
464
 
453
465
  ```ts
454
- import { prisma } from '../../../lib/prisma.server.ts';
455
- const users = await prisma.user.findMany();
466
+ import { db } from '#db/connection.server.ts';
467
+ const users = await db.query.users.findMany();
456
468
  ```
457
469
 
458
- To switch to Postgres or MySQL: change `provider` in `prisma/schema.prisma`
459
- and the `DATABASE_URL` in `.env`.
470
+ To switch to Postgres: scaffold with `--db postgres`, or swap
471
+ `db/columns.server.ts` + `db/connection.server.ts` for the Postgres
472
+ variants and point `DATABASE_URL` at Postgres. The schema, queries, and
473
+ actions are unchanged.
460
474
 
461
475
  ## NPM packages (vendor pipeline)
462
476
 
@@ -521,9 +535,9 @@ committed manifest, optional `--download` for full offline capability,
521
535
  and a `--from` knob to swap the resolver CDN if jspm.io has an
522
536
  incident.
523
537
 
524
- **Don't auto-run `webjs vendor pin` in `predev` / `prestart`.** Auto-pin
525
- would silently churn the committed importmap.json as jspm.io resolves
526
- URLs or transitive deps drift. Pin is a deliberate developer action,
538
+ **Don't auto-run `webjs vendor pin` in a `webjs.dev.before` / `webjs.start.before`
539
+ step.** Auto-pin would silently churn the committed importmap.json as jspm.io
540
+ resolves URLs or transitive deps drift. Pin is a deliberate developer action,
527
541
  like `npm install` itself.
528
542
 
529
543
  **Do NOT modify the `.webjs/` lines in `.gitignore` / `.dockerignore`.**
@@ -740,11 +754,12 @@ legitimately use `static styles = css\`\`` for scoped CSS.
740
754
  ```ts
741
755
  // modules/posts/actions/create-post.server.ts
742
756
  'use server';
743
- import { prisma } from '../../../lib/prisma.server.ts';
757
+ import { db } from '#db/connection.server.ts';
758
+ import { posts } from '#db/schema.server.ts';
744
759
 
745
760
  export async function createPost(input: { title: string; body: string }) {
746
761
  if (!input.title) return { success: false, error: 'title required', status: 400 };
747
- const post = await prisma.post.create({ data: input });
762
+ const [post] = await db.insert(posts).values(input).returning();
748
763
  return { success: true, data: post };
749
764
  }
750
765
  ```
@@ -791,7 +806,7 @@ with a `route.ts` POST handler:
791
806
  ```ts
792
807
  // app/posts/route.ts
793
808
  import { redirect, html } from '@webjsdev/core';
794
- import { createPost } from '../../modules/posts/actions/create-post.server.ts';
809
+ import { createPost } from '#modules/posts/actions/create-post.server.ts';
795
810
 
796
811
  export async function POST(req: Request) {
797
812
  const form = await req.formData();
@@ -1117,13 +1132,14 @@ composition, so a nested shell ends up dropped by the HTML parser.
1117
1132
  1. Custom element tags must contain a hyphen. Pass the tag to `.register('tag-name')` at the bottom of the file. The tag is not a static field.
1118
1133
  2. **Server-only code goes in `.server.{js,ts}` files, `route.ts`
1119
1134
  handlers, or `middleware.ts`. Never in pages, layouts, or
1120
- components.** Direct imports of `@prisma/client`, `node:*`, or any
1121
- server-only dependency from a page, layout, loading.ts, error.ts,
1122
- not-found.ts, or component will crash the browser at module load.
1135
+ components.** Direct imports of a DB driver (`better-sqlite3` / `pg`),
1136
+ `node:*`, or any server-only dependency from a page, layout, loading.ts,
1137
+ error.ts, not-found.ts, or component will crash the browser at module load.
1123
1138
  Wrap the access in a `.server.{js,ts}` file; the framework
1124
- rewrites that import into an RPC stub for the browser. `lib/`
1125
- holds both server-only infra (`lib/prisma.server.ts`, `lib/session.server.ts`)
1126
- and browser-safe utilities (`lib/utils/cn.ts` with `cn`, design-
1139
+ rewrites that import into an RPC stub for the browser. Server-only
1140
+ infra lives in `db/*.server.ts` (the DB) and `lib/*.server.ts`
1141
+ (`lib/session.server.ts`); browser-safe utilities live in
1142
+ `lib/utils/cn.ts` with `cn`, design-
1127
1143
  system helpers). Server-only `lib/*` files must only be imported
1128
1144
  from `.server.ts`/`route.ts`/`middleware.ts`; browser-safe `lib/*`
1129
1145
  files (like `lib/utils/cn.ts`) can be imported anywhere.
@@ -32,18 +32,18 @@ enforced by `webjs check`; follow them by judgment.
32
32
 
33
33
  - **Server actions and queries live in `modules/<feature>/actions/` and
34
34
  `modules/<feature>/queries/`** (`*.server.{js,ts}`), not loose in the
35
- app root. Cross-cutting server infrastructure (the Prisma singleton,
36
- session helpers, auth config) lives in `lib/`.
35
+ app root. The DB connection lives in `db/connection.server.ts`; other
36
+ cross-cutting server infrastructure (session helpers, auth config) lives in `lib/`.
37
37
  - **One exported function per action/query file.** Name the file after
38
38
  the function (`create-post.server.ts` exports `createPost`). It keeps
39
39
  the action surface greppable.
40
40
  - **Every feature has tests.** A `modules/<feature>/` directory should
41
41
  have matching test files under `test/<feature>/`. A unit test for
42
42
  logic, a browser/e2e test for user-facing behaviour.
43
- - **Persist data with Prisma + SQLite, never JSON files.** The scaffold
44
- wires up `prisma/schema.prisma` and `lib/prisma.server.ts`. A
43
+ - **Persist data with Drizzle + SQLite, never JSON files.** The scaffold
44
+ wires up `db/schema.server.ts` and `db/connection.server.ts`. A
45
45
  `data/todos.json` or `db.json` used as a database resets on reload and
46
- cannot scale; define a Prisma model instead.
46
+ cannot scale; define a Drizzle table instead.
47
47
 
48
48
  ---
49
49
 
@@ -254,40 +254,45 @@ docs". That is the agent's default behavior in a webjs project.
254
254
 
255
255
  ---
256
256
 
257
- ## Data persistence: Prisma + SQLite, never JSON files
257
+ ## Data persistence: Drizzle + SQLite, never JSON files
258
258
 
259
259
  <!-- OVERRIDE -->
260
260
 
261
- Every webjs app uses **Prisma + SQLite** for persistence by default. The
262
- scaffold ships `prisma/schema.prisma`, `lib/prisma.server.ts` (singleton), the
263
- `predev` / `prestart` hooks that run `prisma generate` / `prisma migrate
264
- deploy`, and `npm run db:migrate` / `db:generate` / `db:studio` scripts.
261
+ Every webjs app uses **Drizzle + SQLite** for persistence by default. The
262
+ scaffold ships the `db/` folder (`schema.server.ts`, `columns.server.ts`,
263
+ `connection.server.ts`), the `webjs.start.before` step that runs
264
+ `webjs db migrate` inside `webjs start` (#550), and the
265
+ `npm run db:generate` / `db:migrate` / `db:push` / `db:studio` / `db:seed`
266
+ scripts (which route through `webjs db` to drizzle-kit). Drizzle has no
267
+ codegen, so there is no dev `before` step.
265
268
 
266
269
  **AI agents: these rules are absolute.**
267
270
 
268
271
  1. For ANY data the app stores (todos, posts, messages, products,
269
- comments, users…), define a Prisma model in `prisma/schema.prisma`
272
+ comments, users…), define a Drizzle table in `db/schema.server.ts`
270
273
  and persist there.
271
274
  2. **NEVER** create JSON files under `data/`, `db.json`, `posts.json`,
272
275
  `todos.json`, etc. as a fake database. It resets on reload and cannot
273
276
  scale; this is a project convention (see the conventions section above).
274
277
  3. **NEVER** use module-scope arrays or `Map`s as a "store". They
275
278
  reset on every dev-server reload and can't scale beyond one process.
276
- 4. **NEVER** use `localStorage` / `sessionStorage` to persist app data -
279
+ 4. **NEVER** use `localStorage` / `sessionStorage` to persist app data,
277
280
  it's per-browser and never reaches the server. Use it only for UI
278
281
  preferences (theme, sidebar collapsed, etc.).
279
- 5. To add a model: edit `prisma/schema.prisma`, then `npm run db:migrate
280
- -- --name <description>`. Access via `import { prisma } from
281
- '../../../lib/prisma.server.ts'` **only inside `.server.{js,ts}` files,
282
- `route.ts` handlers, or `middleware.ts`**. Never new `PrismaClient()`.
283
- Components, pages, and layouts call into the wrapped server query
284
- instead; the framework rewrites that import to an RPC stub on the
285
- browser side, so prisma source never reaches the client.
286
-
287
- To switch to Postgres or MySQL: change `provider` in
288
- `prisma/schema.prisma` and the `DATABASE_URL` in `.env`. Do this only
289
- if the user explicitly asks for it. SQLite is the right default for
290
- dev and small production workloads.
282
+ 5. To add a model: edit `db/schema.server.ts`, then `npm run db:generate`
283
+ and `npm run db:migrate`. Access via `import { db } from
284
+ '#db/connection.server.ts'` (and the tables from
285
+ `db/schema.server.ts`) **only inside `.server.{js,ts}` files,
286
+ `route.ts` handlers, or `middleware.ts`**. Components, pages, and
287
+ layouts call into the wrapped server query instead; the framework
288
+ rewrites that import to an RPC stub on the browser side, so the DB
289
+ driver never reaches the client.
290
+
291
+ To switch to Postgres: scaffold with `--db postgres`, or swap
292
+ `db/columns.server.ts` + `db/connection.server.ts` for the Postgres
293
+ variants and point `DATABASE_URL` at Postgres. The schema, queries, and
294
+ actions are unchanged. SQLite is the right default for dev and small
295
+ production workloads.
291
296
 
292
297
  ---
293
298
 
@@ -302,9 +307,9 @@ saas templates) is a **starting point**.
302
307
 
303
308
  When the user asks the agent to build their actual app:
304
309
 
305
- 1. **Replace the example `User` model** in `prisma/schema.prisma` with
306
- the real domain models the app needs (e.g. `Todo`, `Post`, `Message`)
307
- - unless the app actually has users.
310
+ 1. **Replace the example `User` model** in `db/schema.server.ts` with
311
+ the real domain models the app needs (e.g. `Todo`, `Post`, `Message`),
312
+ unless the app actually has users.
308
313
  2. **Replace `app/page.ts`** with the app's real homepage. Don't ship
309
314
  "Hello from …" as the deliverable.
310
315
  3. **Delete or replace `components/theme-toggle.ts`** if the app doesn't
@@ -319,10 +324,11 @@ When the user asks the agent to build their actual app:
319
324
  dashboard, or board, or a wide layout overflows into an unnecessary
320
325
  horizontal scrollbar. Keep the design tokens and theme setup, those
321
326
  are infrastructure.
322
- 6. **Keep:** the Prisma setup, the test config, the agent config files
327
+ 6. **Keep:** the Drizzle setup, the test config, the agent config files
323
328
  (`AGENTS.md`, `CONVENTIONS.md`, `CLAUDE.md`, `.cursorrules`, etc.),
324
- `lib/prisma.server.ts`, the directory conventions, the design tokens in
325
- `app/layout.ts`. These are the infrastructure, not the example app.
329
+ `db/connection.server.ts` + `db/columns.server.ts`, the directory
330
+ conventions, the design tokens in `app/layout.ts`. These are the
331
+ infrastructure, not the example app.
326
332
 
327
333
  This is enforced, not just advised. The example `app/page.ts` and
328
334
  `app/layout.ts` carry a `webjs-scaffold-placeholder` marker comment, and
@@ -334,7 +340,7 @@ line. So the delivered app contains only what the user asked for, never
334
340
  leftover scaffold code.
335
341
 
336
342
  The scaffold exists so the agent doesn't reinvent the directory layout,
337
- the Prisma wiring, the test runner config, or the convention files. It
343
+ the Drizzle wiring, the test runner config, or the convention files. It
338
344
  does NOT exist so the agent ships the example homepage.
339
345
 
340
346
  ---
@@ -382,10 +388,11 @@ modules/
382
388
  ```
383
389
 
384
390
  **Rules:**
391
+ - **Prefer the `#` root alias over deep relatives.** Write `import { db } from '#db/connection.server.ts'`, `import { Button } from '#components/ui/button.ts'`, `#lib/...`, `#modules/...` instead of `../../../`. It is native `package.json "imports"` (the single `"#*": "./*"` key covers every top-level folder, so a new folder needs no config), resolved by Node and Bun with no build step. There is no slash after the `#` (`#lib/...`, not `#/lib/...`). A same-directory import stays relative (`./sibling.ts`).
385
392
  - One exported function per server action/query file
386
393
  - Server actions need BOTH the `.server.{js,ts}` extension AND a `'use server'` directive at the top. Extension alone marks a server-only utility (source-protected, not RPC-callable). Directive alone is a lint violation (`use-server-needs-extension`).
387
394
  - Components must call `Class.register('tag')`
388
- - **Server-only code goes in `.server.{js,ts}` files, `route.ts` handlers, or `middleware.ts`. Never in pages, layouts, or components.** Direct imports of `@prisma/client` or `node:*` from pages, layouts, or components crash the browser at module load. Wrap in a `.server.{js,ts}` file; the framework rewrites that import to an RPC stub on the browser side. `lib/` holds both server-only infra (`lib/prisma.server.ts`) and browser-safe utilities (`lib/utils/cn.ts` with `cn`); the convention is "if a `lib/` file needs Node APIs, only import it from server-only files."
395
+ - **Server-only code goes in `.server.{js,ts}` files, `route.ts` handlers, or `middleware.ts`. Never in pages, layouts, or components.** Direct imports of a DB driver (`better-sqlite3` / `pg`) or `node:*` from pages, layouts, or components crash the browser at module load. Wrap in a `.server.{js,ts}` file; the framework rewrites that import to an RPC stub on the browser side. The DB lives in `db/*.server.ts`; `lib/` holds other server-only infra and browser-safe utilities (`lib/utils/cn.ts` with `cn`); the convention is "if a `lib/` file needs Node APIs, only import it from server-only files."
389
396
  - Routes (`app/**/page.ts`, `app/**/route.ts`) must be thin: import logic from modules
390
397
  - **Fetch server data in the component that needs it, with an `async render()`, not by prop-drilling.** A leaf component can write `const u = await getUser(this.uid)` directly in `render()`; SSR awaits it so the data is in the first paint, and the client uses stale-while-revalidate on a re-fetch. Reach for `renderFallback()` only to show a re-fetch loading state, and `Task` / signals only for genuinely client-only data (a `Task` shows its pending state at SSR, losing first-paint data). Do not put `await getData()` in a page / layout when a leaf component can own it (page fetches run sequentially, a route-level waterfall).
391
398
 
@@ -582,8 +589,8 @@ the module once (typically in `app/layout.ts`) and use the tag.
582
589
  ```ts
583
590
  // Tier 1: class helpers on native elements (use this for forms,
584
591
  // dashboards, cards, layouts, anywhere the value is purely visual)
585
- import { buttonClass } from '../components/ui/button.ts';
586
- import { inputClass } from '../components/ui/input.ts';
592
+ import { buttonClass } from '#components/ui/button.ts';
593
+ import { inputClass } from '#components/ui/input.ts';
587
594
  return html`
588
595
  <button class=${buttonClass({ size: 'lg' })}>Save</button>
589
596
  <input class=${inputClass()} placeholder="Email">
@@ -767,7 +774,7 @@ Consume:
767
774
 
768
775
  ```ts
769
776
  // app/page.ts
770
- import { rubric } from '../lib/utils/ui.ts';
777
+ import { rubric } from '#lib/utils/ui.ts';
771
778
 
772
779
  export default function Home() {
773
780
  return html`
@@ -919,15 +926,18 @@ route imports and calls it.
919
926
  ```ts
920
927
  // modules/posts/actions/create-post.server.ts
921
928
  'use server';
929
+ import { db } from '#db/connection.server.ts';
930
+ import { posts } from '#db/schema.server.ts';
922
931
  export async function createPost({ title, body }) {
923
- return prisma.post.create({ data: { title, body } });
932
+ const [post] = await db.insert(posts).values({ title, body }).returning();
933
+ return post;
924
934
  }
925
935
  ```
926
936
 
927
937
  ```ts
928
938
  // app/api/posts/route.ts
929
939
  import { route } from '@webjsdev/server';
930
- import { createPost } from '../../../modules/posts/actions/create-post.server.ts';
940
+ import { createPost } from '#modules/posts/actions/create-post.server.ts';
931
941
  // The route() adapter merges query + route params + JSON body into one input
932
942
  // object and JSON-responds the result. Pass { validate } to guard the input.
933
943
  export const POST = route(createPost);
@@ -1030,7 +1040,8 @@ Where the data lives, where to read it:
1030
1040
  ```ts
1031
1041
  // modules/posts/actions/create-post.server.ts
1032
1042
  'use server';
1033
- import { prisma } from '../../../lib/prisma.server.ts';
1043
+ import { db } from '#db/connection.server.ts';
1044
+ import { posts } from '#db/schema.server.ts';
1034
1045
  import type { ActionResult } from '../types.ts';
1035
1046
 
1036
1047
  export async function createPost(input: {
@@ -1148,7 +1159,7 @@ Create new projects with `webjs create`:
1148
1159
  ```sh
1149
1160
  webjs create <name> # full-stack (default)
1150
1161
  webjs create <name> --template api # backend-only API
1151
- webjs create <name> --template saas # auth + dashboard + Prisma User model
1162
+ webjs create <name> --template saas # auth + dashboard + Drizzle User model
1152
1163
  ```
1153
1164
 
1154
1165
  **Route-wrapping pattern (especially for `--template api` apps):**
@@ -1157,8 +1168,8 @@ Routes are thin wrappers over typed server actions. Business logic lives in
1157
1168
 
1158
1169
  ```ts
1159
1170
  // app/api/users/route.ts: thin wrapper
1160
- import { listUsers } from '../../../modules/users/queries/list-users.server.ts';
1161
- import { createUser } from '../../../modules/users/actions/create-user.server.ts';
1171
+ import { listUsers } from '#modules/users/queries/list-users.server.ts';
1172
+ import { createUser } from '#modules/users/actions/create-user.server.ts';
1162
1173
 
1163
1174
  export async function GET() { return Response.json(await listUsers()); }
1164
1175
  export async function POST(req: Request) {
@@ -21,8 +21,9 @@
21
21
  # framework AGENTS.md "Secure response headers" section.
22
22
  FROM node:24-alpine
23
23
 
24
- # openssl + ca-certificates are required by Prisma's query engine at runtime.
25
- RUN apk add --no-cache openssl ca-certificates
24
+ # ca-certificates for outbound TLS (e.g. a managed Postgres). better-sqlite3
25
+ # is a prebuilt native module, so no build toolchain is needed here.
26
+ RUN apk add --no-cache ca-certificates
26
27
 
27
28
  WORKDIR /app
28
29
 
@@ -35,9 +36,9 @@ RUN npm install --no-audit --no-fund
35
36
  # App source. node_modules and local state are excluded via .dockerignore.
36
37
  COPY . .
37
38
 
38
- # Generate the Prisma client at build time (every scaffold ships a
39
- # prisma/schema.prisma). If you remove Prisma from the app, delete this line.
40
- RUN npx prisma generate
39
+ # Drizzle has no client-codegen step, so there is nothing to build here. The
40
+ # database is migrated at boot via `webjs start` (the `webjs.start.before`
41
+ # step runs `webjs db migrate`). See the CMD note below.
41
42
 
42
43
  ENV NODE_ENV=production
43
44
  # webjs start reads $PORT (default 8080). compose / uncloud / Railway set it.
@@ -55,6 +56,8 @@ EXPOSE 8080
55
56
  HEALTHCHECK --interval=15s --timeout=3s --start-period=40s --retries=5 \
56
57
  CMD ["node", "-e", "fetch('http://127.0.0.1:'+(process.env.PORT||8080)+'/__webjs/ready').then(r=>process.exit(r.ok?0:1),()=>process.exit(1))"]
57
58
 
58
- # `npm start` runs `prestart: prisma migrate deploy` (idempotent, a no-op when
59
- # there are no migrations yet) and then `webjs start`, which serves on $PORT.
59
+ # `npm start` is a thin alias for `webjs start` (#550). `webjs start` runs the
60
+ # `webjs.start.before` step (`webjs db migrate`, idempotent / a no-op with no
61
+ # pending migrations) IN-PROCESS, then serves on $PORT. `CMD ["webjs", "start"]`
62
+ # is now equivalent: the migrate no longer depends on an npm `prestart` hook.
60
63
  CMD ["npm", "start"]
@@ -12,9 +12,9 @@ services:
12
12
  - "8080:8080"
13
13
  environment:
14
14
  PORT: 8080
15
- # SQLite on a volume for local dev. For production, point DATABASE_URL at
16
- # your managed Postgres and switch prisma/schema.prisma's provider to
17
- # "postgresql".
15
+ # SQLite on a volume for local dev. For production, scaffold with
16
+ # --db postgres (or swap db/columns.server.ts + db/connection.server.ts
17
+ # for the pg variant) and point DATABASE_URL at your managed Postgres.
18
18
  DATABASE_URL: file:/data/dev.db
19
19
  # Generate: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
20
20
  AUTH_SECRET: ${AUTH_SECRET:-change-me-please-at-least-32-characters!}
@@ -16,7 +16,7 @@ test('html template renders correctly', async () => {
16
16
 
17
17
  test('example: your first server action test', async () => {
18
18
  // Import your server action:
19
- // import { createPost } from '../../modules/posts/actions/create-post.server.ts';
19
+ // import { createPost } from '#modules/posts/actions/create-post.server.ts';
20
20
  //
21
21
  // const result = await createPost({ title: 'Test', body: 'Content' });
22
22
  // assert.equal(result.success, true);