PAIPAI

Production Workspace App

Use durable storage, trusted workspace scope, and optional realtime.

// server/assistant-agent.ts
import { defineAgent } from "@pai/core";
import { z } from "zod";

export const assistantAgent = defineAgent({
  name: "report",

  identity: z.object({
    workspaceId: z.string(),
  }),

  model: ({ runtimeContext }) => runtimeContext.models.primary,
  instructions: ({ runtimeContext }) =>
    `Draft reports for ${runtimeContext.workspace.name}.`,

  runtimeContext: async ({ identity, signal }) => ({
    workspace: await loadWorkspace(identity.workspaceId, { signal }),
    models: modelRegistry,
  }),
});
// server/http-server.ts
import { serve } from "@hono/node-server";
import { createPai, type InferPaiContract } from "@pai/core";
import { createPaiHonoReceiver } from "@pai/hono";
import { Hono } from "hono";
import { cors } from "hono/cors";
import { assistantAgent } from "./assistant-agent";
import { createProductionThreadLiveProvider } from "./realtime-provider";
import { createProductionThreadStore } from "./thread-storage";

export const pai = createPai({
  agents: { main: assistantAgent },
  storage: createProductionThreadStore({ url: process.env.DATABASE_URL! }),
  realtime: createProductionThreadLiveProvider({
    url: process.env.REALTIME_URL!,
  }),
  scopeKey: (identity) => identity.workspaceId,
});
export type AssistantPai = InferPaiContract<typeof pai>;

const app = new Hono();

app.use("/api/*", cors({ origin: "http://localhost:5173" }));

app.route(
  "/api/workspaces/:workspaceId/pai",
  createPaiHonoReceiver({
    pai,
    resolveIdentity: async ({ request, params, body }) => {
      const user = await requireUser(request);
      const workspace = await requireWorkspaceMember(request, user, params.workspaceId);

      return {
        identity: { userId: user.id, workspaceId: workspace.id },
        clientData: body?.clientData,
      };
    },
  }),
);

serve({
  fetch: app.fetch,
  port: 3001,
});

createPai({ agents }) owns the runtime registry and infrastructure, while createPaiHonoReceiver({ pai, ...options }) adapts it to Hono.

Create a workspace-scoped client:

// web/src/workspace-pai.ts
import { createPaiHttpClient } from "@pai/client-http";
import type { AssistantPai } from "../../server/http-server";

export function createWorkspacePai(workspaceId: string) {
  return createPaiHttpClient<AssistantPai>({
    url: `http://localhost:3001/api/workspaces/${workspaceId}/pai`,
  });
}

requireWorkspaceMember(request, user, workspaceId) checks the authenticated user against the route workspace before returning trusted scope. Keep that logic server-side; do not trust workspace ids from request body JSON.

The server loads threads by scopeKey + threadId, so guessed thread ids cannot cross workspace boundaries.