Skip to content

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:

TableTypeDescription
$usersUserMembershipMembers of the store instance
$userInfoPoeUserInfoProfile 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 ReferenceSyncedStoreClient API (query, subscribe, mutate)
  • Platform — Server-side capabilities in actions