bunderstack 0.23.4 → 0.24.1

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 (159) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/README.md +7 -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/auth.d.ts +20 -9
  14. package/dist/auth.d.ts.map +1 -1
  15. package/dist/auth.js +3 -8
  16. package/dist/auth.js.map +1 -1
  17. package/dist/backend-internals.d.ts +5 -13
  18. package/dist/backend-internals.d.ts.map +1 -1
  19. package/dist/backend-internals.js.map +1 -1
  20. package/dist/backend.d.ts +39 -5
  21. package/dist/backend.d.ts.map +1 -1
  22. package/dist/backend.js +37 -46
  23. package/dist/backend.js.map +1 -1
  24. package/dist/blueprint-generator.d.ts +1 -0
  25. package/dist/blueprint-generator.d.ts.map +1 -1
  26. package/dist/blueprint-generator.js +29 -1
  27. package/dist/blueprint-generator.js.map +1 -1
  28. package/dist/blueprint.d.ts +2 -1
  29. package/dist/blueprint.d.ts.map +1 -1
  30. package/dist/blueprint.js +9 -2
  31. package/dist/blueprint.js.map +1 -1
  32. package/dist/cli.d.ts.map +1 -1
  33. package/dist/cli.js +14 -2
  34. package/dist/cli.js.map +1 -1
  35. package/dist/config.d.ts +23 -25
  36. package/dist/config.d.ts.map +1 -1
  37. package/dist/config.js +1 -1
  38. package/dist/config.js.map +1 -1
  39. package/dist/email/smtp.d.ts +7 -1
  40. package/dist/email/smtp.d.ts.map +1 -1
  41. package/dist/email/smtp.js +5 -1
  42. package/dist/email/smtp.js.map +1 -1
  43. package/dist/email.d.ts +3 -29
  44. package/dist/email.d.ts.map +1 -1
  45. package/dist/email.js +1 -194
  46. package/dist/email.js.map +1 -1
  47. package/dist/env-probe.d.ts +9 -0
  48. package/dist/env-probe.d.ts.map +1 -0
  49. package/dist/env-probe.js +73 -0
  50. package/dist/env-probe.js.map +1 -0
  51. package/dist/env.d.ts +2 -5
  52. package/dist/env.d.ts.map +1 -1
  53. package/dist/env.js +2 -9
  54. package/dist/env.js.map +1 -1
  55. package/dist/hosted-contract.d.ts +11 -0
  56. package/dist/hosted-contract.d.ts.map +1 -0
  57. package/dist/hosted-contract.js +46 -0
  58. package/dist/hosted-contract.js.map +1 -0
  59. package/dist/index.d.ts +3 -2
  60. package/dist/index.d.ts.map +1 -1
  61. package/dist/index.js +1 -1
  62. package/dist/index.js.map +1 -1
  63. package/dist/inspect.d.ts +15 -0
  64. package/dist/inspect.d.ts.map +1 -0
  65. package/dist/inspect.js +43 -0
  66. package/dist/inspect.js.map +1 -0
  67. package/dist/internal-tables-pg.d.ts +43 -94
  68. package/dist/internal-tables-pg.d.ts.map +1 -1
  69. package/dist/internal-tables-pg.js +14 -17
  70. package/dist/internal-tables-pg.js.map +1 -1
  71. package/dist/internal-tables.d.ts +213 -488
  72. package/dist/internal-tables.d.ts.map +1 -1
  73. package/dist/internal-tables.js +33 -33
  74. package/dist/internal-tables.js.map +1 -1
  75. package/dist/jobs/define.d.ts +20 -20
  76. package/dist/jobs/define.d.ts.map +1 -1
  77. package/dist/jobs/define.js.map +1 -1
  78. package/dist/manifest-diff.d.ts +6 -0
  79. package/dist/manifest-diff.d.ts.map +1 -0
  80. package/dist/manifest-diff.js +42 -0
  81. package/dist/manifest-diff.js.map +1 -0
  82. package/dist/manifest.d.ts +10 -2
  83. package/dist/manifest.d.ts.map +1 -1
  84. package/dist/manifest.js +27 -29
  85. package/dist/manifest.js.map +1 -1
  86. package/dist/messaging/email.d.ts +16 -0
  87. package/dist/messaging/email.d.ts.map +1 -0
  88. package/dist/messaging/email.js +8 -0
  89. package/dist/messaging/email.js.map +1 -0
  90. package/dist/messaging/index.d.ts +8 -0
  91. package/dist/messaging/index.d.ts.map +1 -0
  92. package/dist/messaging/index.js +4 -0
  93. package/dist/messaging/index.js.map +1 -0
  94. package/dist/messaging/journal.d.ts +4 -0
  95. package/dist/messaging/journal.d.ts.map +1 -0
  96. package/dist/messaging/journal.js +19 -0
  97. package/dist/messaging/journal.js.map +1 -0
  98. package/dist/messaging/runtime.d.ts +16 -0
  99. package/dist/messaging/runtime.d.ts.map +1 -0
  100. package/dist/messaging/runtime.js +213 -0
  101. package/dist/messaging/runtime.js.map +1 -0
  102. package/dist/messaging/standalone.d.ts +27 -0
  103. package/dist/messaging/standalone.d.ts.map +1 -0
  104. package/dist/messaging/standalone.js +18 -0
  105. package/dist/messaging/standalone.js.map +1 -0
  106. package/dist/messaging/telegram.d.ts +16 -0
  107. package/dist/messaging/telegram.d.ts.map +1 -0
  108. package/dist/messaging/telegram.js +5 -0
  109. package/dist/messaging/telegram.js.map +1 -0
  110. package/dist/messaging/types.d.ts +22 -0
  111. package/dist/messaging/types.d.ts.map +1 -0
  112. package/dist/messaging/types.js +17 -0
  113. package/dist/messaging/types.js.map +1 -0
  114. package/dist/provision-internals.d.ts +1 -1
  115. package/dist/provision-internals.js +1 -1
  116. package/dist/provision-internals.js.map +1 -1
  117. package/dist/provision-runtime.d.ts +7 -0
  118. package/dist/provision-runtime.d.ts.map +1 -0
  119. package/dist/provision-runtime.js +50 -0
  120. package/dist/provision-runtime.js.map +1 -0
  121. package/dist/provision-schema.d.ts +16 -0
  122. package/dist/provision-schema.d.ts.map +1 -0
  123. package/dist/provision-schema.js +63 -0
  124. package/dist/provision-schema.js.map +1 -0
  125. package/dist/provision.d.ts +4 -21
  126. package/dist/provision.d.ts.map +1 -1
  127. package/dist/provision.js +10 -111
  128. package/dist/provision.js.map +1 -1
  129. package/dist/runtime.d.ts +17 -14
  130. package/dist/runtime.d.ts.map +1 -1
  131. package/dist/runtime.js +20 -33
  132. package/dist/runtime.js.map +1 -1
  133. package/dist/schema-export-pg.d.ts +1 -1
  134. package/dist/schema-export-pg.d.ts.map +1 -1
  135. package/dist/schema-export-pg.js +1 -1
  136. package/dist/schema-export-pg.js.map +1 -1
  137. package/dist/schema-export.d.ts +1 -1
  138. package/dist/schema-export.d.ts.map +1 -1
  139. package/dist/schema-export.js +1 -1
  140. package/dist/schema-export.js.map +1 -1
  141. package/dist/testing/fixture.d.ts +3 -3
  142. package/dist/testing/fixture.d.ts.map +1 -1
  143. package/dist/testing/fixture.js +12 -10
  144. package/dist/testing/fixture.js.map +1 -1
  145. package/dist/testing/messaging.d.ts +21 -0
  146. package/dist/testing/messaging.d.ts.map +1 -0
  147. package/dist/testing/messaging.js +34 -0
  148. package/dist/testing/messaging.js.map +1 -0
  149. package/dist/testing.d.ts +1 -0
  150. package/dist/testing.d.ts.map +1 -1
  151. package/dist/testing.js.map +1 -1
  152. package/llms-full.txt +416 -226
  153. package/llms.txt +4 -3
  154. package/package.json +10 -2
  155. package/skills/creating-bunderstack-apps/references/application-structure.md +16 -7
  156. package/skills/creating-bunderstack-apps/references/verification.md +7 -6
  157. package/skills/migrating-to-bunderstack/SKILL.md +75 -51
  158. package/skills/migrating-to-bunderstack/references/audit-checklist.md +2 -2
  159. package/skills/migrating-to-bunderstack/references/runtime-replacements.md +20 -15
