Skip to main content
Every user action in Slides that touches the server follows the same layered path. This page documents that path end-to-end, with specific detail on the AI assistant flow.

Request/response cycle

Client → /rpc → Contract → Router → Service → Domain Logic
                  ↓                    ↓
              Validation          External APIs
                                  (Anthropic, etc.)

1. Client — React app with TanStack Query

The frontend calls the backend exclusively through the typed ORPC client, wrapped in TanStack Query hooks. The client is initialized once in apps/web/src/infra/api/client.ts and consumed via orpc helpers in feature code.
// Feature code — TanStack Query mutation via ORPC
const mutation = orpc.slideshowAssistant.useMutation({
  onSuccess: (data) => {
    // data is fully typed as AssistantResponse
  },
});

mutation.mutate({ slideshowId, prompt, currentSlideshow });
The client has no knowledge of endpoints or HTTP — it works entirely with typed procedure names. Changing the server-side output type immediately produces a TypeScript error here.

2. Transport — HTTP to Elysia at /rpc

ORPC serializes the procedure call to an HTTP request and sends it to the Elysia server. The server (apps/server/src/index.ts) mounts the ORPC handler at /rpc. Elysia routes the request to the ORPC handler without inspecting its contents.

3. Contract — input validation

ORPC validates the incoming request against the procedure’s input schema before the handler runs. Schemas are defined using TypeBox and bridged to Zod via @sinclair/typemap:
// packages/api/src/slideshow/contracts.ts
export const slideshowContracts = {
  slideshowAssistant: publicProcedure
    .input(Zod(AssistantRequestSchema))   // TypeBox → Zod conversion
    .output(Zod(AssistantResponseSchema))
    .errors(slideshowAssistantErrors),
};
If the input does not conform to AssistantRequestSchema, ORPC rejects the request with a typed error before any handler code runs.

4. Router — thin wiring

The router handler connects the validated input to the appropriate service. Routers contain no orchestration logic — they only delegate:
// packages/api/src/routers/slideshow.ts
export const slideshowRouter = {
  slideshowAssistant: slideshowContracts.slideshowAssistant.handler(
    async ({ input, context, errors }) => {
      return handleSlideshowAssistant(input, context.anthropic, errors);
    }
  ),

  slideshowLoad: slideshowContracts.slideshowLoad.handler(
    async ({ input, context }) => {
      return handleSlideshowLoad(input, context.slideshowFs);
    }
  ),

  slideshowSave: slideshowContracts.slideshowSave.handler(
    async ({ input, context }) => {
      return handleSlideshowSave(input, context.slideshowFs);
    }
  ),
};
Each handler is under 5 lines. All logic lives in the service layer.

5. Context — request-scoped dependencies

The createContext function in packages/api/src/context.ts builds the request context that is injected into every handler. It contains the Anthropic client configuration, the filesystem adapter for slideshow persistence, and a placeholder for future session/auth data:
// packages/api/src/context.ts
export async function createContext({ context: _context, slideshowFs, env }) {
  return {
    session: null,          // Future: authenticated user
    slideshowFs,            // Filesystem adapter (load/save slideshows)
    anthropic: {
      apiKey: env.ANTHROPIC_API_KEY,
      model: env.ANTHROPIC_MODEL,
      maxTokens: env.ANTHROPIC_MAX_TOKENS,
    },
  };
}

6. Service — orchestration

Service files (*.service.ts) coordinate domain logic, external API calls, and error handling. They are the only layer that calls both packages/core functions and external providers like Anthropic.

7. Domain — pure business logic

packages/core functions are called by services to apply business rules. Domain functions are pure TypeScript — no HTTP, no I/O, no framework dependencies.

8. Response — typed all the way back

The service returns a value matching the contract’s output schema. ORPC validates the output, serializes it, and sends it back to the client. TanStack Query receives the response and the React component re-renders with the new data — all typed.

AI assistant flow

The AI assistant is the most complex data flow in the system. Here is the full path from user input to updated slideshow:

Step 1 — User submits a prompt

