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.
-
Split browser and server responsibilities
Section titled “Split browser and server responsibilities”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 -
Configure the engine server
Section titled “Configure the engine server”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.
-
Choose the transport connector
Section titled “Choose the transport connector”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,}); -
Make history durable if you enable it
Section titled “Make history durable if you enable it”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.
-
Add production error reporting
Section titled “Add production error reporting”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.Providerconnector={connector}onError={(error) => {reportError(error);}}><m.Chart /></m.Provider>
Production checklist
Section titled “Production checklist”- 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.
Next steps
Section titled “Next steps”- The Engine — understand the reference implementation boundary
- Connectors — choose the transport shape
- Conversation history — configure durable context