package/llms.txt CHANGED
@@ -41,9 +41,10 @@ MINIMAL APP
41
41
 
42
42
  The blueprint imports the backend declaration and never starts the runtime.
43
43
  Database adapters are imported from their own entry points: libsql(),
44
- pglite(), bunSql(), postgresJs(). Provisioning: `await provision(app)` pushes
45
- the schema in development and applies committed migrations once a migrations/
46
- folder exists.
44
+ pglite(), bunSql(), postgresJs(). Production provisioning imports
45
+ `provision(app)` from `bunderstack/provision` and requires committed migrations;
46
+ that entrypoint never imports Drizzle Kit. Development schema push imports the
47
+ same function name from `bunderstack/provision-schema` instead.
47
48
 
48
49
  DECLARING AN API
49
50
 
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.1",
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",
@@ -77,6 +77,10 @@
77
77
  "types": "./dist/provision.d.ts",
78
78
  "default": "./dist/provision.js"
79
79
  },
80
+ "./provision-schema": {
81
+ "types": "./dist/provision-schema.d.ts",
82
+ "default": "./dist/provision-schema.js"
83
+ },
80
84
  "./testing": {
81
85
  "types": "./dist/testing.d.ts",
82
86
  "default": "./dist/testing.js"
@@ -113,6 +117,10 @@
113
117
  "types": "./dist/email/smtp.d.ts",
114
118
  "default": "./dist/email/smtp.js"
115
119
  },
