bunderstack 0.23.1 → 0.23.4
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.
- package/CHANGELOG.md +24 -0
- package/dist/internal-tables-pg.d.ts.map +1 -1
- package/dist/internal-tables-pg.js +1 -0
- package/dist/internal-tables-pg.js.map +1 -1
- package/dist/internal-tables.d.ts.map +1 -1
- package/dist/internal-tables.js +1 -0
- package/dist/internal-tables.js.map +1 -1
- package/dist/jobs/define.d.ts +15 -6
- package/dist/jobs/define.d.ts.map +1 -1
- package/dist/jobs/define.js +15 -3
- package/dist/jobs/define.js.map +1 -1
- package/dist/jobs/index.d.ts +2 -2
- package/dist/jobs/index.d.ts.map +1 -1
- package/dist/jobs/index.js +1 -1
- package/dist/jobs/index.js.map +1 -1
- package/dist/jobs/worker.d.ts.map +1 -1
- package/dist/jobs/worker.js +182 -39
- package/dist/jobs/worker.js.map +1 -1
- package/package.json +1 -1
- package/skills/migrating-to-bunderstack/SKILL.md +554 -81
|
@@ -1,85 +1,558 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: migrating-to-bunderstack
|
|
3
|
-
description: Use when
|
|
3
|
+
description: Use when building, structuring, or migrating an application on Bunderstack, configuring Drizzle schemas, Better Auth, oRPC procedures, access rules, storage, background jobs, realtime, or preparing deployments for Bunderhost.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
#
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
|
56
|
-
|
|
|
57
|
-
| `
|
|
58
|
-
|
|
|
59
|
-
|
|
|
60
|
-
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
`
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
6
|
+
# Bunderstack Architecture & Migration Guide
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
Bunderstack is a batteries-included full-stack backend framework for Bun unifying:
|
|
11
|
+
- **Drizzle ORM** (libSQL / SQLite / Postgres)
|
|
12
|
+
- **Better Auth** (authentication & session management)
|
|
13
|
+
- **oRPC v2** (unified type-safe RPC & OpenAPI/REST procedures with Standard Schema / Valibot)
|
|
14
|
+
- **Storage** (S3 / local disk buckets with transforms via Bun.Image)
|
|
15
|
+
- **Background Jobs & Cron** (durable queue, schedule execution, retry handling)
|
|
16
|
+
- **Realtime** (SSE streaming with heartbeat recovery & optimistic correlation)
|
|
17
|
+
- **Email** (Resend / SMTP / Console)
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 1. Core Architectural Model
|
|
22
|
+
|
|
23
|
+
### Declaration vs. Runtime Separation
|
|
24
|
+
|
|
25
|
+
Bunderstack separates the static application declaration from the running instance:
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { bunderstack } from 'bunderstack'
|
|
29
|
+
import { libsql } from 'bunderstack/libsql'
|
|
30
|
+
import { schema } from './schema'
|
|
31
|
+
import { access } from './access'
|
|
32
|
+
import { api } from './api'
|
|
33
|
+
|
|
34
|
+
// 1. Pure synchronous declaration (does NO I/O, no DB connection, exports static manifest)
|
|
35
|
+
export const backend = bunderstack({
|
|
36
|
+
schema,
|
|
37
|
+
access,
|
|
38
|
+
database: { adapter: libsql(), url: process.env.DATABASE_URL ?? 'file:./data.db' },
|
|
39
|
+
api,
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
// 2. Explicit runtime start (connects to DB, migrates, starts services)
|
|
43
|
+
export const app = await backend.start()
|
|
44
|
+
export type App = typeof app
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
- `backend.manifest` can be read statically by tools (such as blueprint generators) without starting the app or connecting to a database.
|
|
48
|
+
- `app = await backend.start()` explicitly boots the runtime.
|
|
49
|
+
- `backend.test()` creates an isolated, lexically owned test fixture.
|
|
50
|
+
|
|
51
|
+
### Single Package & Flat Subpath Exports
|
|
52
|
+
|
|
53
|
+
All Bunderstack capabilities are imported directly from single-segment subpaths of `bunderstack`:
|
|
54
|
+
|
|
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 |
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## 2. Project Structuring & Best Practices
|
|
78
|
+
|
|
79
|
+
### Scale Decision: Flat vs. Modular
|
|
80
|
+
|
|
81
|
+
1. **Flat Layout (MVP / Small Service: < 5 tables, < 5 procedures, 1 job):**
|
|
82
|
+
```
|
|
83
|
+
src/
|
|
84
|
+
├── bunderstack.ts # backend declaration & app start
|
|
85
|
+
├── schema.ts # Drizzle schema
|
|
86
|
+
├── access.ts # defineAccess rules
|
|
87
|
+
├── api.ts # defineApi and procedures
|
|
88
|
+
├── env.ts # Valibot envSchema
|
|
89
|
+
└── worker.ts # app.runWorker() entry
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
2. **Modular Layout (Production SaaS / Multi-Domain, HR Breakers pattern):**
|
|
93
|
+
Consolidate all backend logic in `src/bunderstack/` with domain separation:
|
|
94
|
+
```
|
|
95
|
+
src/bunderstack/
|
|
96
|
+
├── backend.ts # Synchronous bunderstack({...}) declaration
|
|
97
|
+
├── index.ts # export const app = await backend.start(), provision(app), exports { db, auth, env }
|
|
98
|
+
├── env.ts # envSchema (server / client) via Valibot
|
|
99
|
+
├── access.ts # defineAccess(schema, { ... })
|
|
100
|
+
├── auth.ts # authConfig for Better Auth
|
|
101
|
+
├── schema/ # Segmented Drizzle tables
|
|
102
|
+
│ ├── auth.ts # Better Auth tables
|
|
103
|
+
│ ├── billing.ts # Billing / subscription tables
|
|
104
|
+
│ ├── core.ts # Domain entities
|
|
105
|
+
│ └── index.ts # Aggregates schemas + export * from 'bunderstack/schema'
|
|
106
|
+
├── api/ # oRPC Procedure Graph
|
|
107
|
+
│ ├── base.ts # defineApi({ schema, env }), bases (public, protected, admin), middleware
|
|
108
|
+
│ ├── billing.ts # Billing domain router (plain object)
|
|
109
|
+
│ ├── users.ts # Users domain router
|
|
110
|
+
│ ├── admin.ts # Admin router
|
|
111
|
+
│ └── index.ts # export const api = { billing, users, admin }
|
|
112
|
+
├── jobs/ # Background Jobs & Cron
|
|
113
|
+
│ ├── generate-pdf.ts# Heavy job handler + onFailed
|
|
114
|
+
│ ├── cleanup.ts # jobs.cron() handler
|
|
115
|
+
│ └── index.ts # jobs.define({ ... })
|
|
116
|
+
├── methods.ts # (or services/) Pure domain database operations and business logic
|
|
117
|
+
└── types.ts # Database type aliases ($inferSelect, $inferInsert)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Rule: Thin Routers vs. Service Layer (`methods.ts`)
|
|
121
|
+
|
|
122
|
+
Keep API procedures thin. Routers only validate input, authorize, and delegate heavy logic to service functions:
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
// src/bunderstack/api/resumes.ts
|
|
126
|
+
import * as v from 'valibot'
|
|
127
|
+
import { protectedProcedure } from './base'
|
|
128
|
+
import { generateResumeForUser } from '../methods'
|
|
129
|
+
|
|
130
|
+
export const resumesRouter = {
|
|
131
|
+
generate: protectedProcedure
|
|
132
|
+
.input(v.object({ templateId: v.string() }))
|
|
133
|
+
.output(v.object({ jobId: v.string() }))
|
|
134
|
+
.handler(async ({ context, input }) => {
|
|
135
|
+
// Delegate to service function in methods.ts
|
|
136
|
+
const jobId = await generateResumeForUser(context, input.templateId)
|
|
137
|
+
return { jobId }
|
|
138
|
+
}),
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Rule: Module-Scoped API Builder (`api/base.ts`)
|
|
143
|
+
|
|
144
|
+
Declare `defineApi` once at module scope. Router files import procedure bases directly without needing factory functions:
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
// src/bunderstack/api/base.ts
|
|
148
|
+
import { defineApi } from 'bunderstack'
|
|
149
|
+
import { envSchema } from '../env'
|
|
150
|
+
import { schema } from '../schema'
|
|
151
|
+
import { eq } from 'drizzle-orm'
|
|
152
|
+
|
|
153
|
+
export const o = defineApi({ schema, env: envSchema })
|
|
154
|
+
|
|
155
|
+
export const publicProcedure = o.public
|
|
156
|
+
|
|
157
|
+
export const protectedProcedure = o.protected.use(async ({ context, next }) => {
|
|
158
|
+
// context.user is guaranteed non-null in o.protected
|
|
159
|
+
return next()
|
|
160
|
+
})
|
|
161
|
+
|
|
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
|
+
})
|
|
168
|
+
|
|
169
|
+
// Graph-wide observability middleware (registered in bunderstack({ middleware: [instrumentation] }))
|
|
170
|
+
export const instrumentation = o.middleware(async ({ context, next, path }) => {
|
|
171
|
+
const startedAt = performance.now()
|
|
172
|
+
try {
|
|
173
|
+
const result = await next()
|
|
174
|
+
return result
|
|
175
|
+
} finally {
|
|
176
|
+
const duration = Math.round(performance.now() - startedAt)
|
|
177
|
+
// context.peekSession() reads resolved session without triggering forced auth on public/webhooks
|
|
178
|
+
const userId = context.peekSession()?.user?.id
|
|
179
|
+
console.log(`[oRPC] ${path.join('.')} - ${duration}ms - User: ${userId ?? 'anon'}`)
|
|
180
|
+
}
|
|
181
|
+
})
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Rule: Circular Boot-Time Import Prevention
|
|
185
|
+
|
|
186
|
+
`src/bunderstack/auth.ts` and `api/base.ts` must **NEVER** import `app` or `src/bunderstack/index.ts` at module top-level.
|
|
187
|
+
- 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
|
+
- In `api/*.ts`: Consume `context.db`, `context.env`, `context.jobs`, `context.storage`, `context.auth` from handler parameters.
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## 3. Subsystem Architecture
|
|
193
|
+
|
|
194
|
+
### 3.1 Declarative Access Control & Generated CRUD
|
|
195
|
+
|
|
196
|
+
`defineAccess` automatically exposes REST & RPC CRUD procedures for tables (`list`, `get`, `create`, `update`, `delete`):
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
// src/bunderstack/access.ts
|
|
200
|
+
import { defineAccess } from 'bunderstack'
|
|
201
|
+
import { schema } from './schema'
|
|
202
|
+
|
|
203
|
+
export const access = defineAccess(schema, {
|
|
204
|
+
posts: {
|
|
205
|
+
list: 'public',
|
|
206
|
+
get: 'public',
|
|
207
|
+
create: 'authenticated',
|
|
208
|
+
update: 'owner',
|
|
209
|
+
delete: 'owner',
|
|
210
|
+
ownerColumn: 'authorId', // defaults to userId if present
|
|
211
|
+
filterableColumns: ['authorId', 'category', 'isPublished'],
|
|
212
|
+
sortableColumns: ['createdAt', 'title'],
|
|
213
|
+
defaultSort: { column: 'createdAt', order: 'desc' },
|
|
214
|
+
scope: {
|
|
215
|
+
read: (ctx) => ({ isPublished: true }), // applied to public queries
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
// Hide internal or sensitive tables from public CRUD endpoints
|
|
219
|
+
auditLogs: { crud: false },
|
|
220
|
+
systemSettings: { crud: false },
|
|
221
|
+
})
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
- **Querying CRUD over RPC**:
|
|
225
|
+
```ts
|
|
226
|
+
await client.posts.list.call({
|
|
227
|
+
filters: { authorId: 'user_123' },
|
|
228
|
+
sort: 'createdAt',
|
|
229
|
+
order: 'desc',
|
|
230
|
+
limit: 20,
|
|
231
|
+
})
|
|
232
|
+
```
|
|
233
|
+
- **CRUD Updates**: Accept `{ id, ...changes }` directly:
|
|
234
|
+
```ts
|
|
235
|
+
await client.posts.update.call({ id: 'post_1', title: 'Updated Title' })
|
|
236
|
+
```
|
|
237
|
+
- **Custom List Procedures**: Use `listSpec` to give custom endpoints the same pagination and filtering behavior:
|
|
238
|
+
```ts
|
|
239
|
+
import { listSpec } from 'bunderstack'
|
|
240
|
+
|
|
241
|
+
const logSpec = listSpec(schema.auditLogs, {
|
|
242
|
+
filterable: ['level', 'userId'],
|
|
243
|
+
sortable: ['createdAt'],
|
|
244
|
+
defaultSort: { column: 'createdAt', order: 'desc' },
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
export const logsProcedure = adminProcedure
|
|
248
|
+
.input(logSpec.input)
|
|
249
|
+
.handler(logSpec.handler)
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### 3.2 Authentication (`authConfig`)
|
|
253
|
+
|
|
254
|
+
Export a clean Better Auth config and pass it into `bunderstack({ auth: authConfig })`:
|
|
255
|
+
|
|
256
|
+
```ts
|
|
257
|
+
// src/bunderstack/auth.ts
|
|
258
|
+
import type { BetterAuthConfig } from 'better-auth'
|
|
259
|
+
|
|
260
|
+
export const authConfig = {
|
|
261
|
+
secret: process.env.AUTH_SECRET ?? 'dev-secret-at-least-32-chars-long',
|
|
262
|
+
emailAndPassword: { enabled: true },
|
|
263
|
+
session: { expiresIn: 60 * 60 * 24 * 7 }, // 7 days
|
|
264
|
+
} satisfies BetterAuthConfig
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
In `schema/index.ts`, ensure all Better Auth tables (`user`, `session`, `account`, `verification`) are aggregated so Drizzle migrations generate them.
|
|
268
|
+
|
|
269
|
+
### 3.3 Background Jobs & Scheduled Cron
|
|
270
|
+
|
|
271
|
+
Jobs and cron are declared inside `jobs.define`:
|
|
272
|
+
|
|
273
|
+
```ts
|
|
274
|
+
// src/bunderstack/jobs/index.ts
|
|
275
|
+
import * as v from 'valibot'
|
|
276
|
+
|
|
277
|
+
export const defineJobs = (jobs) =>
|
|
278
|
+
jobs.define({
|
|
279
|
+
sendWelcomeEmail: jobs.job({
|
|
280
|
+
input: v.object({ userId: v.string(), email: v.pipe(v.string(), v.email()) }),
|
|
281
|
+
concurrency: 5,
|
|
282
|
+
timeout: 30_000,
|
|
283
|
+
retries: 3,
|
|
284
|
+
handler: async ({ userId, email }, ctx) => {
|
|
285
|
+
await ctx.email.send({
|
|
286
|
+
to: email,
|
|
287
|
+
subject: 'Welcome!',
|
|
288
|
+
html: '<h1>Welcome to our service</h1>',
|
|
289
|
+
})
|
|
290
|
+
},
|
|
291
|
+
onFailed: async ({ userId }, error, ctx) => {
|
|
292
|
+
console.error(`Failed to send welcome email to user ${userId}`, error)
|
|
293
|
+
},
|
|
294
|
+
}),
|
|
295
|
+
hourlyCleanup: jobs.cron({
|
|
296
|
+
schedule: '0 * * * *', // Five-field UTC cron
|
|
297
|
+
handler: async (_invocation, ctx) => {
|
|
298
|
+
// ctx.db, ctx.env available
|
|
299
|
+
},
|
|
300
|
+
}),
|
|
301
|
+
})
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
- Enqueue from any handler or service: `await app.jobs.enqueue('sendWelcomeEmail', { userId: '1', email: 'user@example.com' })`
|
|
305
|
+
- Cron runs are automatically registered in the deployment blueprint.
|
|
306
|
+
|
|
307
|
+
### 3.4 Storage
|
|
308
|
+
|
|
309
|
+
Declare buckets with access control and file restrictions:
|
|
310
|
+
|
|
311
|
+
```ts
|
|
312
|
+
storage: {
|
|
313
|
+
local: './uploads',
|
|
314
|
+
defaultBucket: 'files',
|
|
315
|
+
buckets: {
|
|
316
|
+
avatars: {
|
|
317
|
+
visibility: 'public',
|
|
318
|
+
access: { create: 'authenticated', get: 'public', delete: 'owner' },
|
|
319
|
+
upload: { maxSize: '5mb', accept: ['image/png', 'image/jpeg', 'image/webp'] },
|
|
320
|
+
transforms: true, // enables on-the-fly resizing via Bun.Image
|
|
321
|
+
},
|
|
322
|
+
documents: {
|
|
323
|
+
visibility: 'private',
|
|
324
|
+
access: { create: 'authenticated', get: 'owner', delete: 'owner' },
|
|
325
|
+
},
|
|
326
|
+
},
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
- Uploading on server: `await app.storage.upload(key, fileBuffer, 'image/png', { bucket: 'avatars' })`
|
|
331
|
+
- Generating signed URL: `const url = await app.storage.getUrl(key, { expiresIn: 3600, bucket: 'documents' })`
|
|
332
|
+
|
|
333
|
+
### 3.5 Realtime Publishing
|
|
334
|
+
|
|
335
|
+
- Generated CRUD publishes updates automatically.
|
|
336
|
+
- For custom database writes, publish the **complete returned row** after the transaction commits:
|
|
337
|
+
|
|
338
|
+
```ts
|
|
339
|
+
const [row] = await ctx.db
|
|
340
|
+
.update(schema.tasks)
|
|
341
|
+
.set({ status: 'completed' })
|
|
342
|
+
.where(eq(schema.tasks.id, input.taskId))
|
|
343
|
+
.returning()
|
|
344
|
+
|
|
345
|
+
// Publish full row with schema table reference
|
|
346
|
+
await ctx.realtime.publish(schema.tasks, 'update', row)
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
### 3.6 Typed Errors
|
|
350
|
+
|
|
351
|
+
Raise typed errors in procedures using `errors`:
|
|
352
|
+
|
|
353
|
+
```ts
|
|
354
|
+
.handler(async ({ context, input, errors }) => {
|
|
355
|
+
const item = await findItem(context.db, input.id)
|
|
356
|
+
if (!item) throw errors.NOT_FOUND({ message: 'Item not found' })
|
|
357
|
+
if (item.locked) throw errors.CONFLICT({ message: 'Item is currently locked' })
|
|
358
|
+
return item
|
|
359
|
+
})
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
Outside procedures (e.g. in background jobs or domain services):
|
|
363
|
+
```ts
|
|
364
|
+
import { BunderstackError } from 'bunderstack'
|
|
365
|
+
|
|
366
|
+
throw new BunderstackError('FORBIDDEN', 'Quota exceeded')
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
---
|
|
370
|
+
|
|
371
|
+
## 4. Database Lifecycle & Strict Migration Rules
|
|
372
|
+
|
|
373
|
+
### Development vs. Production Lifecycle
|
|
374
|
+
|
|
375
|
+
1. **Local Development (No Migrations Folder):**
|
|
376
|
+
- In dev, `await provision(app)` automatically pushes the schema to the SQLite/libSQL/Postgres database.
|
|
377
|
+
- Developers can rapidly prototype and iterate on table schemas without generating migrations on every change.
|
|
378
|
+
|
|
379
|
+
2. **Production & Bunderhost Deployments (MANDATORY Migrations):**
|
|
380
|
+
- **Committed migrations are strictly mandatory for production deployments.**
|
|
381
|
+
- Bunderhost will **NOT** run schema push in production; deployment will fail if committed migrations in `migrations/` are missing or out of date.
|
|
382
|
+
|
|
383
|
+
### CRITICAL MIGRATION RULES
|
|
384
|
+
|
|
385
|
+
> [!CAUTION]
|
|
386
|
+
> **ALL MIGRATIONS MUST BE GENERATED EXCLUSIVELY VIA DRIZZLE-KIT CLI.**
|
|
387
|
+
> - Always run: `bunx drizzle-kit generate` (or `bun run db:generate`).
|
|
388
|
+
> - **NEVER** hand-edit generated migration SQL files.
|
|
389
|
+
> - **NEVER** let an LLM agent write or modify `.sql` files in `migrations/`.
|
|
390
|
+
> - Always commit both the schema changes in `src/bunderstack/schema/` and the newly generated files in `migrations/` together.
|
|
391
|
+
|
|
392
|
+
---
|
|
393
|
+
|
|
394
|
+
## 5. Web Handler & Dedicated Worker Process
|
|
395
|
+
|
|
396
|
+
### TanStack Start Catch-All (`src/routes/api/$.ts`)
|
|
397
|
+
|
|
398
|
+
Delegate all `/api/*` traffic (Better Auth, oRPC RPC, generated CRUD, Storage, Realtime) to a single catch-all handler:
|
|
399
|
+
|
|
400
|
+
```ts
|
|
401
|
+
// src/routes/api/$.ts
|
|
402
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
403
|
+
import { createApiHandlers } from 'bunderstack/start'
|
|
404
|
+
import { app } from '../../bunderstack'
|
|
405
|
+
|
|
406
|
+
export const Route = createFileRoute('/api/$')({
|
|
407
|
+
server: {
|
|
408
|
+
handlers: createApiHandlers(app),
|
|
409
|
+
},
|
|
410
|
+
})
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
*Note: Remove any separate `/api/auth/$`, `/api/trpc/$`, or `/api/cron/*` route files.*
|
|
414
|
+
|
|
415
|
+
### Dedicated Production Worker (`src/worker.ts`)
|
|
416
|
+
|
|
417
|
+
In production, run background jobs in a dedicated worker process:
|
|
418
|
+
|
|
419
|
+
```ts
|
|
420
|
+
// src/worker.ts
|
|
421
|
+
import { backend } from './bunderstack/backend'
|
|
422
|
+
|
|
423
|
+
const app = await backend.start({
|
|
424
|
+
env: { ...process.env, BUNDERSTACK_ROLE: 'worker' },
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
console.log('Bunderstack background worker started.')
|
|
428
|
+
await app.runWorker()
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
Add worker script in `package.json`:
|
|
432
|
+
```json
|
|
433
|
+
{
|
|
434
|
+
"scripts": {
|
|
435
|
+
"dev": "bun --bun vite dev",
|
|
436
|
+
"build": "vite build",
|
|
437
|
+
"start": "bun dist/server/server.js",
|
|
438
|
+
"worker": "bun src/worker.ts",
|
|
439
|
+
"db:generate": "drizzle-kit generate",
|
|
440
|
+
"blueprint": "bunderstack blueprint",
|
|
441
|
+
"blueprint:check": "bunderstack blueprint --check"
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
---
|
|
447
|
+
|
|
448
|
+
## 6. Testing with Lexical Fixtures
|
|
449
|
+
|
|
450
|
+
Use `backend.test()` to create isolated, disposable test fixtures with in-memory DB, mocked auth, and deterministic job queues:
|
|
451
|
+
|
|
452
|
+
```ts
|
|
453
|
+
// src/bunderstack/api/posts.test.ts
|
|
454
|
+
import { test, expect } from 'bun:test'
|
|
455
|
+
import { backend } from '../backend'
|
|
456
|
+
|
|
457
|
+
test('creates and retrieves a post', async () => {
|
|
458
|
+
// Fixture is automatically disposed at the end of scope
|
|
459
|
+
await using t = await backend.test({
|
|
460
|
+
database: { schema: 'push' },
|
|
461
|
+
})
|
|
462
|
+
|
|
463
|
+
// Create mock authenticated session
|
|
464
|
+
const identity = t.auth.mockSession({
|
|
465
|
+
id: 'user_1',
|
|
466
|
+
email: 'author@example.com',
|
|
467
|
+
name: 'Author Name',
|
|
468
|
+
})
|
|
469
|
+
|
|
470
|
+
// Typed in-process oRPC client
|
|
471
|
+
const client = t.client(identity)
|
|
472
|
+
|
|
473
|
+
const created = await client.posts.create({ title: 'New Post', content: 'Hello' })
|
|
474
|
+
expect(created.title).toBe('New Post')
|
|
475
|
+
|
|
476
|
+
// Run all queued background jobs deterministically
|
|
477
|
+
await t.jobs.runUntilIdle()
|
|
478
|
+
|
|
479
|
+
// Inspect sent emails
|
|
480
|
+
expect(t.email.sent).toHaveLength(0)
|
|
481
|
+
})
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
---
|
|
485
|
+
|
|
486
|
+
## 7. Bunderhost Deployment Contract
|
|
487
|
+
|
|
488
|
+
### The Blueprint (`bunderstack.blueprint.yaml`)
|
|
489
|
+
|
|
490
|
+
Bunderhost reads `bunderstack.blueprint.yaml` at the root of the repository to provision infrastructure (databases, buckets, background workers, cron schedules, environment variables).
|
|
491
|
+
|
|
492
|
+
1. Declare the Bunderstack entry point in `package.json`:
|
|
493
|
+
```json
|
|
494
|
+
{
|
|
495
|
+
"bunderstack": {
|
|
496
|
+
"entry": "src/bunderstack/backend.ts"
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
```
|
|
500
|
+
2. Generate and verify the blueprint:
|
|
501
|
+
```bash
|
|
502
|
+
bunx bunderstack blueprint
|
|
503
|
+
bunx bunderstack blueprint --check
|
|
504
|
+
```
|
|
505
|
+
3. Commit `bunderstack.blueprint.yaml` to git.
|
|
506
|
+
|
|
507
|
+
### Readiness Endpoint (`/api/readiness`)
|
|
508
|
+
|
|
509
|
+
Bunderhost monitors application deployment status via `GET /api/readiness`, which checks database connectivity, applied migrations, and queue backlog.
|
|
510
|
+
|
|
511
|
+
---
|
|
512
|
+
|
|
513
|
+
## 8. LLM Documentation & Bunderhost MCP Integration
|
|
514
|
+
|
|
515
|
+
### Official Bunderstack Documentation for LLMs
|
|
516
|
+
|
|
517
|
+
When working on Bunderstack projects, consult the dedicated LLM references:
|
|
518
|
+
- **Web Documentation**: [https://bunderstack.kcrz.dev/docs](https://bunderstack.kcrz.dev/docs)
|
|
519
|
+
- **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
|
+
- **Complete LLM Knowledge Base (`llms-full.txt`)**: [https://bunderstack.kcrz.dev/docs/llms-full.txt](https://bunderstack.kcrz.dev/docs/llms-full.txt)
|
|
521
|
+
|
|
522
|
+
### Bunderhost MCP Server Integration
|
|
523
|
+
|
|
524
|
+
Bunderhost provides a Model Context Protocol (MCP) server that allows coding agents to inspect, manage, and deploy projects.
|
|
525
|
+
|
|
526
|
+
#### Connecting to Bunderhost MCP:
|
|
527
|
+
1. Generate an Agent Access Token in Bunderhost: **Organization → Agent Access → Issue Token**.
|
|
528
|
+
2. Connect your MCP client to `https://<bunderhost-host>/mcp` using the token as a `Bearer` credential.
|
|
529
|
+
|
|
530
|
+
#### Key MCP Tools:
|
|
531
|
+
- `list_projects`: List all projects in the organization.
|
|
532
|
+
- `get_project`: Retrieve project configuration, active deployments, and blueprint status.
|
|
533
|
+
- `get_project_readiness`: Check database reachability, migration state, and queue backlog.
|
|
534
|
+
- `create_setup_session`: Open a secure setup session for the user to configure sensitive environment variables in the dashboard.
|
|
535
|
+
- `deploy_project` / `deploy_revision`: Trigger a deployment (requires user confirmation).
|
|
536
|
+
- `get_deployment_logs`: Fetch build and deploy logs.
|
|
537
|
+
- `get_runtime_logs`: Stream runtime container logs.
|
|
538
|
+
|
|
539
|
+
#### Agent Safety Rules for Bunderhost:
|
|
540
|
+
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
|
+
2. **Mutations Require Confirmation**: Creating projects or deploying revisions require explicit user approval in the MCP client before execution.
|
|
542
|
+
|
|
543
|
+
---
|
|
544
|
+
|
|
545
|
+
## 9. Quick Reference & Common Mistakes
|
|
546
|
+
|
|
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
|
+
|