bunderstack 0.23.4 → 0.24.0

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.
Files changed (137) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/README.md +6 -4
  3. package/dist/api/builder.d.ts +13 -11
  4. package/dist/api/builder.d.ts.map +1 -1
  5. package/dist/api/builder.js.map +1 -1
  6. package/dist/api/context.d.ts +6 -6
  7. package/dist/api/context.d.ts.map +1 -1
  8. package/dist/api/context.js +1 -1
  9. package/dist/api/context.js.map +1 -1
  10. package/dist/api/crud-router.d.ts +6 -6
  11. package/dist/api/realtime-router.d.ts +1 -1
  12. package/dist/api/storage-router.d.ts +5 -5
  13. package/dist/backend-internals.d.ts +5 -13
  14. package/dist/backend-internals.d.ts.map +1 -1
  15. package/dist/backend-internals.js.map +1 -1
  16. package/dist/backend.d.ts +39 -5
  17. package/dist/backend.d.ts.map +1 -1
  18. package/dist/backend.js +37 -46
  19. package/dist/backend.js.map +1 -1
  20. package/dist/blueprint-generator.d.ts +1 -0
  21. package/dist/blueprint-generator.d.ts.map +1 -1
  22. package/dist/blueprint-generator.js +29 -1
  23. package/dist/blueprint-generator.js.map +1 -1
  24. package/dist/blueprint.d.ts +2 -1
  25. package/dist/blueprint.d.ts.map +1 -1
  26. package/dist/blueprint.js +9 -2
  27. package/dist/blueprint.js.map +1 -1
  28. package/dist/cli.d.ts.map +1 -1
  29. package/dist/cli.js +14 -2
  30. package/dist/cli.js.map +1 -1
  31. package/dist/config.d.ts +13 -19
  32. package/dist/config.d.ts.map +1 -1
  33. package/dist/config.js +1 -1
  34. package/dist/config.js.map +1 -1
  35. package/dist/email/smtp.d.ts +7 -1
  36. package/dist/email/smtp.d.ts.map +1 -1
  37. package/dist/email/smtp.js +5 -1
  38. package/dist/email/smtp.js.map +1 -1
  39. package/dist/email.d.ts +3 -29
  40. package/dist/email.d.ts.map +1 -1
  41. package/dist/email.js +1 -194
  42. package/dist/email.js.map +1 -1
  43. package/dist/env-probe.d.ts +9 -0
  44. package/dist/env-probe.d.ts.map +1 -0
  45. package/dist/env-probe.js +73 -0
  46. package/dist/env-probe.js.map +1 -0
  47. package/dist/env.d.ts +2 -5
  48. package/dist/env.d.ts.map +1 -1
  49. package/dist/env.js +2 -9
  50. package/dist/env.js.map +1 -1
  51. package/dist/hosted-contract.d.ts +11 -0
  52. package/dist/hosted-contract.d.ts.map +1 -0
  53. package/dist/hosted-contract.js +46 -0
  54. package/dist/hosted-contract.js.map +1 -0
  55. package/dist/index.d.ts +3 -2
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +1 -1
  58. package/dist/index.js.map +1 -1
  59. package/dist/inspect.d.ts +15 -0
  60. package/dist/inspect.d.ts.map +1 -0
  61. package/dist/inspect.js +43 -0
  62. package/dist/inspect.js.map +1 -0
  63. package/dist/internal-tables-pg.d.ts +43 -94
  64. package/dist/internal-tables-pg.d.ts.map +1 -1
  65. package/dist/internal-tables-pg.js +14 -17
  66. package/dist/internal-tables-pg.js.map +1 -1
  67. package/dist/internal-tables.d.ts +213 -488
  68. package/dist/internal-tables.d.ts.map +1 -1
  69. package/dist/internal-tables.js +33 -33
  70. package/dist/internal-tables.js.map +1 -1
  71. package/dist/jobs/define.d.ts +20 -20
  72. package/dist/jobs/define.d.ts.map +1 -1
  73. package/dist/jobs/define.js.map +1 -1
  74. package/dist/manifest-diff.d.ts +6 -0
  75. package/dist/manifest-diff.d.ts.map +1 -0
  76. package/dist/manifest-diff.js +42 -0
  77. package/dist/manifest-diff.js.map +1 -0
  78. package/dist/manifest.d.ts +10 -2
  79. package/dist/manifest.d.ts.map +1 -1
  80. package/dist/manifest.js +27 -29
  81. package/dist/manifest.js.map +1 -1
  82. package/dist/messaging/email.d.ts +16 -0
  83. package/dist/messaging/email.d.ts.map +1 -0
  84. package/dist/messaging/email.js +8 -0
  85. package/dist/messaging/email.js.map +1 -0
  86. package/dist/messaging/index.d.ts +8 -0
  87. package/dist/messaging/index.d.ts.map +1 -0
  88. package/dist/messaging/index.js +4 -0
  89. package/dist/messaging/index.js.map +1 -0
  90. package/dist/messaging/journal.d.ts +4 -0
  91. package/dist/messaging/journal.d.ts.map +1 -0
  92. package/dist/messaging/journal.js +19 -0
  93. package/dist/messaging/journal.js.map +1 -0
  94. package/dist/messaging/runtime.d.ts +16 -0
  95. package/dist/messaging/runtime.d.ts.map +1 -0
  96. package/dist/messaging/runtime.js +213 -0
  97. package/dist/messaging/runtime.js.map +1 -0
  98. package/dist/messaging/standalone.d.ts +27 -0
  99. package/dist/messaging/standalone.d.ts.map +1 -0
  100. package/dist/messaging/standalone.js +18 -0
  101. package/dist/messaging/standalone.js.map +1 -0
  102. package/dist/messaging/telegram.d.ts +16 -0
  103. package/dist/messaging/telegram.d.ts.map +1 -0
  104. package/dist/messaging/telegram.js +5 -0
  105. package/dist/messaging/telegram.js.map +1 -0
  106. package/dist/messaging/types.d.ts +22 -0
  107. package/dist/messaging/types.d.ts.map +1 -0
  108. package/dist/messaging/types.js +17 -0
  109. package/dist/messaging/types.js.map +1 -0
  110. package/dist/runtime.d.ts +11 -10
  111. package/dist/runtime.d.ts.map +1 -1
  112. package/dist/runtime.js +19 -31
  113. package/dist/runtime.js.map +1 -1
  114. package/dist/schema-export-pg.d.ts +1 -1
  115. package/dist/schema-export-pg.d.ts.map +1 -1
  116. package/dist/schema-export-pg.js +1 -1
  117. package/dist/schema-export-pg.js.map +1 -1
  118. package/dist/schema-export.d.ts +1 -1
  119. package/dist/schema-export.d.ts.map +1 -1
  120. package/dist/schema-export.js +1 -1
  121. package/dist/schema-export.js.map +1 -1
  122. package/dist/testing/fixture.d.ts +2 -2
  123. package/dist/testing/fixture.d.ts.map +1 -1
  124. package/dist/testing/fixture.js +11 -9
  125. package/dist/testing/fixture.js.map +1 -1
  126. package/dist/testing/messaging.d.ts +21 -0
  127. package/dist/testing/messaging.d.ts.map +1 -0
  128. package/dist/testing/messaging.js +34 -0
  129. package/dist/testing/messaging.js.map +1 -0
  130. package/dist/testing.d.ts +1 -0
  131. package/dist/testing.d.ts.map +1 -1
  132. package/dist/testing.js.map +1 -1
  133. package/package.json +6 -2
  134. package/skills/creating-bunderstack-apps/references/application-structure.md +16 -7
  135. package/skills/migrating-to-bunderstack/SKILL.md +72 -50
  136. package/skills/migrating-to-bunderstack/references/audit-checklist.md +2 -2
  137. package/skills/migrating-to-bunderstack/references/runtime-replacements.md +16 -12
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "bunderstack",
3
- "version": "0.23.4",
4
- "description": "Batteries-included backend framework for Bun: type-safe oRPC APIs, auth, storage, realtime, jobs, email, and validated env from one config.",
3
+ "version": "0.24.0",
4
+ "description": "Batteries-included backend framework for Bun: type-safe oRPC APIs, auth, storage, realtime, jobs, messaging, and validated env from one declaration.",
5
5
  "keywords": [
6
6
  "backend",
7
7
  "better-auth",
@@ -113,6 +113,10 @@
113
113
  "types": "./dist/email/smtp.d.ts",
114
114
  "default": "./dist/email/smtp.js"
115
115
  },
