Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
58e176c
feat: add server-side data access patterns and getter functions for p…
olliethedev Feb 19, 2026
0215e69
refactor: remove duplicated code
olliethedev Feb 20, 2026
19c9349
refactor: update BackendPlugin type to exclude plugins without an api…
olliethedev Feb 20, 2026
bd141c9
feat: implement CRUD operations for todos with server-side getters
olliethedev Feb 20, 2026
5444d73
feat: enhance blog API with paginated results and authorization warni…
olliethedev Feb 20, 2026
221a11b
refactor: remove redundant code
olliethedev Feb 20, 2026
63d48be
refactor: improve error handling for JSON parsing in content serializ…
olliethedev Feb 20, 2026
e7bfbbf
refactor: streamline blog API response handling and improve null safe…
olliethedev Feb 20, 2026
ddc5cd3
refactor: improve error handling in content serialization by throwing…
olliethedev Feb 20, 2026
c951b07
refactor: simplify task fetching logic by introducing a helper
olliethedev Feb 20, 2026
bc4073a
feat: blog performance improvements
olliethedev Feb 20, 2026
82da42b
refactor: enhance tag handling in blog API to return empty results fo…
olliethedev Feb 20, 2026
a17840b
refactor: optimize getPostBySlug to improve tag resolution and handle…
olliethedev Feb 20, 2026
0a80c9d
refactor: update blog API tests and client to handle PostListResult s…
olliethedev Feb 20, 2026
8106495
refactor: update blog API documentation
olliethedev Feb 20, 2026
0dd81d1
refactor: make getAllPosts functionality consistent with other plugin…
olliethedev Feb 20, 2026
83bc7c9
refactor: update kanban API to return paginated results for boards wi…
olliethedev Feb 20, 2026
abcad84
refactor: update getAllBoards test to validate paginated results stru…
olliethedev Feb 20, 2026
141e597
refactor: update kanban API to ensure consistency in onBoardsRead hook
olliethedev Feb 20, 2026
72ddce7
refactor: update kanban API response handling to correctly extract bo…
olliethedev Feb 20, 2026
afd5eea
fix: pagination logic consistency
olliethedev Feb 23, 2026
01d28b6
refactor: improve blog API to ensure onPostsRead hook is only invoked…
olliethedev Feb 23, 2026
ed6a046
refactor: simplify blog API onPostsRead hook invocation by removing e…
olliethedev Feb 23, 2026
2db1edd
fix: ensure category options are visible before selection in CMS tests
olliethedev Feb 23, 2026
3f02aeb
docs: update agents.md file with new api
olliethedev Feb 23, 2026
c27ac22
test: enhance form and UI builder tests with additional wait for stat…
olliethedev Feb 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,48 @@ onRouteRender, onRouteError
onBefore*PageRendered
```

### Server-side API Factory (`api`)

Every backend plugin can expose a typed `api` surface for direct server-side or SSG data access (no HTTP roundtrip). Add an `api` factory alongside `routes`:

```typescript
// src/plugins/{name}/api/getters.ts — pure DB functions, no hooks/HTTP context
export async function listItems(adapter: Adapter): Promise<Item[]> { ... }
export async function getItemById(adapter: Adapter, id: string): Promise<Item | null> { ... }

// src/plugins/{name}/api/plugin.ts
export const myBackendPlugin = defineBackendPlugin({
name: "{name}",
dbPlugin: dbSchema,
api: (adapter) => ({ // ← bound to shared adapter
listItems: () => listItems(adapter),
getItemById: (id: string) => getItemById(adapter, id),
}),
routes: (adapter) => { /* HTTP endpoints */ },
})

// src/plugins/{name}/api/index.ts — re-export getters for direct import
export { listItems, getItemById } from "./getters";
```

After calling `stack()`, the result exposes `api` (namespaced per plugin) and `adapter`:

```typescript
export const myStack = stack({ basePath, plugins, adapter })
export const { handler, dbSchema } = myStack

// Use in Server Components, generateStaticParams, scripts, etc.
const items = await myStack.api["{name}"].listItems()
const item = await myStack.api["{name}"].getItemById("abc")
```

**Rules:**
- Keep getters in a separate `getters.ts` — no HTTP context, no lifecycle hooks
- The `api` factory and `routes` factory share the same adapter instance
- If the plugin has a one-time init/sync step (like CMS `syncContentTypes`), call it inside each getter wrapper — not just inside `routes`
- Re-export getters from `api/index.ts` for consumers who need direct import (SSG/build-time)
- Authorization hooks are **not** called via `stack().api.*` — callers are responsible for access control

### Query Keys Factory

Create a query keys file for React Query integration:
Expand Down Expand Up @@ -522,3 +564,7 @@ The `AutoTypeTable` component automatically pulls from TypeScript files, so ensu
9. **Suspense errors not caught** - If errors from `useSuspenseQuery` aren't caught by ErrorBoundary, add the manual throw pattern: `if (error && !isFetching) { throw error; }`

10. **Missing ComposedRoute wrapper** - Page components must be wrapped with `ComposedRoute` to get proper Suspense + ErrorBoundary handling. Without it, errors crash the entire app.

11. **`stack().api` bypasses authorization hooks** - Getters accessed via `myStack.api.*` skip all `onBefore*` hooks. Never use them as a substitute for authenticated HTTP endpoints — enforce access control at the call site.

12. **Plugin init steps not called via `api`** - If a plugin's `routes` factory runs a one-time setup (e.g. CMS `syncContentTypes`), that same setup must also be awaited inside the `api` getter wrappers, otherwise direct getter calls will query an uninitialised database.
40 changes: 40 additions & 0 deletions docs/content/docs/plugins/ai-chat.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -929,3 +929,43 @@ overrides={{
#### AiChatLocalization

<AutoTypeTable path="../packages/stack/src/plugins/ai-chat/client/localization/index.ts" name="AiChatLocalization" />

## Server-side Data Access

The AI Chat plugin exposes standalone getter functions for server-side use cases, giving you direct access to conversation history without going through HTTP.

### Two patterns

**Pattern 1 — via `stack().api`**

```ts title="app/lib/stack.ts"
import { myStack } from "./stack";

