Appearance
Platform
Platform capabilities are server-side services injected into action contexts via ctx.platform. They give your actions access to AI, blob storage, permissions, cross-store communication, and more through a single typed call() method.
PlatformApi
All synced-store apps share a single PlatformApi type. Every capability is available on every action invocation — apps that don't need a particular one simply ignore it.
Available Capabilities
| Service name | Input | Output | Description |
|---|---|---|---|
getPoeApiKey | {} | { apiKey: string | null } | Resolve the Poe API key for the current user |
systemTools.list | {} | { tools: ToolDefinition[] } | List executable tools the LLM can call |
systemTools.call | { toolName, toolInput } | { result: unknown } | Execute a system tool by name |
env.get | {} | { BASE_URL, BLOB_HOST, POE_API_BASE_URL? } | Get environment variables |
store.callAction | { storeTypeId, storeInstanceId, actionName, actionInput } | { success, result?, error? } | Call an action on another store instance |
store.getSchema | { storeTypeId } | { success, result?, error? } | Get the schema for a store type |
blob.put | { content } | { hash } | Store content in blob storage (base64) |
blob.get | { hash } | { content: string | null } | Retrieve content from blob storage (base64) |
blob.has | { hash } | { exists } | Check if a blob exists |
apps.publish | { handle, html } | { success, app?, error? } | Publish an app to the marketplace |
permissions.check | { scope } | { granted } | Check if a permission scope is granted |
permissions.listGranted | {} | { scopes: string[] } | List granted permission scopes |
permissions.listAvailableScopes | {} | { scopes: Array<...> } | List all available permission scopes |
Typed Caller Interface
Pass PlatformCaller as the second type parameter to InferActionHandlers for full autocomplete on ctx.platform.call():
typescript
import type { InferActionHandlers } from "@synced-store/backend";
import type { PlatformCaller } from "@poe/synced-store-platform";
type MyActions = InferActionHandlers<typeof mySchema, PlatformCaller>;
// Now ctx.platform.call() has full autocomplete — no cast needed:
const myActions: MyActions = {
callBot: async (ctx, input) => {
const { apiKey } = await ctx.platform.call("getPoeApiKey", {});
const { tools } = await ctx.platform.call("systemTools.list", {});
const { hash } = await ctx.platform.call("blob.put", { content: btoa("hello") });
const { granted } = await ctx.platform.call("permissions.check", { scope: "files:write" });
},
};Using the Platform in Your App
1. Define your action types with PlatformCaller
typescript
// mutators.ts (or wherever you define your action type)
import type { InferActionHandlers } from "@synced-store/backend";
import type { PlatformCaller } from "@poe/synced-store-platform";
import type { MySchema } from "./schema";
// PlatformCaller provides full autocomplete for ctx.platform.call()
export type MyActions = InferActionHandlers<MySchema, PlatformCaller>;2. Define your backend config
typescript
// backend-config.ts
import { defineBackendConfig } from "@synced-store/backend";
import { mySchema } from "./schema";
import { myMutators } from "./mutators";
import { myActions } from "./actions";
export const myBackendConfig = defineBackendConfig<typeof mySchema>({
schema: mySchema,
mutators: myMutators,
actions: myActions,
});3. Access platform capabilities in actions
Platform capabilities are available via ctx.platform.call() in action handlers:
typescript
// actions.ts
import type { MyActions } from "./mutators";
export const myActions: MyActions = {
callBot: async (ctx, input) => {
const { apiKey } = await ctx.platform.call("getPoeApiKey", {});
// Use apiKey to make Poe API calls...
},
saveFile: async (ctx, input) => {
const { hash } = await ctx.platform.call("blob.put", {
content: btoa(input.content),
});
// Store the hash in your synced-store table...
},
checkAccess: async (ctx, input) => {
const { granted } = await ctx.platform.call("permissions.check", {
scope: "files:write",
});
if (!granted) throw new Error("Permission denied");
// Proceed with the operation...
},
};TIP
Platform capabilities are only available in actions, not mutators. Mutators run on both client and server, so they cannot access server-side dependencies.
Testing with Mock Platform Callers
For tests, create a mock platform caller using createMockPlatformCaller from your test utilities:
typescript
import { createMockPlatformCaller } from "@poe/synced-store-platform/test-utils";
const runner = createLocalStoreFunctionRunner({
...myBackendConfig,
createPlatformCaller: () => createMockPlatformCaller(),
});E2E tests with TestServer
For Playwright E2E tests, pass createPlatformCaller to TestServer:
typescript
import { TestServer } from "poe-apps-sdk/test-utils";
const server = new TestServer({
createPlatformCaller: () => createMockPlatformCaller(),
});See the E2E testing guide for a full example.