120
+ "./messaging": {
121
+ "types": "./dist/messaging/index.d.ts",
122
+ "default": "./dist/messaging/index.js"
123
+ },
116
124
  "./api": {
117
125
  "types": "./dist/api/types.d.ts",
118
126
  "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
 
@@ -17,9 +17,10 @@ the configured Bunderstack entry. Set `package.json#bunderstack.entry` when the
17
17
  entry is not `src/bunderstack.ts`. `bun run blueprint:check` must pass in CI so
18
18
  the committed declaration matches the application.
19
19
 
20
- Before production, generate and commit the Drizzle `migrations/` folder. With
21
- no migrations folder, `provision(app)` uses the development schema-push loop
22
- (and needs drizzle-kit). Once migrations are committed, it applies pending
23
- migrations without importing drizzle-kit. Keep the generated migrations,
24
- blueprint, tests, worker entry, API mount, and deployment scripts under version
25
- control; never commit secrets, databases, uploads, or build output.
20
+ Before production, generate and commit the Drizzle `migrations/` folder.
21
+ `provision(app)` from `bunderstack/provision` only applies committed
22
+ migrations and never imports drizzle-kit. For the local schema-push loop, import
23
+ `provision` from `bunderstack/provision-schema`; that development-only
24
+ entrypoint requires drizzle-kit. Keep the generated migrations, blueprint,
25
+ tests, worker entry, API mount, and deployment scripts under version control;
26
+ never commit secrets, databases, uploads, or build output.
@@ -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,26 @@ 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` | Production provisioning from committed migrations |
74
+ | `bunderstack/provision-schema` | Development-only schema push through Drizzle Kit |
75
+ | `bunderstack/testing` | Test fixture helpers |
76
+ | `bunderstack/schema` | Internal system tables (`export * from 'bunderstack/schema'`) |
77
+ | `bunderstack/typeid` | TypeID column types & generators |
74
78
 
75
79
  ---
76
80
 
@@ -79,6 +83,7 @@ All Bunderstack capabilities are imported directly from single-segment subpaths
79
83
  ### Scale Decision: Flat vs. Modular
80
84
 
81
85
  1. **Flat Layout (MVP / Small Service: < 5 tables, < 5 procedures, 1 job):**
86
+
82
87
  ```
83
88
  src/
84
89
  ├── bunderstack.ts # backend declaration & app start
@@ -159,14 +164,16 @@ export const protectedProcedure = o.protected.use(async ({ context, next }) => {
159
164
  return next()
160
165
  })
161
166
 
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
- })
167
+ export const adminProcedure = protectedProcedure.use(
168
+ async ({ context, next, errors }) => {
169
+ if (context.user.role !== 'admin') {
170
+ throw errors.FORBIDDEN({ message: 'Admin privileges required' })
171
+ }
172
+ return next()
173
+ },
174
+ )
168
175
 