// List all conversations (optionally scoped to a user)
const all = await myStack.api["ai-chat"].getAllConversations();
const userConvs = await myStack.api["ai-chat"].getAllConversations("user-123");

// Get a conversation with its full message history
const conv = await myStack.api["ai-chat"].getConversationById("conv-456");
if (conv) {
console.log(conv.messages); // Message[]
}
```

**Pattern 2 — direct import**

```ts
import {
getAllConversations,
getConversationById,
} from "@btst/stack/plugins/ai-chat/api";

const conv = await getConversationById(myAdapter, conversationId);
```

### Available getters

| Function | Description |
|---|---|
| `getAllConversations(adapter, userId?)` | Returns all conversations, optionally filtered by userId |
| `getConversationById(adapter, id)` | Returns a conversation with messages, or `null` |
54 changes: 54 additions & 0 deletions docs/content/docs/plugins/blog.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -527,3 +527,57 @@ You can import the hooks from `"@btst/stack/plugins/blog/client/hooks"` to use i
#### PostUpdateInput

<AutoTypeTable path="../packages/stack/src/plugins/blog/client/hooks/blog-hooks.tsx" name="PostUpdateInput" />

## Server-side Data Access

The blog plugin exposes standalone getter functions for server-side and SSG use cases. These bypass the HTTP layer entirely and query the database directly.

### Two patterns

**Pattern 1 — via `stack().api` (recommended for runtime server code)**

After calling `stack()`, the returned object includes a fully-typed `api` namespace. Getters are pre-bound to the adapter:

```ts title="app/lib/stack.ts"
import { myStack } from "./stack"; // your stack() instance

// In a Server Component, generateStaticParams, etc.
const result = await myStack.api.blog.getAllPosts({ published: true });
// result.items — Post[]
// result.total — total count before pagination
// result.limit — applied limit
// result.offset — applied offset

const post = await myStack.api.blog.getPostBySlug("hello-world");
const tags = await myStack.api.blog.getAllTags();
```

**Pattern 2 — direct import (SSG, build-time, or custom adapter)**

Import getters directly and pass any `Adapter`:

```ts
import { getAllPosts, getPostBySlug, getAllTags } from "@btst/stack/plugins/blog/api";

// e.g. in Next.js generateStaticParams
export async function generateStaticParams() {
const { items } = await getAllPosts(myAdapter, { published: true });
return items.map((p) => ({ slug: p.slug }));
}
```

### Available getters

| Function | Returns | Description |
|---|---|---|
| `getAllPosts(adapter, params?)` | `PostListResult` | Paginated posts matching optional filter params |
| `getPostBySlug(adapter, slug)` | `Post \| null` | Single post by slug, or `null` if not found |
| `getAllTags(adapter)` | `Tag[]` | All tags, sorted alphabetically |

### `PostListParams`

<AutoTypeTable path="../packages/stack/src/plugins/blog/api/getters.ts" name="PostListParams" />

### `PostListResult`

<AutoTypeTable path="../packages/stack/src/plugins/blog/api/getters.ts" name="PostListResult" />
39 changes: 39 additions & 0 deletions docs/content/docs/plugins/cms.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1248,3 +1248,42 @@ const result = zodSchema.safeParse(data)
<AutoTypeTable path="../packages/ui/src/lib/schema-converter.ts" name="FormStep" />

<AutoTypeTable path="../packages/ui/src/lib/schema-converter.ts" name="FormSchemaMetadata" />

## Server-side Data Access

The CMS plugin exposes standalone getter functions for server-side and SSG use cases.

### Two patterns

**Pattern 1 — via `stack().api`**

```ts title="app/lib/stack.ts"
import { myStack } from "./stack";

const types = await myStack.api.cms.getAllContentTypes();
const items = await myStack.api.cms.getAllContentItems("posts", { limit: 10 });
const item = await myStack.api.cms.getContentItemBySlug("posts", "my-first-post");
```

**Pattern 2 — direct import**

```ts
import {
getAllContentTypes,
getAllContentItems,
getContentItemBySlug,
} from "@btst/stack/plugins/cms/api";

export async function generateStaticParams() {
const result = await getAllContentItems(myAdapter, "posts", { limit: 100 });
return result.items.map((item) => ({ slug: item.slug }));
}
```

### Available getters

| Function | Description |
|---|---|
| `getAllContentTypes(adapter)` | Returns all registered content types, sorted by name |
| `getAllContentItems(adapter, typeSlug, params?)` | Returns paginated items for a content type |
| `getContentItemBySlug(adapter, typeSlug, slug)` | Returns a single item by slug, or `null` |
Loading