Architecture
What Opereo AI stores, what the box owns, and what happens when you create an engine.
Architecture
Opereo AI is a control plane, not a runtime. The agents don't run inside Opereo AI — each engine brings its own substrate (a Hermes engine is a box of its own, on the provider you pick), and Opereo AI drives it over HTTP.
The split
| Layer | Owns |
|---|---|
| Opereo AI (Next.js + Postgres) | organizations, projects, engines, API keys, canvas layout |
| The engine's runtime | the agents, their config, sessions, transcripts, skills, memory — all of it |
| The edge Worker (optional) | which hostname label points at which box origin |
| The relay (optional) | which WhatsApp sender is bound to which engine and profile |
The important consequence: Opereo AI stores almost nothing about your agents. A profile's skills, channels, automations and instructions live on its box and are read back on demand. There's no sync loop and no second copy to drift.
What Opereo AI does keep is small. Each engine row carries a status (creating, ready, failed), an error, and a meta JSON column typed per kind. For Hermes that's one flat object — { type, name, url, dashboardPassword, dashboardCookie } plus one nullable per-provider field (hetzner: { ip }, railway: { serviceId }, upstash: { boxId }) — everything needed to reach the box, and nothing else.
The two Workers
Both are optional Cloudflare Workers, each its own package under workers/, deployed
independently of the app.
The edge Worker (workers/edge/) is a hostname router: it reads the label off
<name>.<BOX_DOMAIN>, looks the matching origin up in a KV namespace, and forwards the
request. It is provider-blind — the app writes the KV entry when it provisions a box, so
the Worker never knows or cares whether the origin is a Hetzner VM, a Railway service or an
Upstash Box. Without it, each box is reached on its provider's own origin.
The relay (workers/relay/) is a Hermes Relay connector fronting one shared WhatsApp
number for every engine. Nothing dials into a box: the box's Hermes gateway opens a single
outbound WebSocket to the relay, held by a Durable Object per engine that hibernates
while idle and buffers inbound messages in its own storage until the gateway reconnects and
acknowledges them. A D1 database is the directory — per-engine gateway credentials, the
shared number, the WhatsApp-sender-to-profile bindings, and pending link codes — and
inbound media is re-hosted in R2 behind short-lived capability URLs. The app never
touches the socket or the database: it only ever calls the relay's admin API, to enrol an
engine and to mint a link code.
Creating an engine
Creating a project provisions nothing; an engine is created on its own, and the call returns before the box exists.
- Insert — an engine row lands with status
creatingand the request returns immediately. The rest runs in the background. - Provision — the provider's
createbrings up the box (a Hetzner VM, a Railway service, an Upstash Box), and its origin is registered with the edge Worker if public hostnames are configured. - Configure — Opereo AI waits for the box's dashboard, then seeds the default profile: its model on the engine's free tier, the connector plumbing, and its API gateway. No provider keys are injected — users connect their own.
- Finish — the row becomes
ready, orfailedwith the error that stopped it.
The UI polls until the row settles. A Hetzner engine is usually ready in 2–3 minutes (it pulls the image cold), Railway in about a minute, Upstash in about 25 seconds. Every step is idempotent — it looks for what it needs before creating it — so re-running a failed creation resumes rather than duplicating.
Talking to an engine
Each engine kind exposes its whole surface through its own API namespace (/api/hermes/{engineId}/… today), and Opereo AI reads agent state back from the runtime on demand. How a kind talks to its substrate is its own business — Hermes' typed dashboard client is described in Engines.
Auth
Organizations, members, and roles come from the auth provider. Every API route resolves the caller first — a browser session or an org-scoped API key — then checks that the caller's org owns the project before touching an engine. Admins reach any project in their org; members are scoped to their own. The full contract is on the API page.