The user types a natural language prompt in the assistant panel. The frontend packages the current slideshow state alongside the prompt into an AssistantRequest and calls orpc.slideshowAssistant.mutate().
// AssistantRequest shape (from packages/api/src/slideshow/schemas.ts)
{
  slideshows: Slideshow[],          // all loaded slideshows
  currentSlideshowIndex: number,    // active slideshow index
  currentSlide: number,             // active slide index
  userInput: string,                // e.g. "Make the title slide more concise"
  previousMessages?: AssistantMessagePayload[],  // conversation history
  useFullContext: boolean,          // true = full slideshow, false = current slide only
}

Step 2 — Server calls Anthropic Claude

The handleSlideshowAssistant service in packages/api/src/slideshow/assistant-service.ts receives the request. It:
  1. Creates a StateDigest — a lightweight snapshot of the slideshow state (slide count, IDs, concept keys).
  2. Calls buildAssistantMessages from packages/core to assemble the Anthropic message array, including the system prompt and current context (full slideshow or current slide).
  3. Calls the Anthropic Messages API at https://api.anthropic.com/v1/messages with SLIDESHOW_SYSTEM_PROMPT and the assembled messages.

Step 3 — Claude returns a text response with an embedded JSON patch

Claude returns a plain text response that embeds JSON Patch operations in its body. The service extracts the patch using extractPatchFromResponse from packages/core.

Step 4 — Patch is validated, NOT applied

The service calls validatePatchWithSemantics to check the patch against the current slideshow structure. This produces a PatchOrchestrationResult:
  • "ok" — patch is valid; a PatchTransaction (immutable record) is created via buildPatchTransactionFromResponse
  • "invalid" — patch failed semantic validation; errors are returned for display
  • "noop" — no patch found in the response or the target slide is missing
The patch is not applied server-side. The server returns the transaction to the client, which decides whether to apply it.

Step 5 — AssistantResponse returned to client

The service returns an AssistantResponsePayload:
// AssistantResponsePayload shape (from packages/core/src/schema/assistant.ts)
{
  assistantText: string,            // Claude's full text response
  wasTruncated: boolean,            // true if hit max_tokens
  patchResult: PatchOrchestrationResult,  // ok | invalid | noop
  digest: StateDigest,              // snapshot for staleness detection
}
The frontend holds the PatchTransaction as pending. The user clicks Apply to apply it locally. Before applying, evaluateTransactionFreshness checks whether the slideshow state has drifted since the response was generated.

AI flow diagram

User prompt


AssistantRequest { slideshows, currentSlideshowIndex, currentSlide,
                   userInput, previousMessages, useFullContext }

    ▼  (HTTP POST /rpc)
ORPC contract validates input


Router → handleSlideshowAssistant(input, context.anthropic)


createStateDigest + buildAssistantMessages  (packages/core)


Anthropic Claude API call  (POST api.anthropic.com/v1/messages)


extractPatchFromResponse  (packages/core)


validatePatchWithSemantics  (packages/core) → PatchOrchestrationResult


AssistantResponsePayload { assistantText, wasTruncated, patchResult, digest }

    ▼  (HTTP response)
ORPC contract validates output


Client receives PatchTransaction → user clicks Apply → patch applied locally
The Anthropic API key is held server-side only, in validated environment variables accessed via @slides/config/server. The frontend never has access to the key and communicates only through the typed ORPC endpoint.

Load and save flow

For slideshowLoad and slideshowSave, the flow is simpler:
  1. Client calls orpc.slideshowLoad.useQuery() or orpc.slideshowSave.useMutation()
  2. ORPC validates input against LoadInputSchema / SaveInputSchema
  3. Router delegates to handleSlideshowLoad / handleSlideshowSave
  4. Service uses the SlideshowFsAdapter from context to read/write JSON files on the filesystem
  5. Response is validated against LoadOutputSchema / SaveOutputSchema and returned
The filesystem adapter is injected via context, which means the server app controls the storage implementation and the service code remains testable in isolation.

Build docs developers (and LLMs) love