169
- // Graph-wide observability middleware (registered in bunderstack({ middleware: [instrumentation] }))
176
+ // Graph-wide observability middleware (registered as `middleware: [instrumentation]`)
170
177
  export const instrumentation = o.middleware(async ({ context, next, path }) => {
171
178
  const startedAt = performance.now()
172
179
  try {
@@ -176,7 +183,9 @@ export const instrumentation = o.middleware(async ({ context, next, path }) => {
176
183
  const duration = Math.round(performance.now() - startedAt)
177
184
  // context.peekSession() reads resolved session without triggering forced auth on public/webhooks
178
185
  const userId = context.peekSession()?.user?.id
179
- console.log(`[oRPC] ${path.join('.')} - ${duration}ms - User: ${userId ?? 'anon'}`)
186
+ console.log(
187
+ `[oRPC] ${path.join('.')} - ${duration}ms - User: ${userId ?? 'anon'}`,
188
+ )
180
189
  }
181
190
  })
182
191
  ```
@@ -184,6 +193,7 @@ export const instrumentation = o.middleware(async ({ context, next, path }) => {
184
193
  ### Rule: Circular Boot-Time Import Prevention
185
194
 
186
195
  `src/bunderstack/auth.ts` and `api/base.ts` must **NEVER** import `app` or `src/bunderstack/index.ts` at module top-level.
196
+
187
197
  - 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
198
  - In `api/*.ts`: Consume `context.db`, `context.env`, `context.jobs`, `context.storage`, `context.auth` from handler parameters.
189
199
 
@@ -235,15 +245,16 @@ export const access = defineAccess(schema, {
235
245
  await client.posts.update.call({ id: 'post_1', title: 'Updated Title' })
236
246
  ```
237
247
  - **Custom List Procedures**: Use `listSpec` to give custom endpoints the same pagination and filtering behavior:
248
+
238
249
  ```ts
239
250
  import { listSpec } from 'bunderstack'
240
-
251
+
241
252
  const logSpec = listSpec(schema.auditLogs, {
242
253
  filterable: ['level', 'userId'],
243
254
  sortable: ['createdAt'],
244
255
  defaultSort: { column: 'createdAt', order: 'desc' },
245
256
  })
246
-
257
+
247
258
  export const logsProcedure = adminProcedure
248
259
  .input(logSpec.input)
249
260
  .handler(logSpec.handler)
@@ -251,7 +262,7 @@ export const access = defineAccess(schema, {
251
262
 
252
263
  ### 3.2 Authentication (`authConfig`)
253
264
 
254
- Export a clean Better Auth config and pass it into `bunderstack({ auth: authConfig })`:
265
+ 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
266
 
256
267
  ```ts
257
268
  // src/bunderstack/auth.ts
@@ -277,12 +288,15 @@ import * as v from 'valibot'
277
288
  export const defineJobs = (jobs) =>
278
289
  jobs.define({
279
290
  sendWelcomeEmail: jobs.job({
280
- input: v.object({ userId: v.string(), email: v.pipe(v.string(), v.email()) }),
291
+ input: v.object({
292
+ userId: v.string(),
293
+ email: v.pipe(v.string(), v.email()),
294
+ }),
281
295
  concurrency: 5,
282
296
  timeout: 30_000,
283
297
  retries: 3,
284
298
  handler: async ({ userId, email }, ctx) => {
285
- await ctx.email.send({
299
+ await ctx.messaging.email.send({
286
300
  to: email,
287
301
  subject: 'Welcome!',
288
302
  html: '<h1>Welcome to our service</h1>',
@@ -360,6 +374,7 @@ Raise typed errors in procedures using `errors`:
360
374
  ```
361
375
 
362
376
  Outside procedures (e.g. in background jobs or domain services):
377
+
363
378
  ```ts
364
379
  import { BunderstackError } from 'bunderstack'
365
380
 
@@ -373,17 +388,19 @@ throw new BunderstackError('FORBIDDEN', 'Quota exceeded')
373
388
  ### Development vs. Production Lifecycle
374
389
 
375
390
  1. **Local Development (No Migrations Folder):**
376
- - In dev, `await provision(app)` automatically pushes the schema to the SQLite/libSQL/Postgres database.
391
+ - Import `provision` from `bunderstack/provision-schema`; it pushes the schema to the SQLite/libSQL/Postgres database.
377
392
  - Developers can rapidly prototype and iterate on table schemas without generating migrations on every change.
378
393
 
379
394
  2. **Production & Bunderhost Deployments (MANDATORY Migrations):**
380
395
  - **Committed migrations are strictly mandatory for production deployments.**
396
+ - Import `provision` from `bunderstack/provision`; it contains no Drizzle Kit import edge and fails clearly when the migration journal is missing.
381
397
  - Bunderhost will **NOT** run schema push in production; deployment will fail if committed migrations in `migrations/` are missing or out of date.
382
398
 
383
399
  ### CRITICAL MIGRATION RULES
384
400
 
385
401
  > [!CAUTION]
386
402
  > **ALL MIGRATIONS MUST BE GENERATED EXCLUSIVELY VIA DRIZZLE-KIT CLI.**
403
+ >
387
404
  > - Always run: `bunx drizzle-kit generate` (or `bun run db:generate`).
388
405
  > - **NEVER** hand-edit generated migration SQL files.
389
406
  > - **NEVER** let an LLM agent write or modify `.sql` files in `migrations/`.
@@ -410,7 +427,7 @@ export const Route = createFileRoute('/api/$')({
410
427
  })
411
428
  ```
412
429
 
413
- *Note: Remove any separate `/api/auth/$`, `/api/trpc/$`, or `/api/cron/*` route files.*
430
+ _Note: Remove any separate `/api/auth/$`, `/api/trpc/$`, or `/api/cron/_` route files.\*
414
431
 
415
432
  ### Dedicated Production Worker (`src/worker.ts`)
416
433
 
@@ -429,6 +446,7 @@ await app.runWorker()
429
446
  ```
430
447
 
431
448
  Add worker script in `package.json`:
449
+
432
450
  ```json
433
451
  {
434
452
  "scripts": {
@@ -470,14 +488,17 @@ test('creates and retrieves a post', async () => {
470
488
  // Typed in-process oRPC client
471
489
  const client = t.client(identity)
472
490
 
473
- const created = await client.posts.create({ title: 'New Post', content: 'Hello' })
491
+ const created = await client.posts.create({
492
+ title: 'New Post',
493
+ content: 'Hello',
494
+ })
474
495
  expect(created.title).toBe('New Post')
475
496
 
476
497
  // Run all queued background jobs deterministically
477
498
  await t.jobs.runUntilIdle()
478
499
 
479
500
  // Inspect sent emails
480
- expect(t.email.sent).toHaveLength(0)
501
+ expect(t.messaging.email.sent).toHaveLength(0)
481
502
  })
482
503
  ```
483
504
 
@@ -515,6 +536,7 @@ Bunderhost monitors application deployment status via `GET /api/readiness`, whic
515
536
  ### Official Bunderstack Documentation for LLMs
516
537
 
517
538
  When working on Bunderstack projects, consult the dedicated LLM references:
539
+
518
540
  - **Web Documentation**: [https://bunderstack.kcrz.dev/docs](https://bunderstack.kcrz.dev/docs)
519
541
  - **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
542
  - **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 +546,12 @@ When working on Bunderstack projects, consult the dedicated LLM references:
524
546
  Bunderhost provides a Model Context Protocol (MCP) server that allows coding agents to inspect, manage, and deploy projects.
525
547
 
526
548
  #### Connecting to Bunderhost MCP:
549
+
527
550
  1. Generate an Agent Access Token in Bunderhost: **Organization → Agent Access → Issue Token**.
528
551
  2. Connect your MCP client to `https://<bunderhost-host>/mcp` using the token as a `Bearer` credential.
529
552
 
530
553
  #### Key MCP Tools:
554
+
531
555
  - `list_projects`: List all projects in the organization.
532
556
  - `get_project`: Retrieve project configuration, active deployments, and blueprint status.
533
557
  - `get_project_readiness`: Check database reachability, migration state, and queue backlog.
@@ -537,6 +561,7 @@ Bunderhost provides a Model Context Protocol (MCP) server that allows coding age
537
561
  - `get_runtime_logs`: Stream runtime container logs.
538
562
 
539
563
  #### Agent Safety Rules for Bunderhost:
564
+
540
565
  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
566
  2. **Mutations Require Confirmation**: Creating projects or deploying revisions require explicit user approval in the MCP client before execution.
542
567
 
@@ -544,15 +569,14 @@ Bunderhost provides a Model Context Protocol (MCP) server that allows coding age
544
569
 
545
570
  ## 9. Quick Reference & Common Mistakes
546
571
 
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
-
572
+ | Anti-Pattern (Don't Do This) | Canonical Pattern (Do This) |
573
+ | ----------------------------------------------------------- | ------------------------------------------------------------------------------- |
574
+ | Creating separate `/api/auth/$` and `/api/trpc/$` routes | Single catch-all `src/routes/api/$.ts` with `createApiHandlers(app)` |
575
+ | Creating multiple Drizzle instances in `src/lib/db.ts` | Use `app.db` and `context.db`; export types with `BunderstackDb<typeof schema>` |
576
+ | Constructing `ORPCError` manually | Use `errors.CODE({ message })` or `new BunderstackError('CODE', message)` |
577
+ | Calling `getSession()` inside global middleware | Use `context.peekSession()` for non-blocking observability |
578
+ | Editing `.sql` files in `migrations/` by hand | Always generate with `bunx drizzle-kit generate` and commit untouched |
579
+ | Deploying to Bunderhost with schema push only | Generate and commit Drizzle migrations before deploying |
580
+ | Starting workers inside the web server process in prod | Run dedicated `src/worker.ts` with `app.runWorker()` |
581
+ | Hand-written HTTP `/api/cron/*` endpoints | Use `jobs.cron({ schedule, handler })` |
582
+ | 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,
@@ -251,9 +255,10 @@ with names and safe placeholders only.
251
255
 
252
256
  ## Provisioning, migrations, and blueprint
253
257
 
254
- `provision(app)` uses the development schema-push loop while no `migrations/`
255
- folder exists, and applies committed migrations once one does. Generate and
256
- commit migrations before production:
258
+ `provision(app)` from `bunderstack/provision` applies committed migrations
259
+ without importing Drizzle Kit and fails when the journal is absent. During
260
+ local prototyping, import it from `bunderstack/provision-schema` to use the
261
+ development schema-push loop. Generate and commit migrations before production:
257
262
 
258
263
  ```json
259
264
  {