116
+ "./messaging": {
117
+ "types": "./dist/messaging/index.d.ts",
118
+ "default": "./dist/messaging/index.js"
119
+ },
116
120
  "./api": {
117
121
  "types": "./dist/api/types.d.ts",
118
122
  "default": "./dist/api/types.js"
@@ -3,9 +3,12 @@
3
3
  ## Separate declaration from runtime
4
4
 
5
5
  `src/bunderstack/backend.ts` synchronously constructs and exports `backend =
6
- bunderstack({...})`. It is pure: it validates the declaration and exposes
7
- `backend.manifest`, but it does not connect to infrastructure. The blueprint
8
- imports this declaration without starting the application.
6
+ bunderstack({...})`. The declaration is one object; `database`, `storage`,
7
+ `messaging`, and `realtime` also accept a function of the validated
8
+ environment, and `auth` accepts a builder over `{ db, env }`. It is pure:
9
+ `backend.inspect({ env })` resolves it and returns a manifest without
10
+ connecting to infrastructure, and the blueprint imports this declaration
11
+ without starting the application.
9
12
 
10
13
  `src/bunderstack/index.ts` owns the production runtime: it imports `backend`,
11
14
  calls `await backend.start()`, exports `app`, and calls `provision(app)` when the
@@ -122,7 +125,12 @@ const instrumentation = o.middleware(async ({ context, next, path }) => {
122
125
  }
123
126
  })
124
127
 
125
- bunderstack({ schema, database, middleware: [instrumentation], api })
128
+ bunderstack({
129
+ schema,
130
+ database,
131
+ middleware: [instrumentation],
132
+ api,
133
+ })
126
134
  ```
127
135
 
128
136
  Three rules apply to a graph-wide middleware. It runs before authentication, so
@@ -186,9 +194,10 @@ Commit `.env.example` with names and safe placeholders only. Keep production
186
194
  secrets, database URLs, storage credentials, and auth secrets in the runtime
187
195
  environment.
188
196
 
189
- Use local libSQL storage and console email for development when appropriate;
190
- declare production adapters and credentials through configuration and runtime
191
- environment rather than hard-coding them.
197
+ Use local libSQL storage for development, and let a messaging channel without
198
+ credentials capture instead of sending; declare production adapters and
199
+ credentials through the declaration and the runtime environment rather than
200
+ hard-coding them.
192
201
 
193
202
  ## Publish direct writes
194
203
 
@@ -8,6 +8,7 @@ description: Use when building, structuring, or migrating an application on Bund
8
8
  ## Overview
9
9
 
10
10
  Bunderstack is a batteries-included full-stack backend framework for Bun unifying:
11
+
11
12
  - **Drizzle ORM** (libSQL / SQLite / Postgres)
12
13
  - **Better Auth** (authentication & session management)
13
14
  - **oRPC v2** (unified type-safe RPC & OpenAPI/REST procedures with Standard Schema / Valibot)
@@ -31,11 +32,13 @@ import { schema } from './schema'
31
32
  import { access } from './access'
32
33
  import { api } from './api'
33
34
 
34
- // 1. Pure synchronous declaration (does NO I/O, no DB connection, exports static manifest)
35
+ // 1. Pure synchronous declaration (does NO I/O, no DB connection)
36
+ // One object. `database`, `storage`, `messaging`, and `realtime` also take
37
+ // a function of the validated env when they need a key or a URL.
35
38
  export const backend = bunderstack({
36
39
  schema,
37
40
  access,
38
- database: { adapter: libsql(), url: process.env.DATABASE_URL ?? 'file:./data.db' },
41
+ database: { adapter: libsql() }, // url defaults to DATABASE_URL
39
42
  api,
40
43
  })
41
44
 
@@ -44,7 +47,7 @@ export const app = await backend.start()
44
47
  export type App = typeof app
45
48
  ```
46
49
 
47
- - `backend.manifest` can be read statically by tools (such as blueprint generators) without starting the app or connecting to a database.
50
+ - `backend.inspect({ env })` resolves the declaration and returns its manifest for tools (such as blueprint generators) without starting the app or connecting to a database.
48
51
  - `app = await backend.start()` explicitly boots the runtime.
49
52
  - `backend.test()` creates an isolated, lexically owned test fixture.
50
53
 
@@ -52,25 +55,25 @@ export type App = typeof app
52
55
 
53
56
  All Bunderstack capabilities are imported directly from single-segment subpaths of `bunderstack`:
54
57
 
55
- | Subpath Import | Purpose |
56
- | --- | --- |
57
- | `bunderstack` | Core backend builder (`bunderstack`, `defineApi`, `defineAccess`, `BunderstackError`) |
58
- | `bunderstack/libsql` | libSQL / SQLite database adapter |
59
- | `bunderstack/postgres-js` | postgres.js database adapter |
60
- | `bunderstack/bun-sql` | `Bun.sql` Postgres adapter |
61
- | `bunderstack/pglite` | PGlite in-memory / embedded Postgres adapter |
62
- | `bunderstack/client` | Framework-neutral typed client & `createLiveView` |
63
- | `bunderstack/client-react` | React LiveView hook (`useLiveView`) |
64
- | `bunderstack/client-rest` | Type-safe REST client |
65
- | `bunderstack/query` | TanStack Query integration (`createClient`, `syncRealtime`) |
66
- | `bunderstack/query-react` | React-specific query helpers |
67
- | `bunderstack/sync` | TanStack DB realtime sync collections |
68
- | `bunderstack/start` | TanStack Start integration (`createApiHandlers`) |
69
- | `bunderstack/start-auth` | Better Auth client for TanStack Start |
70
- | `bunderstack/provision` | Database schema provisioning (`provision(app)`) |
71
- | `bunderstack/testing` | Test fixture helpers |
72
- | `bunderstack/schema` | Internal system tables (`export * from 'bunderstack/schema'`) |
73
- | `bunderstack/typeid` | TypeID column types & generators |
58
+ | Subpath Import | Purpose |
59
+ | -------------------------- | ------------------------------------------------------------------------------------- |
60
+ | `bunderstack` | Core backend builder (`bunderstack`, `defineApi`, `defineAccess`, `BunderstackError`) |
61
+ | `bunderstack/libsql` | libSQL / SQLite database adapter |
62
+ | `bunderstack/postgres-js` | postgres.js database adapter |
63
+ | `bunderstack/bun-sql` | `Bun.sql` Postgres adapter |
64
+ | `bunderstack/pglite` | PGlite in-memory / embedded Postgres adapter |
65
+ | `bunderstack/client` | Framework-neutral typed client & `createLiveView` |
66
+ | `bunderstack/client-react` | React LiveView hook (`useLiveView`) |
67
+ | `bunderstack/client-rest` | Type-safe REST client |
68
+ | `bunderstack/query` | TanStack Query integration (`createClient`, `syncRealtime`) |
69
+ | `bunderstack/query-react` | React-specific query helpers |
70
+ | `bunderstack/sync` | TanStack DB realtime sync collections |
71
+ | `bunderstack/start` | TanStack Start integration (`createApiHandlers`) |
72
+ | `bunderstack/start-auth` | Better Auth client for TanStack Start |
73
+ | `bunderstack/provision` | Database schema provisioning (`provision(app)`) |
74
+ | `bunderstack/testing` | Test fixture helpers |
75
+ | `bunderstack/schema` | Internal system tables (`export * from 'bunderstack/schema'`) |
76
+ | `bunderstack/typeid` | TypeID column types & generators |
74
77
 
75
78
  ---
76
79
 
@@ -79,6 +82,7 @@ All Bunderstack capabilities are imported directly from single-segment subpaths
79
82
  ### Scale Decision: Flat vs. Modular
80
83
 
81
84
  1. **Flat Layout (MVP / Small Service: < 5 tables, < 5 procedures, 1 job):**
85
+
82
86
  ```
83
87
  src/
84
88
  ├── bunderstack.ts # backend declaration & app start
@@ -159,14 +163,16 @@ export const protectedProcedure = o.protected.use(async ({ context, next }) => {
159
163
  return next()
160
164
  })
161
165
 
162
- export const adminProcedure = protectedProcedure.use(async ({ context, next, errors }) => {
163
- if (context.user.role !== 'admin') {
164
- throw errors.FORBIDDEN({ message: 'Admin privileges required' })
165
- }
166
- return next()
167
- })
166
+ export const adminProcedure = protectedProcedure.use(
167
+ async ({ context, next, errors }) => {
168
+ if (context.user.role !== 'admin') {
169
+ throw errors.FORBIDDEN({ message: 'Admin privileges required' })
170
+ }
171
+ return next()
172
+ },
173
+ )
168
174
 
169
- // Graph-wide observability middleware (registered in bunderstack({ middleware: [instrumentation] }))
175
+ // Graph-wide observability middleware (registered as `middleware: [instrumentation]`)
170
176
  export const instrumentation = o.middleware(async ({ context, next, path }) => {
171
177
  const startedAt = performance.now()
172
178
  try {
@@ -176,7 +182,9 @@ export const instrumentation = o.middleware(async ({ context, next, path }) => {
176
182
  const duration = Math.round(performance.now() - startedAt)
177
183
  // context.peekSession() reads resolved session without triggering forced auth on public/webhooks
178
184
  const userId = context.peekSession()?.user?.id
179
- console.log(`[oRPC] ${path.join('.')} - ${duration}ms - User: ${userId ?? 'anon'}`)
185
+ console.log(
186
+ `[oRPC] ${path.join('.')} - ${duration}ms - User: ${userId ?? 'anon'}`,
187
+ )
180
188
  }
181
189
  })
182
190
  ```
@@ -184,6 +192,7 @@ export const instrumentation = o.middleware(async ({ context, next, path }) => {
184
192
  ### Rule: Circular Boot-Time Import Prevention
185
193
 
186
194
  `src/bunderstack/auth.ts` and `api/base.ts` must **NEVER** import `app` or `src/bunderstack/index.ts` at module top-level.
195
+
187
196
  - In `auth.ts`: Read `process.env` directly for secret keys. If an async hook (like email sending) needs the initialized app, use dynamic `import('./index')` inside the callback.
188
197
  - In `api/*.ts`: Consume `context.db`, `context.env`, `context.jobs`, `context.storage`, `context.auth` from handler parameters.
189
198
 
@@ -235,15 +244,16 @@ export const access = defineAccess(schema, {
235
244
  await client.posts.update.call({ id: 'post_1', title: 'Updated Title' })
236
245
  ```
237
246
  - **Custom List Procedures**: Use `listSpec` to give custom endpoints the same pagination and filtering behavior:
247
+
238
248
  ```ts
239
249
  import { listSpec } from 'bunderstack'
240
-
250
+
241
251
  const logSpec = listSpec(schema.auditLogs, {
242
252
  filterable: ['level', 'userId'],
243
253
  sortable: ['createdAt'],
244
254
  defaultSort: { column: 'createdAt', order: 'desc' },
245
255
  })
246
-
256
+
247
257
  export const logsProcedure = adminProcedure
248
258
  .input(logSpec.input)
249
259
  .handler(logSpec.handler)
@@ -251,7 +261,7 @@ export const access = defineAccess(schema, {
251
261
 
252
262
  ### 3.2 Authentication (`authConfig`)
253
263
 
254
- Export a clean Better Auth config and pass it into `bunderstack({ auth: authConfig })`:
264
+ Export a clean Better Auth config and pass it as `auth: authConfig`; use the `({ db, env })` builder form when a hook needs the database or an env value:
255
265
 
256
266
  ```ts
257
267
  // src/bunderstack/auth.ts
@@ -277,12 +287,15 @@ import * as v from 'valibot'
277
287
  export const defineJobs = (jobs) =>
278
288
  jobs.define({
279
289
  sendWelcomeEmail: jobs.job({
280
- input: v.object({ userId: v.string(), email: v.pipe(v.string(), v.email()) }),
290
+ input: v.object({
291
+ userId: v.string(),
292
+ email: v.pipe(v.string(), v.email()),
293
+ }),
281
294
  concurrency: 5,
282
295
  timeout: 30_000,
283
296
  retries: 3,
284
297
  handler: async ({ userId, email }, ctx) => {
285
- await ctx.email.send({
298
+ await ctx.messaging.email.send({
286
299
  to: email,
287
300
  subject: 'Welcome!',
288
301
  html: '<h1>Welcome to our service</h1>',
@@ -360,6 +373,7 @@ Raise typed errors in procedures using `errors`:
360
373
  ```
361
374
 
362
375
  Outside procedures (e.g. in background jobs or domain services):
376
+
363
377
  ```ts
364
378
  import { BunderstackError } from 'bunderstack'
365
379
 
@@ -384,6 +398,7 @@ throw new BunderstackError('FORBIDDEN', 'Quota exceeded')
384
398
 
385
399
  > [!CAUTION]
386
400
  > **ALL MIGRATIONS MUST BE GENERATED EXCLUSIVELY VIA DRIZZLE-KIT CLI.**
401
+ >
387
402
  > - Always run: `bunx drizzle-kit generate` (or `bun run db:generate`).
388
403
  > - **NEVER** hand-edit generated migration SQL files.
389
404
  > - **NEVER** let an LLM agent write or modify `.sql` files in `migrations/`.
@@ -410,7 +425,7 @@ export const Route = createFileRoute('/api/$')({
410
425
  })
411
426
  ```
412
427
 
413
- *Note: Remove any separate `/api/auth/$`, `/api/trpc/$`, or `/api/cron/*` route files.*
428
+ _Note: Remove any separate `/api/auth/$`, `/api/trpc/$`, or `/api/cron/_` route files.\*
414
429
 
415
430
  ### Dedicated Production Worker (`src/worker.ts`)
416
431
 
@@ -429,6 +444,7 @@ await app.runWorker()
429
444
  ```
430
445
 
431
446
  Add worker script in `package.json`:
447
+
432
448
  ```json
433
449
  {
434
450
  "scripts": {
@@ -470,14 +486,17 @@ test('creates and retrieves a post', async () => {
470
486
  // Typed in-process oRPC client
471
487
  const client = t.client(identity)
472
488
 
473
- const created = await client.posts.create({ title: 'New Post', content: 'Hello' })
489
+ const created = await client.posts.create({
490
+ title: 'New Post',
491
+ content: 'Hello',
492
+ })
474
493
  expect(created.title).toBe('New Post')
475
494
 
476
495
  // Run all queued background jobs deterministically
477
496
  await t.jobs.runUntilIdle()
478
497
 
479
498
  // Inspect sent emails
480
- expect(t.email.sent).toHaveLength(0)
499
+ expect(t.messaging.email.sent).toHaveLength(0)
481
500
  })
482
501
  ```
483
502
 
@@ -515,6 +534,7 @@ Bunderhost monitors application deployment status via `GET /api/readiness`, whic
515
534
  ### Official Bunderstack Documentation for LLMs
516
535
 
517
536
  When working on Bunderstack projects, consult the dedicated LLM references:
537
+
518
538
  - **Web Documentation**: [https://bunderstack.kcrz.dev/docs](https://bunderstack.kcrz.dev/docs)
519
539
  - **Compact LLM Context (`llms.txt`)**: [https://bunderstack.kcrz.dev/docs/llms.txt](https://bunderstack.kcrz.dev/docs/llms.txt) (or local `node_modules/bunderstack/llms.txt`)
520
540
  - **Complete LLM Knowledge Base (`llms-full.txt`)**: [https://bunderstack.kcrz.dev/docs/llms-full.txt](https://bunderstack.kcrz.dev/docs/llms-full.txt)
@@ -524,10 +544,12 @@ When working on Bunderstack projects, consult the dedicated LLM references:
524
544
  Bunderhost provides a Model Context Protocol (MCP) server that allows coding agents to inspect, manage, and deploy projects.
525
545
 
526
546
  #### Connecting to Bunderhost MCP:
547
+
527
548
  1. Generate an Agent Access Token in Bunderhost: **Organization → Agent Access → Issue Token**.
528
549
  2. Connect your MCP client to `https://<bunderhost-host>/mcp` using the token as a `Bearer` credential.
529
550
 
530
551
  #### Key MCP Tools:
552
+
531
553
  - `list_projects`: List all projects in the organization.
532
554
  - `get_project`: Retrieve project configuration, active deployments, and blueprint status.
533
555
  - `get_project_readiness`: Check database reachability, migration state, and queue backlog.
@@ -537,6 +559,7 @@ Bunderhost provides a Model Context Protocol (MCP) server that allows coding age
537
559
  - `get_runtime_logs`: Stream runtime container logs.
538
560
 
539
561
  #### Agent Safety Rules for Bunderhost:
562
+
540
563
  1. **Secrets Are Never Exposed**: Database passwords, encryption keys, and environment values are never returned by MCP tools. When a new secret is needed, the agent must create a setup session (`create_setup_session`), and the user types the secret in the Bunderhost UI.
541
564
  2. **Mutations Require Confirmation**: Creating projects or deploying revisions require explicit user approval in the MCP client before execution.
542
565
 
@@ -544,15 +567,14 @@ Bunderhost provides a Model Context Protocol (MCP) server that allows coding age
544
567
 
545
568
  ## 9. Quick Reference & Common Mistakes
546
569
 
547
- | Anti-Pattern (Don't Do This) | Canonical Pattern (Do This) |
548
- | --- | --- |
549
- | Creating separate `/api/auth/$` and `/api/trpc/$` routes | Single catch-all `src/routes/api/$.ts` with `createApiHandlers(app)` |
550
- | Creating multiple Drizzle instances in `src/lib/db.ts` | Use `app.db` and `context.db`; export types with `BunderstackDb<typeof schema>` |
551
- | Constructing `ORPCError` manually | Use `errors.CODE({ message })` or `new BunderstackError('CODE', message)` |
552
- | Calling `getSession()` inside global middleware | Use `context.peekSession()` for non-blocking observability |
553
- | Editing `.sql` files in `migrations/` by hand | Always generate with `bunx drizzle-kit generate` and commit untouched |
554
- | Deploying to Bunderhost with schema push only | Generate and commit Drizzle migrations before deploying |
555
- | Starting workers inside the web server process in prod | Run dedicated `src/worker.ts` with `app.runWorker()` |
556
- | Hand-written HTTP `/api/cron/*` endpoints | Use `jobs.cron({ schedule, handler })` |
557
- | Top-level import of `app` inside `auth.ts` or `api/base.ts` | Consume `context` in handlers or use dynamic `import('./index')` in callbacks |
558
-
570
+ | Anti-Pattern (Don't Do This) | Canonical Pattern (Do This) |
571
+ | ----------------------------------------------------------- | ------------------------------------------------------------------------------- |
572
+ | Creating separate `/api/auth/$` and `/api/trpc/$` routes | Single catch-all `src/routes/api/$.ts` with `createApiHandlers(app)` |
573
+ | Creating multiple Drizzle instances in `src/lib/db.ts` | Use `app.db` and `context.db`; export types with `BunderstackDb<typeof schema>` |
574
+ | Constructing `ORPCError` manually | Use `errors.CODE({ message })` or `new BunderstackError('CODE', message)` |
575
+ | Calling `getSession()` inside global middleware | Use `context.peekSession()` for non-blocking observability |
576
+ | Editing `.sql` files in `migrations/` by hand | Always generate with `bunx drizzle-kit generate` and commit untouched |
577
+ | Deploying to Bunderhost with schema push only | Generate and commit Drizzle migrations before deploying |
578
+ | Starting workers inside the web server process in prod | Run dedicated `src/worker.ts` with `app.runWorker()` |
579
+ | Hand-written HTTP `/api/cron/*` endpoints | Use `jobs.cron({ schedule, handler })` |
580
+ | Top-level import of `app` inside `auth.ts` or `api/base.ts` | Consume `context` in handlers or use dynamic `import('./index')` in callbacks |
@@ -12,12 +12,12 @@ output or a file reference, not an assertion.
12
12
  | API mounting | Hand-written handler maps; separate `/api/auth/$`, `/api/trpc/$` | `createApiHandlers(app)` on one `/api/$` | Auth and oRPC requests succeed with only the catch-all present | Shadowing route files deleted |
13
13
  | Custom API routes | Route files doing CRUD the framework can generate | Generated CRUD plus `defineAccess`, or an `o.protected` procedure | Access rules cover each exposed table; a cross-owner request is denied | Route file has no client callers |
14
14
  | Access control | Per-endpoint session checks and hand-written SQL filters | `defineAccess(schema, rules)` with `scope.read` / `scope.write` | A test asserts a second user cannot read or write the first user's rows | Manual filter helpers unused |
15
- | Jobs | BullMQ or a bespoke queue module | `jobs.define({ ... })` and `app.jobs.enqueue(...)` | Job appears in `backend.manifest.background.jobs` | No queue library importer; package uninstalled |
15
+ | Jobs | BullMQ or a bespoke queue module | `jobs.define({ ... })` and `app.jobs.enqueue(...)` | Job appears in `backend.inspect().background.jobs` | No queue library importer; package uninstalled |
16
16
  | Cron | `/api/cron/*` guarded by a shared secret | `jobs.cron({ schedule, handler })` | Cron task appears in the blueprint | Cron route file and its secret removed from env |
17
17
  | Worker topology | `startWorker()` or a queue bootstrap in the web entry | `src/worker.ts` calling `app.runWorker()`, run as its own process | Web entry starts no worker; the worker command exists in deployment config | Worker process is deployed before the embedded call is removed |
18
18
  | Realtime | Custom WebSocket server, manual pub/sub, channel-and-payload publishing | `realtime` config plus `ctx.realtime.publish(table, event, row)` after commit | A direct write reaches a subscriber with the complete row | Custom transport deleted; shared Redis configured for multi-process |
19
19
  | Storage | AWS or Tigris SDK wrapper, custom multipart upload route | Declared buckets and `app.storage` | Upload, signed URL, and delete work through the facade | Wrapper deleted and SDK uninstalled |
20
- | Email | Resend or SMTP SDK wrapper | `email` config and `app.email.send(...)` | A send succeeds through the configured provider | Wrapper deleted and SDK uninstalled |
20
+ | Messaging | Resend, SMTP, or Telegram SDK wrapper | `messaging` channels and `app.messaging.<channel>.send(...)` | A send succeeds through the configured provider | Wrapper deleted and SDK uninstalled |
21
21
  | Env | `createEnv()` beside the app, `dotenv`, unchecked `process.env` reads | `env` schema in `bunderstack()`; source in `backend.start()`; `app.env` / `ctx.env` | Boot fails with a clear message when a required variable is missing | Legacy env module unused; `.env.example` lists names only |
22
22
  | API declaration | Router factories taking a bag of procedures; hand-written builder generics | `defineApi({ schema, env })` bases in one module, plain router objects, `api` object | A router module imports its base and exports an object; no factory remains | `BunderstackApiBuilder<...>` and `os.$context<...>()` deleted |
23
23
  | Observability | Tracing or logging attached to an application procedure base | `middleware: [...]` in the config, which also reaches the generated CRUD | A generated CRUD request produces a span or log line | Per-base instrumentation removed |
@@ -23,12 +23,14 @@ export const backend = bunderstack({
23
23
  schema,
24
24
  access,
25
25
  env: envSchema,
26
- database: {
27
- adapter: libsql(),
28
- url: 'file:./data.db',
29
- },
26
+ database: { adapter: libsql() },
30
27
  auth: authConfig,
31
- email: { from: 'App <no-reply@example.com>' },
28
+ messaging: (env) => ({
29
+ email: resend({
30
+ apiKey: env.RESEND_API_KEY,
31
+ from: 'App <no-reply@example.com>',
32
+ }),
33
+ }),
32
34
  storage: {
33
35
  local: './uploads',
34
36
  defaultBucket: 'files',
@@ -69,7 +71,7 @@ await provision(app)
69
71
 
70
72
  The database adapter is imported explicitly; there is no implicit driver. Keep
71
73
  unrelated external side effects out of the backend import graph. The blueprint
72
- imports the declaration and reads `backend.manifest`; it never starts the app,
74
+ imports the declaration and calls `backend.inspect({ env })`; it never starts the app,
73
75
  connects to a queue, or needs a special environment flag.
74
76
 
75
77
  Aggregate every domain, Better Auth, plugin, and internal table in the schema
@@ -230,19 +232,21 @@ access rules. Delete the AWS or Tigris wrapper and uninstall the SDK. A custom
230
232
  multipart upload route is replaced by the bucket's own upload route unless it
231
233
  performs domain work that cannot move into a job.
232
234
 
233
- ## Email
235
+ ## Messaging
234
236
 
235
237
  ```ts
236
- await app.email.send({ to, subject, html })
238
+ await app.messaging.email.send({ to, subject, html })
237
239
  ```
238
240
 
239
- Configure `email: { from, provider }`. `provider` defaults to `resend` when
240
- `RESEND_API_KEY` is set and `console` in development. The facade uses Web
241
- Standard `fetch`, so the `resend` package is uninstalled.
241
+ Declare named channels: `messaging: (env) => ({ email: resend({ apiKey: env.RESEND_API_KEY, from }) })`. A
242
+ channel whose credentials are absent or empty captures to the message journal
243
+ instead of sending, and prints to the console locally. Telegram is a provider
244
+ too, with its own message type. The facade uses Web Standard `fetch`, so the
245
+ `resend` package is uninstalled.
242
246
 
243
247
  ## Env
244
248
 
245
- Pass `envSchema` to `bunderstack({ env: envSchema })` and read `app.env`
249
+ Pass `envSchema` as the `env` key of the declaration and read `app.env`
246
250
  or `ctx.env`. Remove `@t3-oss/env-core` `createEnv()` calls and `dotenv`; Bun
247
251
  loads `.env` itself. Server variables must not use the `PUBLIC_` prefix, and
248
252
  browser-safe variables must. Declared env appears in the deployment blueprint,