Skip to content

Deploy to Production

A local Maelstrom demo can run with an in-memory engine, a development WebSocket, and a single provider key. Production needs stricter boundaries: provider credentials stay server-side, history must be durable if you enable it, and the connector path must survive deploys, restarts, and network failures.

This tutorial turns a development setup into a production-shaped deployment checklist. It does not prescribe one hosting provider; it names the boundaries you need to make explicit before shipping.

  1. Browser code should render the Chart, capture user intent, and talk to a connector. Server code should own LLM provider keys and the engine implementation.

    flowchart LR
      subgraph Browser["Browser app"]
        React["@maelstrom-co/react"]
        Connector["@maelstrom-co/client connector"]
        NoSecrets["no provider secrets"]
      end
    
      subgraph Server["Server runtime"]
        Engine["engine implementation"]
        Provider["LLM provider adapter + API key"]
        History["optional history store"]
      end
    
      Connector --> Engine
      Engine --> Provider
      Engine -. optional .-> History
  2. Keep provider configuration in server environment variables. Pick the adapter and model intentionally; stronger structured-output models can reduce healing and retries, while lower-latency models can improve responsiveness.

    src/server/engine.ts
    import { createEngine } from '@maelstrom-co/engine';
    import { llmPlanner } from '@maelstrom-co/planner-llm';
    import { geminiText } from '@tanstack/ai-gemini';
    export const engine = createEngine({
    planner: llmPlanner({
    adapter: geminiText(process.env.MAELSTROM_MODEL ?? 'gemini-2.0-flash'),
    }),
    retry: { max: 1 },
    });

    See Use a custom LLM provider for provider-specific setup.

  3. WebSocket is the most common production shape because Maelstrom turns are interactive. HTTP can work for request/response deployments. In-memory connectors are useful for tests, not browser deployments.

    Put the public connector URL in browser configuration and keep private provider keys on the server:

    src/client/connector.ts
    import { WebSocketConnector } from '@maelstrom-co/client';
    export const connector = new WebSocketConnector({
    url: import.meta.env.PUBLIC_MAELSTROM_WS_URL,
    });
  4. Conversation history is opt-in. If you enable it in production, use a durable store such as Redis, Postgres, or SQLite, and account for concurrency across server instances.

    Read Enable conversation history before enabling history for real users.

  5. Render connector state in the UI and report server-side engine failures to your observability stack. Keep the user-facing recovery path simple: if the connector is disconnected, disable intent submission; if processing fails, allow the user to retry the intent.

    <m.Provider
    connector={connector}
    onError={(error) => {
    reportError(error);
    }}
    >
    <m.Chart />
    </m.Provider>
  • Browser bundle contains no LLM provider secrets.
  • Engine server has provider keys injected from the deployment environment.
  • Connector URL is environment-specific and points at the deployed runtime.
  • UI renders connection and processing errors with retry paths.
  • History is either disabled or backed by a durable store.
  • Multi-instance deployments have session affinity, a distributed lock, or a per-session queue if history/concurrency matters.