File structure
src/plugins/{name}/
api/
plugin.ts ← defineBackendPlugin entry
getters.ts ← read-only DB functions (no HTTP context)
mutations.ts ← lower-level write DB functions
operations.ts ← validated operations, permission facts, lifecycle
index.ts ← re-exports getters + mutations + types
query-keys.ts ← React Query key factory
Rules
getters.ts— pure async DB functions only. No HTTP context, no lifecycle hooks. Always takesadapteras first arg.mutations.ts— lower-level write primitives. No HTTP context or lifecycle composition. Document that callers own validation and lifecycle.api/index.ts— re-export everything from getters + mutations for direct server-side import.operations.ts— define the one maintained business inventory with input validation, exact permission descriptors, authoritative facts, domain execution, and lifecycle hooks.- Bind HTTP routes to same-key operations. Use
operationRouteMaponly for real route-name mismatches. - The optional
rawfactory is narrow: first-party plugins expose onlyprefetchForRoutefor SSG. Do not duplicate business getters or mutations oncreateBackendStack().raw. - Use
myStack.forRequest(request).operations.*for request work andmyStack.trusted.*for explicitly trusted jobs.
Key patterns
- Import
defineBackendPluginandcreateEndpointfrom"@btst/stack/plugins/api"(not@btst/stack/plugins). - Import the adapter type as
import type { DBAdapter as Adapter } from "@btst/db". - Routes are defined with
createEndpoint(path, { method, query?, body? }, handler)— not string-keyed"GET /path"objects. - Route handlers return data directly (
return item) — noctx.json(). - Throw errors with
throw ctx.error(statusCode, { message }). - The
routesfactory returns a named object:return { listItems, createItem } as const. - Export the router type as
ReturnType<ReturnType<typeof myBackendPlugin>["routes"]>.
Lifecycle hook naming
Pattern: onBefore{Action}{Entity}, onAfter{Action}{Entity}, onError{Action}{Entity}
onBeforeListPosts, onAfterListPosts, onErrorListPosts
onBeforeCreatePost, onAfterCreatePost, onErrorCreatePost
onBeforeUpdatePost, onAfterUpdatePost, onErrorUpdatePost
onBeforeDeletePost, onAfterDeletePost, onErrorDeletePost
// Preserve meaningful domain events instead of inventing CRUD phases.
onBeforeChat, onAfterChat, onErrorChat
onBeforeActivateTools
Normalize names without adding lifecycle phases that the plugin does not already support.
Trusted operations in AI tool execute functions
myStack is a module-level const. The execute closure runs lazily (only on HTTP request), so myStack is always initialised by then:
import { createBackendStack } from "@btst/stack/api"
export const myStack = createBackendStack({ ... })
const myTool = tool({
execute: async (params) => {
await myStack.trusted.kanban.createTask({
title: params.title,
columnId: "col-id",
})
return { success: true }
}
})
Gotchas
- Wrong import path — always import from
"@btst/stack/plugins/api", not"@btst/stack/plugins". - Wrong adapter type — use
import type { DBAdapter as Adapter } from "@btst/db"in getters/mutations/plugin files. "GET /path"string keys — routes usecreateEndpoint(), not string-keyed method/path objects.ctx.json()— does not exist; return data directly from route handlers.- Business methods on
createBackendStack().raw— do not add them. Keep the composed lifecycle explicit throughforRequest(request).operationsortrusted, and reserverawfor narrow lower-level/SSG helpers. - Authorization in lifecycle hooks — routine access control belongs in operation descriptors and the one shared rule. Hooks receive already-authorized context.
- Write ops in
getters.ts— write functions belong inmutations.ts, notgetters.ts.
Full code patterns
See REFERENCE.md for complete defineBackendPlugin, getters, mutations, and api/index.ts code shapes.