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.