bunderstack 0.17.0-beta.8 → 0.17.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.
- package/dist/cli-skills.d.ts +16 -0
- package/dist/cli-skills.d.ts.map +1 -0
- package/dist/cli-skills.js +116 -0
- package/dist/cli-skills.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +36 -4
- package/dist/cli.js.map +1 -1
- package/llms.txt +360 -0
- package/package.json +3 -1
- package/skills/creating-bunderstack-apps/SKILL.md +46 -0
- package/skills/creating-bunderstack-apps/agents/openai.yaml +4 -0
- package/skills/creating-bunderstack-apps/references/application-structure.md +197 -0
- package/skills/creating-bunderstack-apps/references/runtime-integrations.md +68 -0
- package/skills/creating-bunderstack-apps/references/verification.md +25 -0
- package/skills/migrating-to-bunderstack/SKILL.md +84 -0
- package/skills/migrating-to-bunderstack/agents/openai.yaml +4 -0
- package/skills/migrating-to-bunderstack/references/audit-checklist.md +57 -0
- package/skills/migrating-to-bunderstack/references/runtime-replacements.md +286 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# Application structure
|
|
2
|
+
|
|
3
|
+
## Keep the entry declarative
|
|
4
|
+
|
|
5
|
+
The Bunderstack entry constructs and exports `app` (and usually `type App =
|
|
6
|
+
typeof app`), configures the declared capabilities, and calls `provision(app)`
|
|
7
|
+
when the application owns provisioning. Keep unrelated external side effects
|
|
8
|
+
out of its import graph: the blueprint command imports this entry with
|
|
9
|
+
`BUNDERSTACK_INTROSPECT=1`.
|
|
10
|
+
|
|
11
|
+
Start a small API in `src/bunderstack.ts`. Split a meaningful configuration
|
|
12
|
+
into `src/bunderstack/` with `index.ts`, `schema/`, `access.ts`, `auth.ts`,
|
|
13
|
+
`env.ts`, `jobs/`, and `api/` as needed. The entry remains the one place that
|
|
14
|
+
assembles those modules into `createBunderstack()`; do not create parallel app,
|
|
15
|
+
database, or auth instances.
|
|
16
|
+
|
|
17
|
+
## Aggregate the schema
|
|
18
|
+
|
|
19
|
+
Export every domain, Better Auth, plugin, and Bunderstack internal table from
|
|
20
|
+
the schema object passed to `createBunderstack()`. Include
|
|
21
|
+
`export * from 'bunderstack/schema'` so migrations include the internal tables.
|
|
22
|
+
Define Better Auth tables required by the selected auth flows and plugins; do
|
|
23
|
+
not assume a minimal auth configuration needs every optional provider table.
|
|
24
|
+
|
|
25
|
+
## Define authorization on the server
|
|
26
|
+
|
|
27
|
+
Use `defineAccess(schema, rules)` for generated CRUD. Give each exposed table
|
|
28
|
+
an explicit operation policy and set `ownerColumn` for owner rules. Keep auth,
|
|
29
|
+
internal, and administrative tables out of generated CRUD unless deliberately
|
|
30
|
+
exposing the supported `user` table.
|
|
31
|
+
|
|
32
|
+
Use `scope.read` and `scope.write` to enforce tenant columns on generated
|
|
33
|
+
lists, reads, and writes. For example, an organization-owned table can derive
|
|
34
|
+
`{ organizationId: ctx.session?.activeOrganizationId ?? '__none__' }`, keeping
|
|
35
|
+
users without an active organization outside tenant rows. Use protected oRPC
|
|
36
|
+
procedures built from `o.protected` when authorization depends on related rows
|
|
37
|
+
or roles; hiding a UI route is not authorization.
|
|
38
|
+
|
|
39
|
+
## Extend the one API graph
|
|
40
|
+
|
|
41
|
+
Declare the builder once at module scope with `defineApi({ schema, env })`. It
|
|
42
|
+
infers the schema and env types from the values, so no application writes
|
|
43
|
+
`BunderstackApiBuilder<...>` by hand, and it reads nothing at runtime.
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
// src/bunderstack/api/base.ts
|
|
47
|
+
export const o = defineApi({ schema, env: envSchema })
|
|
48
|
+
export const publicProcedure = o.public
|
|
49
|
+
export const protectedProcedure = o.protected
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Router modules are plain objects that import the base they need, and the entry
|
|
53
|
+
passes the finished router as `api`:
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
// src/bunderstack/api/projects.ts
|
|
57
|
+
export const projectsRouter = {
|
|
58
|
+
stats: protectedProcedure.input(...).handler(...),
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/bunderstack/api/index.ts
|
|
62
|
+
export const api = { projects: projectsRouter }
|
|
63
|
+
|
|
64
|
+
// entry
|
|
65
|
+
createBunderstack({ schema, database, api })
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Do not write a router factory that receives a bag of procedures. That shape
|
|
69
|
+
only existed because `api` used to be a callback. The callback form,
|
|
70
|
+
`api: (o) => ({ ... })`, still works for a router that must be built from the
|
|
71
|
+
framework builder at configuration time.
|
|
72
|
+
|
|
73
|
+
Use `o.public`, `o.protected`, or `o.webhook`; add `.route(...)` only when a
|
|
74
|
+
stable HTTP projection is useful. Generated CRUD, custom procedures, files,
|
|
75
|
+
health, and `realtime.changes` remain in the same typed oRPC graph and client.
|
|
76
|
+
|
|
77
|
+
## Give a group of procedures its own base
|
|
78
|
+
|
|
79
|
+
A base is an oRPC builder, so `.use()` produces another one. Declare a rule
|
|
80
|
+
once instead of repeating it in every handler that depends on it:
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
export const adminProcedure = o.protected.use(async ({ context, next, errors }) => {
|
|
84
|
+
if (context.user.role !== 'admin') {
|
|
85
|
+
throw errors.FORBIDDEN({ message: 'Admin access required' })
|
|
86
|
+
}
|
|
87
|
+
return next()
|
|
88
|
+
})
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`next({ context })` merges into the context and types it for everything
|
|
92
|
+
downstream, which is how an organization scope or a resolved tenant reaches
|
|
93
|
+
handlers without an argument.
|
|
94
|
+
|
|
95
|
+
## Instrument the whole graph, not one base
|
|
96
|
+
|
|
97
|
+
Bunderstack builds the CRUD, storage, and realtime procedures itself, so they
|
|
98
|
+
never pass through a base the application declares. A middleware attached to
|
|
99
|
+
`o.protected` therefore measures the application's own procedures and leaves
|
|
100
|
+
the generated CRUD — usually the larger share of traffic — unmeasured.
|
|
101
|
+
|
|
102
|
+
Register cross-cutting middleware in the configuration instead:
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
const instrumentation = o.middleware(async ({ context, next, path }) => {
|
|
106
|
+
if (path[0] === 'realtime') return next()
|
|
107
|
+
const startedAt = performance.now()
|
|
108
|
+
try {
|
|
109
|
+
return await next()
|
|
110
|
+
} finally {
|
|
111
|
+
record(path.join('.'), performance.now() - startedAt, context.peekSession()?.user?.id)
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
createBunderstack({ schema, database, middleware: [instrumentation], api })
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Three rules apply to a graph-wide middleware. It runs before authentication, so
|
|
119
|
+
`context.user` does not exist there. Read the caller with
|
|
120
|
+
`context.peekSession()` after `await next()`, never `getSession()`: forcing the
|
|
121
|
+
session makes every request pay for authentication, including signed webhooks
|
|
122
|
+
that never needed it, and `peekSession()` is for observability only, never for
|
|
123
|
+
authorization. A realtime subscription is one long-lived call, so code after
|
|
124
|
+
`await next()` runs when the stream closes, not when it starts.
|
|
125
|
+
|
|
126
|
+
## Raise declared errors
|
|
127
|
+
|
|
128
|
+
Every procedure carries one error map: `BAD_REQUEST`, `UNAUTHORIZED`,
|
|
129
|
+
`FORBIDDEN`, `NOT_FOUND`, `CONFLICT`, `PAYLOAD_TOO_LARGE`,
|
|
130
|
+
`TOO_MANY_REQUESTS`. Inside a handler or middleware, raise from the `errors`
|
|
131
|
+
argument, with extra context in `data.details`:
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
throw errors.NOT_FOUND({ message: 'Project not found' })
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Code outside a handler — a service function or a job — has no `errors`
|
|
138
|
+
argument. Throw `BunderstackError`, which the framework maps to the same typed
|
|
139
|
+
error. Do not construct `ORPCError` by hand anywhere.
|
|
140
|
+
|
|
141
|
+
## Give list endpoints the shared contract
|
|
142
|
+
|
|
143
|
+
`listSpec(table, options)` gives a procedure you write the same filter, sort,
|
|
144
|
+
cursor, and count contract the generated CRUD list uses. Apply both parts to
|
|
145
|
+
your own base, which is what preserves the row type to the client:
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
const logsList = listSpec(appLogs, { filterable: ['level'], sortable: ['createdAt'] })
|
|
149
|
+
logs: adminProcedure.input(logsList.input).handler(logsList.handler)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
It reads no `access` configuration; the base procedure carries the policy.
|
|
153
|
+
|
|
154
|
+
## Type helpers that take the database
|
|
155
|
+
|
|
156
|
+
A service module cannot reach `typeof app.db` without an import cycle back to
|
|
157
|
+
the entry. Use `BunderstackDb<typeof schema>` and `BunderstackTx<typeof schema>`
|
|
158
|
+
instead of `any`.
|
|
159
|
+
|
|
160
|
+
## Keep the entry out of its own import graph
|
|
161
|
+
|
|
162
|
+
The api router and everything it imports are evaluated when the entry is
|
|
163
|
+
imported. A module in that graph that imports the app back — a bot client
|
|
164
|
+
reading `db`, a logger reading `app.env` — closes a cycle and breaks module
|
|
165
|
+
initialization. Load the app lazily in such a module.
|
|
166
|
+
|
|
167
|
+
## Keep credentials environment-owned
|
|
168
|
+
|
|
169
|
+
Declare validated server and browser-safe client variables in `env`. Server
|
|
170
|
+
variables must not use the `PUBLIC_` prefix; client variables must use it.
|
|
171
|
+
Use `app.env` (and `ctx.env`) rather than duplicating unchecked configuration.
|
|
172
|
+
Commit `.env.example` with names and safe placeholders only. Keep production
|
|
173
|
+
secrets, database URLs, storage credentials, and auth secrets in the runtime
|
|
174
|
+
environment.
|
|
175
|
+
|
|
176
|
+
Use local libSQL storage and console email for development when appropriate;
|
|
177
|
+
declare production adapters and credentials through configuration and runtime
|
|
178
|
+
environment rather than hard-coding them.
|
|
179
|
+
|
|
180
|
+
## Publish direct writes
|
|
181
|
+
|
|
182
|
+
Generated CRUD publishes realtime changes automatically. For a write through
|
|
183
|
+
`app.db` or `ctx.db`, return the complete changed row, wait for its transaction
|
|
184
|
+
to commit, then publish it with the table object:
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
const [task] = await ctx.db
|
|
188
|
+
.update(schema.tasks)
|
|
189
|
+
.set({ status: 'done' })
|
|
190
|
+
.where(eq(schema.tasks.id, taskId))
|
|
191
|
+
.returning()
|
|
192
|
+
|
|
193
|
+
await ctx.realtime.publish(schema.tasks, 'update', task)
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Do not publish from inside an enclosing transaction. The complete row lets the
|
|
197
|
+
existing access filter evaluate owner and read-scope columns.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Runtime integrations
|
|
2
|
+
|
|
3
|
+
`app.handler` is the single Web Standard `Request -> Response` integration
|
|
4
|
+
point. Mount it once; do not recreate routing, auth, or database layers in a
|
|
5
|
+
framework adapter.
|
|
6
|
+
|
|
7
|
+
## TanStack Start
|
|
8
|
+
|
|
9
|
+
TanStack Start owns the web process. Use `bunderstackStart<App>()` for the
|
|
10
|
+
client and mount the catch-all route with `createApiHandlers(app)`:
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
export const Route = createFileRoute('/api/$')({
|
|
14
|
+
server: { handlers: createApiHandlers(app) },
|
|
15
|
+
})
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Keep the client setup in `src/api.ts`, not `src/client.ts`, which is a reserved
|
|
19
|
+
Start entry point. Import `App` as a type so the browser does not load server
|
|
20
|
+
runtime code.
|
|
21
|
+
|
|
22
|
+
## Standalone Bun and other runtimes
|
|
23
|
+
|
|
24
|
+
For a standalone server, pass the handler directly:
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
Bun.serve({ fetch: app.handler })
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Other server frameworks must adapt their request and response objects to the
|
|
31
|
+
Web Standard pair, then delegate to `app.handler`. Astro adapters therefore
|
|
32
|
+
convert to and from Web Standard requests and responses. A browser-only React
|
|
33
|
+
SPA has no server request handler: run a separate Bun API process and point the
|
|
34
|
+
frontend's API base URL at that process.
|
|
35
|
+
|
|
36
|
+
## Background runtime
|
|
37
|
+
|
|
38
|
+
Declare queue jobs with `jobs: (j) => j.define(...)`, then run them in a
|
|
39
|
+
separate production process:
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
import { app } from './bunderstack'
|
|
43
|
+
|
|
44
|
+
await app.runWorker()
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Do not start a production worker or cron scheduler from the web entry.
|
|
48
|
+
`j.cron()` is delivered by the platform over authenticated HTTP. Queue handlers
|
|
49
|
+
are at-least-once, so make them idempotent and declare input validation and
|
|
50
|
+
retries.
|
|
51
|
+
|
|
52
|
+
If workers publish realtime events, configure the same shared Redis transport
|
|
53
|
+
for web and worker processes. `realtime: true` alone is process-local and is
|
|
54
|
+
only safe when the worker is embedded with `app.startWorker()` for local work.
|
|
55
|
+
|
|
56
|
+
## Realtime and synced collections
|
|
57
|
+
|
|
58
|
+
Clients consume the typed `realtime.changes` async iterator. Idle HTTP streams
|
|
59
|
+
receive a transport-only `heartbeat` every five seconds; the Bunderstack query
|
|
60
|
+
client filters it before cache callbacks and does not advance the Publisher
|
|
61
|
+
resume ID. Do not add an application polling loop or publish heartbeat events
|
|
62
|
+
through oRPC Publisher.
|
|
63
|
+
|
|
64
|
+
`bunderstack-sync` reconciles successful mutations from the canonical row
|
|
65
|
+
returned by generated CRUD, without a follow-up list refetch. Realtime echoes
|
|
66
|
+
are idempotent, and reconnect performs the full refetch used to repair drift.
|
|
67
|
+
Keep custom replacement procedures compatible by returning the complete row,
|
|
68
|
+
including `id`.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Verification contract
|
|
2
|
+
|
|
3
|
+
Run these gates from the application root after changing dependencies,
|
|
4
|
+
configuration, or application code:
|
|
5
|
+
|
|
6
|
+
```sh
|
|
7
|
+
bun install
|
|
8
|
+
bun test
|
|
9
|
+
bun run typecheck
|
|
10
|
+
bun run build
|
|
11
|
+
bun run blueprint
|
|
12
|
+
bun run blueprint:check
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
`bun run blueprint` generates the committed `bunderstack.blueprint.yaml` from
|
|
16
|
+
the configured Bunderstack entry. Set `package.json#bunderstack.entry` when the
|
|
17
|
+
entry is not `src/bunderstack.ts`. `bun run blueprint:check` must pass in CI so
|
|
18
|
+
the committed declaration matches the application.
|
|
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.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: migrating-to-bunderstack
|
|
3
|
+
description: Use when moving an existing or partially migrated application onto Bunderstack and replacing its separate auth, database, API, storage, email, jobs, cron, or realtime infrastructure, or when finishing a migration that stalled part-way.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Migrating to Bunderstack
|
|
7
|
+
|
|
8
|
+
Migration deletes infrastructure rather than wrapping it, and the application
|
|
9
|
+
stays working at every phase. For a new application with no existing
|
|
10
|
+
infrastructure, use `creating-bunderstack-apps` instead.
|
|
11
|
+
|
|
12
|
+
## Workflow
|
|
13
|
+
|
|
14
|
+
1. Inventory current auth, database, API, storage, email, jobs, cron, realtime,
|
|
15
|
+
env, migrations, and deployment ownership. Record which module owns each
|
|
16
|
+
capability today and which call sites depend on it.
|
|
17
|
+
2. Add a migration contract test before removing legacy paths, so every
|
|
18
|
+
deletion has a gate that fails when behaviour is lost.
|
|
19
|
+
3. Establish one Bunderstack app and one schema aggregate.
|
|
20
|
+
4. Move auth and access without creating duplicate instances.
|
|
21
|
+
5. Replace infrastructure capability by capability.
|
|
22
|
+
6. Mount one handler and separate the production worker.
|
|
23
|
+
7. Remove wrappers only after call sites and tests move.
|
|
24
|
+
8. Generate migrations and the blueprint, then verify production topology.
|
|
25
|
+
|
|
26
|
+
## One live instance per capability
|
|
27
|
+
|
|
28
|
+
The most damaging migration state is two working implementations of the same
|
|
29
|
+
capability. A second Better Auth instance splits session validation. A second
|
|
30
|
+
database client sits outside provisioning, migration state, and request
|
|
31
|
+
transactions. A second env schema drifts from the validated one and passes
|
|
32
|
+
locally while failing at boot.
|
|
33
|
+
|
|
34
|
+
Pass `authConfig` into `createBunderstack()`, re-export `app.auth` and `app.db`
|
|
35
|
+
from the entry, and let the `env` passed to `createBunderstack()` be the only
|
|
36
|
+
validated source. A more specific file route also shadows the catch-all, so a
|
|
37
|
+
surviving `/api/auth/$` silently keeps serving the instance you meant to delete.
|
|
38
|
+
|
|
39
|
+
## Replacements
|
|
40
|
+
|
|
41
|
+
| Legacy shape | Current contract |
|
|
42
|
+
| ------------------------------------------------------------- | --------------------------------------------------------------------------------- |
|
|
43
|
+
| Hand-written `ALL: ({ request }) => app.handler(request)` map | `createApiHandlers(app)` on one `/api/$` route |
|
|
44
|
+
| Separate `/api/auth/$` or `/api/trpc/$` mounts | Deleted; the catch-all serves Better Auth and the unified oRPC `/api/rpc/*` graph |
|
|
45
|
+
| tRPC router and procedure clients | Router modules over `defineApi` bases, passed as `api`, with one inferred client |
|
|
46
|
+
| Per-file router factories taking a bag of procedures | Bases exported from one module, imported by plain router objects |
|
|
47
|
+
| Hand-built `ORPCError` or a second error model | `errors.CODE({ message })`, or `BunderstackError` outside a handler |
|
|
48
|
+
| Tracing attached to an application base | `middleware: [...]`, which also covers the generated CRUD |
|
|
49
|
+
| Hand-rolled limit/offset/count blocks | `listSpec(table, options)` applied to your own base |
|
|
50
|
+
| `any`-typed db parameters in helpers | `BunderstackDb<typeof schema>` and `BunderstackTx<typeof schema>` |
|
|
51
|
+
| Worker started from the web entry | `src/worker.ts` owning `app.runWorker()` |
|
|
52
|
+
| `/api/cron/*` guarded by a shared secret | `jobs.cron()` with platform delivery |
|
|
53
|
+
| Channel-and-payload realtime publishing | `ctx.realtime.publish(schema.tasks, 'update', row)` after the write commits |
|
|
54
|
+
| AWS or Tigris SDK wrapper | `app.storage` buckets |
|
|
55
|
+
| Resend SDK wrapper | `app.email.send(...)` |
|
|
56
|
+
| `createEnv()` beside the app | `env` passed to `createBunderstack()` |
|
|
57
|
+
| Implicit database driver | Explicit adapter, `database: { adapter: libsql(), url }` |
|
|
58
|
+
| Schema push in production | Committed Drizzle `migrations/`, applied by `provision(app)` |
|
|
59
|
+
| Undeclared deployment | `package.json#bunderstack.entry` and a checked blueprint |
|
|
60
|
+
|
|
61
|
+
Read [runtime replacements](references/runtime-replacements.md) before writing
|
|
62
|
+
any replacement above; it holds the current snippets and the realtime transport
|
|
63
|
+
rule for multi-process deployments. Read the
|
|
64
|
+
[audit checklist](references/audit-checklist.md) during phase 1 to inventory
|
|
65
|
+
ownership, and again at phase 7 before each deletion.
|
|
66
|
+
|
|
67
|
+
## Deletion gate
|
|
68
|
+
|
|
69
|
+
Do not delete a legacy module until its replacement is mounted, every call site
|
|
70
|
+
imports the replacement, a migration contract test covers the behaviour, and
|
|
71
|
+
`bun run typecheck` reports no remaining importers. A one-release re-export
|
|
72
|
+
shim is acceptable when call sites are numerous; the shim is deleted under this
|
|
73
|
+
same gate. Uninstall the replaced SDK package in the commit that removes its
|
|
74
|
+
last importer, so a stale wrapper cannot be reintroduced silently.
|
|
75
|
+
|
|
76
|
+
Tests and scripts that construct their own app instance must call
|
|
77
|
+
`app.close()`.
|
|
78
|
+
|
|
79
|
+
## Production gate
|
|
80
|
+
|
|
81
|
+
Before the cutover deploy: committed migrations exist, the worker runs as its
|
|
82
|
+
own process, web and worker share a realtime transport if jobs publish events,
|
|
83
|
+
`package.json#bunderstack.entry` points at the entry, and
|
|
84
|
+
`bun run blueprint:check` passes in CI.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Audit checklist
|
|
2
|
+
|
|
3
|
+
Use this table at phase 1 to record who owns each capability today, and again
|
|
4
|
+
at phase 7 before deleting anything. Fill the evidence column with a command
|
|
5
|
+
output or a file reference, not an assertion.
|
|
6
|
+
|
|
7
|
+
| Capability | Legacy shape to find | Authoritative replacement | Evidence that the move is done | Deletion gate |
|
|
8
|
+
| ---------------------- | ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
|
|
9
|
+
| Auth instance | A second `betterAuth({...})` call, a custom session resolver, a patched `getSession` | `authConfig` passed to `createBunderstack()`; consumers import `app.auth` | One `betterAuth` construction in the repository; a protected route rejects an unauthenticated request | No importer of the legacy auth module remains |
|
|
10
|
+
| Auth schema | Auth tables generated into a legacy schema directory | Auth tables in the one schema aggregate | Generated migration includes `user`, `session`, `account`, `verification` | Legacy schema directory has no importers |
|
|
11
|
+
| Database client | A module constructing its own libSQL/Postgres client | `app.db`, re-exported from the entry | The entry is the only place calling the adapter factory | Legacy `db` module deleted or reduced to a re-export, then deleted |
|
|
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
|
+
| 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
|
+
| 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 `app.manifest.background.jobs` | No queue library importer; package uninstalled |
|
|
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
|
+
| 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
|
+
| 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
|
+
| 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 |
|
|
21
|
+
| Env | `createEnv()` beside the app, `dotenv`, unchecked `process.env` reads | `env` passed to `createBunderstack()`; `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
|
+
| 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
|
+
| 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 |
|
|
24
|
+
| Errors | `new ORPCError(...)` at call sites, or a second error model | `errors.CODE({ message })`; `BunderstackError` outside a handler | A failing request answers the declared status, not 500 | `ORPCError` import gone from the api layer |
|
|
25
|
+
| List endpoints | Hand-rolled limit/offset/filter/count blocks repeated per table | `listSpec(table, options)` applied to your own base | The endpoint accepts cursor and `count: true` and answers `ListResult` | Duplicated paging helpers deleted |
|
|
26
|
+
| Migrations | Schema push against production | Committed Drizzle `migrations/`, applied by `provision(app)` | `migrations/` is under version control and applies cleanly to an empty database | Push command removed from deployment |
|
|
27
|
+
| Deployment declaration | No `bunderstack.entry`, no blueprint | `package.json#bunderstack.entry` and a committed blueprint | `bun run blueprint:check` passes in CI | Deployment reads the blueprint rather than ad-hoc process config |
|
|
28
|
+
| App lifetime | Tests and scripts leaking app instances | `app.close()` in a `finally` or `afterEach` | The test run exits without hanging | — |
|
|
29
|
+
|
|
30
|
+
## Reading a partially migrated application
|
|
31
|
+
|
|
32
|
+
Some applications are already on Bunderstack for part of their surface. The
|
|
33
|
+
audit is the same, but the answer to "who owns this capability" is often _both_,
|
|
34
|
+
which is the state that causes production incidents.
|
|
35
|
+
|
|
36
|
+
A real example: an application whose Bunderstack modules live in
|
|
37
|
+
`src/bunderstack/` while `src/lib/` still holds an auth config, a schema
|
|
38
|
+
directory, and SDK wrappers, whose `package.json` has no `bunderstack.entry`,
|
|
39
|
+
no worker command, and no blueprint scripts, and whose deployment runs a single
|
|
40
|
+
web command. Nothing there is broken in development. In production it means the
|
|
41
|
+
declared entry is unknown to the host, queue jobs run inside web replicas or
|
|
42
|
+
not at all, and two auth configurations disagree about sessions.
|
|
43
|
+
|
|
44
|
+
Treat that shape as a sequence of the gates above, not as a file list to copy.
|
|
45
|
+
The directory names in any one application are incidental; the ownership
|
|
46
|
+
question is not.
|
|
47
|
+
|
|
48
|
+
## Order that keeps the application working
|
|
49
|
+
|
|
50
|
+
Auth and database first, because everything else reads them. Then access and
|
|
51
|
+
API mounting, so authorization is enforced in one place before routes move.
|
|
52
|
+
Then storage, email, jobs, and cron, which are independent of each other. Then
|
|
53
|
+
realtime, which depends on the write paths already being correct. Migrations
|
|
54
|
+
and the blueprint last, because they declare the finished shape.
|
|
55
|
+
|
|
56
|
+
Deleting in the reverse order of adoption keeps the system reversible: the
|
|
57
|
+
replacement is live and tested before its predecessor stops existing.
|