bunbase 0.0.9 → 1.0.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.
- package/README.md +700 -7
- package/package.json +15 -34
- package/LICENSE +0 -21
- package/dist/cli/index.js +0 -1530
- package/dist/index.d.ts +0 -543
- package/dist/index.js +0 -164
- package/dist/shared/chunk-k195ahh5.js +0 -79
package/README.md
CHANGED
|
@@ -1,25 +1,718 @@
|
|
|
1
|
-
#
|
|
1
|
+
# bunbase
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A type-safe, batteries-included backend framework for [Bun](https://bun.sh) that makes building APIs delightful.
|
|
4
|
+
|
|
5
|
+
## Why Bunbase?
|
|
6
|
+
|
|
7
|
+
- **🎯 Type-safe by default** - Full end-to-end type safety with TypeBox schemas
|
|
8
|
+
- **⚡ Built for Bun** - Leverages Bun's native performance and APIs
|
|
9
|
+
- **🔒 Authorization first** - Composable guards with RBAC and multi-tenancy built-in
|
|
10
|
+
- **🚀 Zero boilerplate** - Define actions, not routes. File-based discovery just works.
|
|
11
|
+
- **🔄 Job queue & scheduler** - Postgres-backed queue with cron support included
|
|
12
|
+
- **📡 Multiple triggers** - API, events, cron, webhooks, and MCP tools from one action
|
|
13
|
+
- **🎨 HTTP field mapping** - Route fields to body, headers, query, cookies, path automatically
|
|
14
|
+
- **📊 Built-in observability** - Action logs, runs tracking, and Studio dashboard
|
|
15
|
+
- **🔌 Optional Redis** - Drop-in Redis support for KV store and distributed rate limiting
|
|
4
16
|
|
|
5
17
|
## Installation
|
|
6
18
|
|
|
7
19
|
```bash
|
|
8
|
-
bun add
|
|
20
|
+
bun add bunbase
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
### 1. Initialize a new project
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
bunbase init my-app
|
|
29
|
+
cd my-app
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### 2. Configure your database
|
|
33
|
+
|
|
34
|
+
Create a `bunbase.config.ts`:
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
import { defineConfig } from 'bunbase'
|
|
38
|
+
|
|
39
|
+
export default defineConfig({
|
|
40
|
+
db: {
|
|
41
|
+
url: process.env.DATABASE_URL || 'postgresql://localhost:5432/myapp',
|
|
42
|
+
},
|
|
43
|
+
server: {
|
|
44
|
+
port: 3000,
|
|
45
|
+
},
|
|
46
|
+
})
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### 3. Create your first action
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
// src/tasks/create-task.action.ts
|
|
53
|
+
import { action, t, triggers } from 'bunbase'
|
|
54
|
+
|
|
55
|
+
export const createTask = action({
|
|
56
|
+
name: 'create-task',
|
|
57
|
+
description: 'Create a new task',
|
|
58
|
+
input: t.Object({
|
|
59
|
+
title: t.String({ minLength: 1, maxLength: 200 }),
|
|
60
|
+
description: t.Optional(t.String()),
|
|
61
|
+
}),
|
|
62
|
+
output: t.Object({
|
|
63
|
+
id: t.String(),
|
|
64
|
+
title: t.String(),
|
|
65
|
+
createdAt: t.String(),
|
|
66
|
+
}),
|
|
67
|
+
triggers: [triggers.api('POST', '/tasks')],
|
|
68
|
+
}, async ({ input, ctx }) => {
|
|
69
|
+
const task = await ctx.db
|
|
70
|
+
.from('tasks')
|
|
71
|
+
.insert({ title: input.title, description: input.description })
|
|
72
|
+
.returning('id', 'title', 'created_at')
|
|
73
|
+
.single()
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
id: task.id,
|
|
77
|
+
title: task.title,
|
|
78
|
+
createdAt: task.created_at,
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### 4. Run your server
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
bunbase dev
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Your API is now live at `http://localhost:3000/tasks`! 🎉
|
|
90
|
+
|
|
91
|
+
## Core Concepts
|
|
92
|
+
|
|
93
|
+
### Actions
|
|
94
|
+
|
|
95
|
+
Actions are the fundamental building blocks of Bunbase. They are reusable, validated functions that represent atomic units of work.
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
import { action, t, triggers, guards } from 'bunbase'
|
|
99
|
+
|
|
100
|
+
export const updateProfile = action({
|
|
101
|
+
name: 'update-profile',
|
|
102
|
+
description: 'Update user profile',
|
|
103
|
+
|
|
104
|
+
// TypeBox input schema with validation
|
|
105
|
+
input: t.Object({
|
|
106
|
+
name: t.String({ minLength: 2 }),
|
|
107
|
+
bio: t.Optional(t.String({ maxLength: 500 })),
|
|
108
|
+
}),
|
|
109
|
+
|
|
110
|
+
// TypeBox output schema
|
|
111
|
+
output: t.Object({
|
|
112
|
+
id: t.String(),
|
|
113
|
+
name: t.String(),
|
|
114
|
+
bio: t.Union([t.String(), t.Null()]),
|
|
115
|
+
}),
|
|
116
|
+
|
|
117
|
+
// How this action can be invoked
|
|
118
|
+
triggers: [
|
|
119
|
+
triggers.api('PATCH', '/profile'),
|
|
120
|
+
],
|
|
121
|
+
|
|
122
|
+
// Authorization checks
|
|
123
|
+
guards: [guards.authenticated()],
|
|
124
|
+
|
|
125
|
+
// Optional retry configuration
|
|
126
|
+
retry: {
|
|
127
|
+
maxAttempts: 3,
|
|
128
|
+
backoff: 'exponential',
|
|
129
|
+
backoffMs: 1000,
|
|
130
|
+
},
|
|
131
|
+
}, async ({ input, ctx }) => {
|
|
132
|
+
// ctx provides: db, logger, auth, event, queue, scheduler
|
|
133
|
+
const user = await ctx.db
|
|
134
|
+
.from('users')
|
|
135
|
+
.eq('id', ctx.auth.userId)
|
|
136
|
+
.update({ name: input.name, bio: input.bio })
|
|
137
|
+
.returning('id', 'name', 'bio')
|
|
138
|
+
.single()
|
|
139
|
+
|
|
140
|
+
return user
|
|
141
|
+
})
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Modules
|
|
145
|
+
|
|
146
|
+
Modules group related actions with shared configuration:
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
// src/billing/_module.ts
|
|
150
|
+
import { module, guards } from 'bunbase'
|
|
151
|
+
import { createSubscription } from './create-subscription.action.ts'
|
|
152
|
+
import { cancelSubscription } from './cancel-subscription.action.ts'
|
|
153
|
+
import { getInvoices } from './get-invoices.action.ts'
|
|
154
|
+
|
|
155
|
+
export default module({
|
|
156
|
+
name: 'billing',
|
|
157
|
+
description: 'Subscription and billing management',
|
|
158
|
+
apiPrefix: '/billing', // All action routes prefixed with this
|
|
159
|
+
guards: [guards.authenticated(), guards.hasFeature('billing')],
|
|
160
|
+
actions: [createSubscription, cancelSubscription, getInvoices],
|
|
161
|
+
})
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Triggers
|
|
165
|
+
|
|
166
|
+
Actions can be invoked through multiple triggers:
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
// API endpoint
|
|
170
|
+
triggers.api('POST', '/tasks')
|
|
171
|
+
|
|
172
|
+
// Scheduled cron job
|
|
173
|
+
triggers.cron('0 0 * * *', { timezone: 'America/New_York' })
|
|
174
|
+
|
|
175
|
+
// Event-driven
|
|
176
|
+
triggers.event('task.completed')
|
|
177
|
+
|
|
178
|
+
// Webhook with signature verification
|
|
179
|
+
triggers.webhook('/webhooks/stripe', {
|
|
180
|
+
verify: async (req) => verifyStripeSignature(req)
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
// MCP tool (for AI assistants)
|
|
184
|
+
triggers.mcp({
|
|
185
|
+
description: 'Create a new task',
|
|
186
|
+
parameters: { /* ... */ }
|
|
187
|
+
})
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Database
|
|
191
|
+
|
|
192
|
+
Bunbase includes a type-safe query builder with fluent API:
|
|
193
|
+
|
|
194
|
+
### Query Building
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
// Select with filters
|
|
198
|
+
const tasks = await ctx.db
|
|
199
|
+
.from('tasks')
|
|
200
|
+
.eq('status', 'active')
|
|
201
|
+
.gt('priority', 5)
|
|
202
|
+
.orderBy('created_at', 'desc')
|
|
203
|
+
.limit(10)
|
|
204
|
+
.exec()
|
|
205
|
+
|
|
206
|
+
// Single record
|
|
207
|
+
const user = await ctx.db
|
|
208
|
+
.from('users')
|
|
209
|
+
.eq('email', 'user@example.com')
|
|
210
|
+
.single() // Throws if not found
|
|
211
|
+
|
|
212
|
+
// Maybe single (returns null if not found)
|
|
213
|
+
const task = await ctx.db
|
|
214
|
+
.from('tasks')
|
|
215
|
+
.eq('id', taskId)
|
|
216
|
+
.maybeSingle()
|
|
217
|
+
|
|
218
|
+
// Insert
|
|
219
|
+
const newTask = await ctx.db
|
|
220
|
+
.from('tasks')
|
|
221
|
+
.insert({ title: 'New task', status: 'pending' })
|
|
222
|
+
.returning('id', 'title', 'created_at')
|
|
223
|
+
.single()
|
|
224
|
+
|
|
225
|
+
// Update
|
|
226
|
+
await ctx.db
|
|
227
|
+
.from('tasks')
|
|
228
|
+
.eq('id', taskId)
|
|
229
|
+
.update({ status: 'completed', completed_at: new Date().toISOString() })
|
|
230
|
+
.exec()
|
|
231
|
+
|
|
232
|
+
// Delete
|
|
233
|
+
await ctx.db
|
|
234
|
+
.from('tasks')
|
|
235
|
+
.eq('status', 'archived')
|
|
236
|
+
.delete()
|
|
237
|
+
.exec()
|
|
238
|
+
|
|
239
|
+
// Count
|
|
240
|
+
const { count } = await ctx.db
|
|
241
|
+
.from('tasks')
|
|
242
|
+
.eq('status', 'active')
|
|
243
|
+
.count()
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Type Generation
|
|
247
|
+
|
|
248
|
+
Generate TypeScript types from your PostgreSQL database:
|
|
249
|
+
|
|
250
|
+
```bash
|
|
251
|
+
bunbase typegen:db
|
|
9
252
|
```
|
|
10
253
|
|
|
11
|
-
|
|
254
|
+
This creates `.bunbase/database.d.ts` with Row/Insert/Update types for all tables. The types are automatically picked up by the database client via module augmentation.
|
|
12
255
|
|
|
13
256
|
```typescript
|
|
14
|
-
|
|
257
|
+
// Types are automatically inferred!
|
|
258
|
+
const user = await ctx.db.from('users').eq('id', userId).single()
|
|
259
|
+
// ^? { id: string; email: string; name: string; ... }
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
## Guards & Authorization
|
|
15
263
|
|
|
16
|
-
|
|
264
|
+
Guards are composable authorization functions that run before action handlers:
|
|
265
|
+
|
|
266
|
+
### Built-in Guards
|
|
267
|
+
|
|
268
|
+
```typescript
|
|
269
|
+
import { guards } from 'bunbase'
|
|
270
|
+
|
|
271
|
+
// Require authenticated user
|
|
272
|
+
guards.authenticated()
|
|
273
|
+
|
|
274
|
+
// Role-based access control
|
|
275
|
+
guards.hasRole('admin')
|
|
276
|
+
guards.hasPermission('tasks:delete')
|
|
277
|
+
|
|
278
|
+
// Multi-tenant SaaS
|
|
279
|
+
guards.inOrg() // User must be in an organization
|
|
280
|
+
guards.hasFeature('advanced-analytics') // Org must have feature
|
|
281
|
+
guards.trialActiveOrPaid() // Org must have active trial or paid plan
|
|
282
|
+
|
|
283
|
+
// Rate limiting
|
|
284
|
+
guards.rateLimit({
|
|
285
|
+
points: 100, // 100 requests
|
|
286
|
+
duration: 60000, // per 60 seconds
|
|
287
|
+
keyPrefix: 'api',
|
|
288
|
+
blockDuration: 300000, // Block for 5 minutes if exceeded
|
|
289
|
+
})
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### Custom Guards
|
|
293
|
+
|
|
294
|
+
```typescript
|
|
295
|
+
import { type GuardFn, GuardError } from 'bunbase'
|
|
296
|
+
|
|
297
|
+
function isTaskOwner(): GuardFn {
|
|
298
|
+
return async ({ input, ctx }) => {
|
|
299
|
+
const task = await ctx.db
|
|
300
|
+
.from('tasks')
|
|
301
|
+
.eq('id', input.taskId)
|
|
302
|
+
.single()
|
|
303
|
+
|
|
304
|
+
if (task.created_by !== ctx.auth.userId) {
|
|
305
|
+
throw new GuardError('Not authorized to access this task', 403)
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Use in action
|
|
311
|
+
export const deleteTask = action({
|
|
312
|
+
guards: [guards.authenticated(), isTaskOwner()],
|
|
313
|
+
// ...
|
|
314
|
+
})
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
## HTTP Field Mapping
|
|
318
|
+
|
|
319
|
+
Route fields to different HTTP locations automatically:
|
|
320
|
+
|
|
321
|
+
```typescript
|
|
322
|
+
import { action, t, triggers, http } from 'bunbase'
|
|
323
|
+
|
|
324
|
+
export const advancedLogin = action({
|
|
325
|
+
name: 'advanced-login',
|
|
326
|
+
input: t.Object({
|
|
327
|
+
// Regular fields go to JSON body
|
|
328
|
+
email: t.String({ format: 'email' }),
|
|
329
|
+
password: t.String(),
|
|
330
|
+
|
|
331
|
+
// Map to HTTP locations
|
|
332
|
+
apiKey: http.Header(t.String(), 'X-API-Key'),
|
|
333
|
+
remember: http.Query(t.Boolean()),
|
|
334
|
+
deviceId: http.Cookie(t.String()),
|
|
335
|
+
}),
|
|
336
|
+
output: t.Object({
|
|
337
|
+
user: t.Object({ id: t.String(), email: t.String() }),
|
|
338
|
+
token: t.String(),
|
|
339
|
+
|
|
340
|
+
// Response headers and cookies
|
|
341
|
+
userId: http.Header(t.String(), 'X-User-ID'),
|
|
342
|
+
refreshToken: http.Cookie(t.String(), 'refresh_token', {
|
|
343
|
+
httpOnly: true,
|
|
344
|
+
secure: true,
|
|
345
|
+
sameSite: 'strict',
|
|
346
|
+
maxAge: 7 * 24 * 60 * 60, // 7 days
|
|
347
|
+
}),
|
|
348
|
+
}),
|
|
349
|
+
triggers: [triggers.api('POST', '/auth/login')],
|
|
350
|
+
}, async ({ input, ctx }) => {
|
|
351
|
+
// All fields are available in input
|
|
352
|
+
// HTTP routing happens automatically
|
|
353
|
+
const user = await authenticateUser(input.email, input.password)
|
|
354
|
+
const token = generateToken(user.id)
|
|
355
|
+
|
|
356
|
+
return {
|
|
357
|
+
user: { id: user.id, email: user.email },
|
|
358
|
+
token,
|
|
359
|
+
userId: user.id, // Will be in X-User-ID response header
|
|
360
|
+
refreshToken: 'refresh_token_value', // Will be in Set-Cookie header
|
|
361
|
+
}
|
|
362
|
+
})
|
|
17
363
|
```
|
|
18
364
|
|
|
365
|
+
When using [@bunbase/react](../react), the client automatically handles all HTTP field routing based on your backend schema.
|
|
366
|
+
|
|
367
|
+
## Job Queue
|
|
368
|
+
|
|
369
|
+
Postgres-backed job queue with priorities, retries, and dead letter queue:
|
|
370
|
+
|
|
371
|
+
```typescript
|
|
372
|
+
// Push a job
|
|
373
|
+
await ctx.queue.push('send-email', {
|
|
374
|
+
to: 'user@example.com',
|
|
375
|
+
subject: 'Welcome!',
|
|
376
|
+
body: 'Thanks for signing up',
|
|
377
|
+
}, {
|
|
378
|
+
priority: 10, // Higher priority = processed first
|
|
379
|
+
delay: 5000, // Delay 5 seconds
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
// Define job handler
|
|
383
|
+
export const sendEmail = action({
|
|
384
|
+
name: 'send-email',
|
|
385
|
+
triggers: [triggers.job()],
|
|
386
|
+
input: t.Object({
|
|
387
|
+
to: t.String({ format: 'email' }),
|
|
388
|
+
subject: t.String(),
|
|
389
|
+
body: t.String(),
|
|
390
|
+
}),
|
|
391
|
+
output: t.Object({
|
|
392
|
+
messageId: t.String(),
|
|
393
|
+
}),
|
|
394
|
+
retry: {
|
|
395
|
+
maxAttempts: 5,
|
|
396
|
+
backoff: 'exponential',
|
|
397
|
+
backoffMs: 1000,
|
|
398
|
+
maxBackoffMs: 30000,
|
|
399
|
+
},
|
|
400
|
+
}, async ({ input, ctx }) => {
|
|
401
|
+
const messageId = await emailService.send(input)
|
|
402
|
+
return { messageId }
|
|
403
|
+
})
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
## Scheduler
|
|
407
|
+
|
|
408
|
+
Schedule actions to run at specific times or intervals:
|
|
409
|
+
|
|
410
|
+
```typescript
|
|
411
|
+
// Cron-based scheduling
|
|
412
|
+
export const dailyReport = action({
|
|
413
|
+
name: 'daily-report',
|
|
414
|
+
triggers: [
|
|
415
|
+
triggers.cron('0 9 * * *', { timezone: 'America/New_York' })
|
|
416
|
+
],
|
|
417
|
+
}, async ({ ctx }) => {
|
|
418
|
+
const report = await generateDailyReport()
|
|
419
|
+
await ctx.event.emit('report.generated', { report })
|
|
420
|
+
})
|
|
421
|
+
|
|
422
|
+
// One-time delayed execution
|
|
423
|
+
await ctx.scheduler.schedule(
|
|
424
|
+
'send-reminder',
|
|
425
|
+
{ taskId: task.id },
|
|
426
|
+
new Date(Date.now() + 24 * 60 * 60 * 1000) // 24 hours from now
|
|
427
|
+
)
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
## Event Bus
|
|
431
|
+
|
|
432
|
+
In-memory event emitter for action-to-action communication:
|
|
433
|
+
|
|
434
|
+
```typescript
|
|
435
|
+
// Emit event
|
|
436
|
+
await ctx.event.emit('task.created', {
|
|
437
|
+
taskId: task.id,
|
|
438
|
+
title: task.title,
|
|
439
|
+
createdBy: ctx.auth.userId,
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
// Listen for event
|
|
443
|
+
export const onTaskCreated = action({
|
|
444
|
+
name: 'on-task-created',
|
|
445
|
+
triggers: [triggers.event('task.created')],
|
|
446
|
+
input: t.Object({
|
|
447
|
+
taskId: t.String(),
|
|
448
|
+
title: t.String(),
|
|
449
|
+
createdBy: t.String(),
|
|
450
|
+
}),
|
|
451
|
+
}, async ({ input, ctx }) => {
|
|
452
|
+
// Send notifications, update analytics, etc.
|
|
453
|
+
await ctx.queue.push('send-notification', {
|
|
454
|
+
userId: input.createdBy,
|
|
455
|
+
message: `Task "${input.title}" created`,
|
|
456
|
+
})
|
|
457
|
+
})
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
## Redis Support (Optional)
|
|
461
|
+
|
|
462
|
+
Add Redis for high-performance KV operations and distributed rate limiting:
|
|
463
|
+
|
|
464
|
+
```typescript
|
|
465
|
+
// bunbase.config.ts
|
|
466
|
+
export default defineConfig({
|
|
467
|
+
redis: {
|
|
468
|
+
url: process.env.REDIS_URL || 'redis://localhost:6379',
|
|
469
|
+
connectionTimeout: 5000,
|
|
470
|
+
autoReconnect: true,
|
|
471
|
+
maxRetries: 10,
|
|
472
|
+
tls: false,
|
|
473
|
+
},
|
|
474
|
+
db: { /* ... */ },
|
|
475
|
+
})
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
When Redis is configured:
|
|
479
|
+
- **KV Store**: ~10-100x faster than Postgres for key-value operations
|
|
480
|
+
- **Rate Limiting**: Distributed rate limiting across multiple server instances
|
|
481
|
+
- **Auto-fallback**: Falls back to Postgres/in-memory if Redis unavailable
|
|
482
|
+
|
|
483
|
+
```typescript
|
|
484
|
+
// KV operations (uses Redis if configured, Postgres otherwise)
|
|
485
|
+
await ctx.kv.set('user:123:preferences', { theme: 'dark' })
|
|
486
|
+
const prefs = await ctx.kv.get('user:123:preferences')
|
|
487
|
+
await ctx.kv.delete('user:123:preferences')
|
|
488
|
+
|
|
489
|
+
// Rate limiting automatically uses Redis when available
|
|
490
|
+
guards.rateLimit({ points: 100, duration: 60000 })
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
## CLI Commands
|
|
494
|
+
|
|
495
|
+
```bash
|
|
496
|
+
# Initialize new project
|
|
497
|
+
bunbase init <project-name>
|
|
498
|
+
|
|
499
|
+
# Generate action or module
|
|
500
|
+
bunbase generate action <name>
|
|
501
|
+
bunbase generate module <name>
|
|
502
|
+
|
|
503
|
+
# Database migrations
|
|
504
|
+
bunbase migrate # Run pending migrations
|
|
505
|
+
bunbase migrate new <name> # Create new migration
|
|
506
|
+
|
|
507
|
+
# Type generation
|
|
508
|
+
bunbase typegen:db # PostgreSQL → TypeScript
|
|
509
|
+
bunbase typegen:react --url <backend-url> # Backend schema → React types
|
|
510
|
+
|
|
511
|
+
# Development server
|
|
512
|
+
bunbase dev
|
|
513
|
+
|
|
514
|
+
# Production build
|
|
515
|
+
bun run build
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
## Configuration
|
|
519
|
+
|
|
520
|
+
Complete `bunbase.config.ts` example:
|
|
521
|
+
|
|
522
|
+
```typescript
|
|
523
|
+
import { defineConfig } from 'bunbase'
|
|
524
|
+
|
|
525
|
+
export default defineConfig({
|
|
526
|
+
// Database configuration (required)
|
|
527
|
+
db: {
|
|
528
|
+
url: process.env.DATABASE_URL!,
|
|
529
|
+
},
|
|
530
|
+
|
|
531
|
+
// Server configuration
|
|
532
|
+
server: {
|
|
533
|
+
port: 3000,
|
|
534
|
+
hostname: '0.0.0.0',
|
|
535
|
+
cors: {
|
|
536
|
+
origin: ['http://localhost:3000', 'https://myapp.com'],
|
|
537
|
+
credentials: true,
|
|
538
|
+
},
|
|
539
|
+
},
|
|
540
|
+
|
|
541
|
+
// Redis configuration (optional)
|
|
542
|
+
redis: {
|
|
543
|
+
url: process.env.REDIS_URL,
|
|
544
|
+
connectionTimeout: 5000,
|
|
545
|
+
idleTimeout: 30000,
|
|
546
|
+
autoReconnect: true,
|
|
547
|
+
maxRetries: 10,
|
|
548
|
+
tls: false,
|
|
549
|
+
},
|
|
550
|
+
|
|
551
|
+
// Session configuration
|
|
552
|
+
session: {
|
|
553
|
+
secret: process.env.SESSION_SECRET!,
|
|
554
|
+
cookieName: 'my_session',
|
|
555
|
+
maxAge: 7 * 24 * 60 * 60, // 7 days
|
|
556
|
+
},
|
|
557
|
+
|
|
558
|
+
// Logging configuration
|
|
559
|
+
logging: {
|
|
560
|
+
level: 'info',
|
|
561
|
+
pretty: true,
|
|
562
|
+
},
|
|
563
|
+
|
|
564
|
+
// Write buffer for action logs/runs
|
|
565
|
+
writeBuffer: {
|
|
566
|
+
flushInterval: 2000, // Flush every 2 seconds
|
|
567
|
+
maxSize: 500, // Or when 500 entries buffered
|
|
568
|
+
},
|
|
569
|
+
|
|
570
|
+
// Queue configuration
|
|
571
|
+
queue: {
|
|
572
|
+
pollInterval: 1000, // Check for jobs every second
|
|
573
|
+
maxConcurrency: 10, // Process up to 10 jobs concurrently
|
|
574
|
+
},
|
|
575
|
+
})
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
## Studio Dashboard
|
|
579
|
+
|
|
580
|
+
Bunbase includes a built-in development dashboard at `http://localhost:3000/_studio`:
|
|
581
|
+
|
|
582
|
+
- 📊 View all registered actions and modules
|
|
583
|
+
- 🔍 Browse action execution logs and runs
|
|
584
|
+
- ⚡ Monitor performance metrics
|
|
585
|
+
- 🎯 Test actions directly from the UI
|
|
586
|
+
- 📈 Success rates and average duration stats
|
|
587
|
+
|
|
588
|
+
## React Integration
|
|
589
|
+
|
|
590
|
+
Generate fully-typed React client with automatic HTTP field routing:
|
|
591
|
+
|
|
592
|
+
```bash
|
|
593
|
+
bunbase typegen:react --url http://localhost:3000
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
```typescript
|
|
597
|
+
import { createBunbaseClient } from '@bunbase/react'
|
|
598
|
+
import type { BunbaseAPI } from './.bunbase/api'
|
|
599
|
+
import { bunbaseAPISchema } from './.bunbase/api'
|
|
600
|
+
|
|
601
|
+
export const bunbase = createBunbaseClient<BunbaseAPI>({
|
|
602
|
+
baseUrl: 'http://localhost:3000',
|
|
603
|
+
schema: bunbaseAPISchema, // Enables automatic field routing
|
|
604
|
+
})
|
|
605
|
+
|
|
606
|
+
// Use in components
|
|
607
|
+
function TaskList() {
|
|
608
|
+
const { data, isLoading } = bunbase.useQuery('list-tasks', {
|
|
609
|
+
status: 'active'
|
|
610
|
+
})
|
|
611
|
+
|
|
612
|
+
const createTask = bunbase.useMutation('create-task')
|
|
613
|
+
|
|
614
|
+
// Full type safety and automatic HTTP field routing!
|
|
615
|
+
}
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
See [@bunbase/react](../react) for complete documentation.
|
|
619
|
+
|
|
620
|
+
## Error Handling
|
|
621
|
+
|
|
622
|
+
```typescript
|
|
623
|
+
import { GuardError, NonRetriableError } from 'bunbase'
|
|
624
|
+
|
|
625
|
+
// Guard errors (401/403/429)
|
|
626
|
+
throw new GuardError('Not authorized', 403)
|
|
627
|
+
|
|
628
|
+
// Non-retriable errors (won't retry even if retry config exists)
|
|
629
|
+
throw new NonRetriableError('Invalid payment method')
|
|
630
|
+
|
|
631
|
+
// Regular errors (will retry based on action retry config)
|
|
632
|
+
throw new Error('External API timeout')
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
## Multi-Tenant SaaS
|
|
636
|
+
|
|
637
|
+
Built-in support for multi-tenant applications:
|
|
638
|
+
|
|
639
|
+
```typescript
|
|
640
|
+
import { guards } from 'bunbase'
|
|
641
|
+
|
|
642
|
+
// Ensure user is in an organization
|
|
643
|
+
guards.inOrg()
|
|
644
|
+
|
|
645
|
+
// Check organization has feature
|
|
646
|
+
guards.hasFeature('advanced-analytics')
|
|
647
|
+
|
|
648
|
+
// Check subscription status
|
|
649
|
+
guards.trialActiveOrPaid()
|
|
650
|
+
|
|
651
|
+
// Access current org in action
|
|
652
|
+
async ({ ctx }) => {
|
|
653
|
+
const orgId = ctx.auth.orgId
|
|
654
|
+
const org = await ctx.db.from('organizations').eq('id', orgId).single()
|
|
655
|
+
}
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
## Observability
|
|
659
|
+
|
|
660
|
+
Every action execution is automatically tracked:
|
|
661
|
+
|
|
662
|
+
```typescript
|
|
663
|
+
// Access action run info
|
|
664
|
+
async ({ ctx }) => {
|
|
665
|
+
ctx.logger.info('Processing task', { taskId: '123' })
|
|
666
|
+
|
|
667
|
+
// Current retry attempt (if using retry)
|
|
668
|
+
if (ctx.retry.attempt > 1) {
|
|
669
|
+
ctx.logger.warn(`Retry attempt ${ctx.retry.attempt}/${ctx.retry.maxAttempts}`)
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
Action logs and runs are stored in the database and visible in Studio.
|
|
675
|
+
|
|
676
|
+
## TypeScript
|
|
677
|
+
|
|
678
|
+
Bunbase is written in TypeScript with strict mode and provides:
|
|
679
|
+
|
|
680
|
+
- Full type inference for database queries
|
|
681
|
+
- TypeBox schemas for runtime validation and type generation
|
|
682
|
+
- Generated types from database schema
|
|
683
|
+
- End-to-end type safety with React client
|
|
684
|
+
|
|
685
|
+
## Examples
|
|
686
|
+
|
|
687
|
+
- [Basic Example](../../examples/basic) - Complete working app with all features
|
|
688
|
+
- [AMANTRA Control Panel](../../examples/amantra-cpanel) - Real-world SaaS application
|
|
689
|
+
|
|
690
|
+
## Architecture
|
|
691
|
+
|
|
692
|
+
Bunbase follows these principles:
|
|
693
|
+
|
|
694
|
+
- **Composition over inheritance** - Guards, triggers, and actions are composable functions
|
|
695
|
+
- **Registry pattern** - Single ActionRegistry holds all discovered actions
|
|
696
|
+
- **Builder pattern** - Fluent APIs for actions, modules, triggers, queries
|
|
697
|
+
- **Write buffering** - Batched writes to avoid database bombardment
|
|
698
|
+
- **Convention over configuration** - File-based discovery, sensible defaults
|
|
699
|
+
|
|
700
|
+
## Performance
|
|
701
|
+
|
|
702
|
+
- Built on Bun's native HTTP server and SQLite/PostgreSQL client
|
|
703
|
+
- Automatic request batching with write buffer
|
|
704
|
+
- Optional Redis for high-performance operations
|
|
705
|
+
- Efficient query builder with minimal overhead
|
|
706
|
+
- Zero-copy cookie parsing with Bun's CookieMap
|
|
707
|
+
|
|
19
708
|
## Contributing
|
|
20
709
|
|
|
21
|
-
|
|
710
|
+
Issues and PRs welcome! See [CONTRIBUTING.md](../../CONTRIBUTING.md).
|
|
22
711
|
|
|
23
712
|
## License
|
|
24
713
|
|
|
25
714
|
MIT
|
|
715
|
+
|
|
716
|
+
---
|
|
717
|
+
|
|
718
|
+
Built with ❤️ using [Bun](https://bun.sh)
|