Appearance
SyncedStore Quick Reference
How the client and backend configs connect to give you a fully typed synced-store app.
The Two Configs
Every bundled app has two configs — one for the client (runs in the browser) and one for the backend (runs on the server). Both are derived from the same schema.
schema.ts ──→ defineBackendConfig() ──→ backend-config.ts (server)
│
└──────→ defineClientConfig() ──→ client-config.ts (browser)defineBackendConfig() — Server Side
Import from poe-apps-sdk/v1/backend.js. Bundles schema + mutators + actions + hooks. Poe system tables and hooks are auto-wired.
typescript
import { defineSchema, table, defineBackendConfig } from "poe-apps-sdk/v1/backend.js";
import { z } from "zod";
const schema = defineSchema({
schemaVersion: 1,
tables: { items: { schema: table(z.object({ text: z.string() })) } },
mutators: { addItem: { input: z.object({ text: z.string() }) } },
actions: {},
});
export const backendConfig = defineBackendConfig({
schema,
mutators: { addItem: async (ctx, input) => { /* ... */ } },
actions: {},
});defineClientConfig() — Client Side
Import from poe-apps-sdk/v1/client.js. Bundles mutators only (no schema or Zod at runtime). Use a type-only import of the schema to get full type inference without bundling Zod (~280KB).
typescript
import { defineClientConfig } from "poe-apps-sdk/v1/client.js";
import type { schema } from "./schema"; // type-only!
import { myMutators } from "./mutators";
export const clientConfig = defineClientConfig<typeof schema>({
mutators: myMutators,
schemaVersion: 1,
});Then in your app entry:
typescript
import { createPoe, PostMessageEnvironment } from "poe-apps-sdk/v1/client.js";
const Poe = createPoe({ environment: new PostMessageEnvironment() });
const store = Poe.setupStore(clientConfig);System Tables
Poe apps automatically get three system tables, accessible via ctx.table() in mutators and actions:
| Table | Type | Description |
|---|---|---|
$users | UserMembership | Members of the store instance |
$userInfo | PoeUserInfo | Profile info (name, avatar, etc.) |
typescript
// Read system tables like any other table
const members = await ctx.table("$users").entries().toArray();
const myInfo = await ctx.table("$userInfo").get(userId);These are managed by the platform — your app reads them but doesn't write to them directly. User lifecycle events are handled via system hooks.
Further Reading
- API Patterns — Schema, mutators, configs, entry wiring
- Mutator Rules — Toggle-free writes, external IDs, read-before-write
- Actions — Server-only operations (AI, external APIs)
- Client API Reference —
SyncedStoreClientAPI (query, subscribe, mutate) - Platform — Server-side capabilities in actions