Skip to content

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 nameInputOutputDescription
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.