Skip to content

Services

Services are server-side dependencies injected into action contexts. They give your actions access to platform capabilities like AI, blob storage, permissions, and cross-store communication.

PlatformServices

All synced-store apps share a single PlatformServices type. Every field is available on every action invocation — apps that don't need a particular service simply ignore it.

Poe Employee Note

Services are very rough right now — the API we launch with will be different from what's documented below. In particular, ctx.services will likely be renamed to ctx.platform, ctx.environment, or ctx.poe. See the type definition and the implementation.

Available Services

ServiceTypeDescription
getPoeApiKey() => Promise<string | null>Resolve the Poe API key for the current user
systemToolsExecutableTool[]Executable tools the LLM can call during streaming (e.g. bash tool)
callStoreAction(storeTypeId, storeInstanceId, actionName, actionInput) => Promise<...>Call an action on another store instance
getStoreSchema(storeTypeId) => Promise<...>Get the schema for a store type (used to build tool definitions)
blobStorageIBlobStorageContent-addressable blob storage
env{ BASE_URL, BLOB_HOST }Environment variables available to actions
publishApp(input) => Promise<...>Publish an app to the marketplace
permissions{ check, listGranted, listAvailableScopes }Manage Poe permissions (e.g. point usage) and access to connected third-party accounts (e.g. GitHub, Google Drive)

Full Type

typescript
import type { IBlobStorage } from "@ai-app/blob-storage";
import type { ExecutableTool } from "@ai-app/poe-api-js";

type PlatformServices = {
  getPoeApiKey: () => Promise<string | null>;
  systemTools: ExecutableTool[];
  callStoreAction: (
    storeTypeId: string,
    storeInstanceId: string,
    actionName: string,
    actionInput: unknown,
  ) => Promise<{
    success: boolean;
    result?: unknown;
    error?: { type: string; message: string };
  }>;
  getStoreSchema: (storeTypeId: string) => Promise<{
    success: boolean;
    result?: StoreSchemaInfo;
    error?: { type: string; message: string };
  }>;
  blobStorage: IBlobStorage;
  env: { BASE_URL: string; BLOB_HOST: string };
  publishApp: (input: { handle: string; html: string }) => Promise<
    | { success: true; app: { id: string; handle: string; /* ... */ } }
    | { success: false; error: string }
  >;
  permissions: {
    check: (scope: string) => Promise<boolean>;
    listGranted: () => Promise<string[]>;
    listAvailableScopes: () => Array<{
      scope: string;
      displayName: string;
      description: string;
      requiresOAuth: boolean;
    }>;
  };
};

Using Services in Your App

1. Alias the type in your schema

Each app aliases PlatformServices to a local name for readability:

typescript
// schema.ts
import type { PlatformServices } from "@ai-app/slop-poe-platform-types";

export type MyAppServices = PlatformServices;

2. Pass it to your backend config

typescript
// backend-config.ts
import { defineBackendConfig } from "@synced-store/backend";
import { mySchema } from "./schema";
import type { MyAppServices } from "./schema";
import { myMutators } from "./mutators";
import { myActions } from "./actions";

export const myBackendConfig = defineBackendConfig<
  typeof mySchema,
  MyAppServices
>({
  schema: mySchema,
  mutators: myMutators,
  actions: myActions,
});

3. Access services in actions

Services are available via ctx.services in action handlers:

typescript
// actions.ts
export const myActions = {
  callBot: async (ctx, input) => {
    const apiKey = await ctx.services.getPoeApiKey();
    // Use apiKey to make Poe API calls...
  },

  saveFile: async (ctx, input) => {
    const hash = await ctx.services.blobStorage.put(input.content);
    // Store the hash in your synced-store table...
  },

  checkAccess: async (ctx, input) => {
    const allowed = await ctx.services.permissions.check("files:write");
    if (!allowed) throw new Error("Permission denied");
    // Proceed with the operation...
  },
};

TIP

Services are only available in actions, not mutators. Mutators run on both client and server, so they cannot access server-side dependencies.

Testing with Mock Services

For tests, create a mock implementation of PlatformServices:

typescript
import type { PlatformServices } from "@ai-app/slop-poe-platform-types";
import { MemoryBlobStorage } from "@ai-app/blob-storage";

function createMockPlatformServices(
  overrides?: Partial<PlatformServices>,
): PlatformServices {
  return {
    getPoeApiKey: async () => "test-api-key",
    systemTools: [],
    callStoreAction: async () => ({ success: true }),
    getStoreSchema: async () => ({ success: false }),
    blobStorage: new MemoryBlobStorage(),
    env: { BASE_URL: "http://localhost:8787", BLOB_HOST: "http://localhost:8788" },
    publishApp: async () => ({ success: false, error: "not implemented" }),
    permissions: {
      check: async () => true,
      listGranted: async () => [],
      listAvailableScopes: () => [],
    },
    ...overrides,
  };
}

Then pass it via createServices in your test setup:

typescript
const runner = createLocalStoreFunctionRunner({
  ...myBackendConfig,
  createServices: () => createMockPlatformServices(),
});