Opereo AI

Self-hosting

Run your own Opereo AI — prerequisites, environment, the Workers, and first boot.

Self-hosting

Opereo AI is MIT licensed and runs on your own infrastructure. It's a Next.js app plus Postgres; the agents themselves run on boxes it provisions.

What you'll need

  • Node 22+ and pnpm
  • Postgres — any instance you can reach
  • Clerk — auth, organizations, and roles
  • One provider — where each engine's box runs. Hetzner is the default and needs an API token plus an SSH key pair; Railway and Upstash Box work instead of it, or alongside it
  • Cloudflare — optional, for public box hostnames and for WhatsApp
  • Model keys are optional — fresh agents start on Hermes' anonymous free tier, and every user connects their own providers in the app

Install

git clone https://github.com/genlabs-ai/opereo-ai.git
cd opereo-ai/opereo
pnpm install

Create opereo/.env, then:

pnpm db:push     # sync the schema to your Postgres
pnpm dev         # http://localhost:3000

Next loads .env automatically.

There is no pnpm workspace. opereo/, workers/edge/, workers/relay/ and docs/ are independent packages, each with its own lockfile and its own .env — nothing reads opereo/.env but the app.

Environment

Set the variables for the features you use. The blocks below mirror opereo/.env.example.

Database

DATABASE_URL=postgres://...

Auth (Clerk)

NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=
CLERK_SECRET_KEY=
CLERK_WEBHOOK_SIGNING_SECRET=
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
NEXT_PUBLIC_CLERK_SIGN_IN_FALLBACK_REDIRECT_URL=/
NEXT_PUBLIC_CLERK_SIGN_UP_FALLBACK_REDIRECT_URL=/

Opereo AI

OPEREO_COMPOSIO_KEY=   # powers the Integrations panel (Composio)

Providers — a provider is offered when its credentials are set. Engines default to hetzner.

HETZNER_API_TOKEN=
OPEREO_RAILWAY_TOKEN=
OPEREO_RAILWAY_PROJECT_ID=
UPSTASH_BOX_API_KEY=
UPSTASH_BOX_HERMES_SNAPSHOT_ID=
# One ed25519 key pair for the deployment; Hetzner updates run over SSH with it.
# Private key is an OpenSSH PEM with newlines written as \n.
HETZNER_SSH_PRIVATE_KEY=
HETZNER_SSH_PUBLIC_KEY=

Box exposure (Cloudflare, optional) — the app writes KV routes itself, so it needs these even though the Worker has its own copy.

BOX_DOMAIN=
CLOUDFLARE_API_TOKEN=
CLOUDFLARE_ACCOUNT_ID=
CLOUDFLARE_KV_NAMESPACE_ID=

Edge Worker (workers/edge/.env, optional)

BOX_DOMAIN=
CLOUDFLARE_API_TOKEN=
CLOUDFLARE_ACCOUNT_ID=

Relay (workers/relay/.env, optional) — the same three Cloudflare variables plus four of its own. The duplication is deliberate: each package is self-contained.

BOX_DOMAIN=
CLOUDFLARE_API_TOKEN=
CLOUDFLARE_ACCOUNT_ID=
RELAY_ADMIN_TOKEN=      # the bearer the app presents to /admin/*; invent a long random string
KAPSO_API_KEY=          # from the Kapso dashboard: the project API key
KAPSO_WEBHOOK_SECRET=   # the number's webhook signing secret
KAPSO_PHONE_NUMBER_ID=  # the number's id

Providers

An engine is one Hermes runtime; the provider decides where it runs. It's picked per engine at create time and defaults to hetzner.

  • hetzner — a Hetzner Cloud VM running the official Hermes image behind Caddy. Enabled by HETZNER_API_TOKEN plus the SSH key pair.
  • railway — the same image as a Railway service with a persistent volume. Enabled by OPEREO_RAILWAY_TOKEN and OPEREO_RAILWAY_PROJECT_ID.
  • upstash — an Upstash Box restored from a baked snapshot. Enabled by UPSTASH_BOX_API_KEY and UPSTASH_BOX_HERMES_SNAPSHOT_ID.

The Upstash snapshot

Only the upstash provider uses a snapshot. UPSTASH_BOX_HERMES_SNAPSHOT_ID points at a prebuilt image with the runtime already installed, so provisioning restores it instead of installing from scratch.

To bake your own:

pnpm upstash:bake         # ~7 min, prints a new snapshot id

Put the printed id in .env. The script verifies the result before snapshotting (runtime version, dashboard build, browser launch) so a broken image fails the bake instead of shipping.

Baking creates a real box and deletes it when it succeeds. If it fails it deliberately leaves the box running so you can inspect the logs — delete it yourself afterward.

Public hostnames (edge Worker)

Optional. With it, every engine answers on https://<name>.<BOX_DOMAIN> (its dashboard) and https://<name>-<port>.<BOX_DOMAIN> (a profile's API), through a 17-line Worker that maps a hostname label to an origin in KV. Without it, each box answers on its provider's own origin.

The Worker is its own package at workers/edge/. Your domain has to be a zone on your Cloudflare account already, and workers/edge/wrangler.toml carries the route and BOX_DOMAIN literally — change pattern, zone_name and BOX_DOMAIN to your own domain before the first setup.

cd workers/edge
pnpm install
cp .env.example .env
pnpm provision     # idempotent: KV namespace, wildcard DNS, deploy; prints CLOUDFLARE_KV_NAMESPACE_ID

Copy the printed namespace id into opereo/.env.

WhatsApp (the relay)

Optional, and it requires the edge Worker's domain setup — the relay lives at relay.<BOX_DOMAIN>. It fronts one shared WhatsApp number for every engine: no box holds a WhatsApp credential and none opens an inbound port. See Engines for what it gives a user.

The number is created in Kapso's hosted Meta wizard; the free tier covers one number and 2,000 messages.

cd workers/relay
pnpm install
cp .env.example .env
pnpm provision     # idempotent: D1, R2, migrations, wrangler.toml, deploy, secrets

pnpm provision ensures the D1 database opereo-relay and the R2 bucket opereo-relay-media, applies the migrations, writes the database id, route and PUBLIC_URL into wrangler.toml from BOX_DOMAIN, deploys, pushes the four secrets, and prints the webhook URL — https://relay.<BOX_DOMAIN>/webhooks/whatsapp — to paste into Kapso.

The app needs two variables of its own in opereo/.env to reach the relay; without them the WhatsApp row in channels reports Unavailable:

RELAY_URL=https://relay.<BOX_DOMAIN>
RELAY_ADMIN_TOKEN=      # the same value as workers/relay/.env

Every agent created after that is enrolled automatically; phones are linked from the agent's channels tab.

Deploying the Workers

pnpm provision is the first deploy of each Worker, run locally from its own directory. After that both deploy through Cloudflare Workers Builds: connect the repository once per Worker in the Cloudflare dashboard (Worker → Settings → Builds → Connect), with

Settingworkers/edgeworkers/relay
Root directoryworkers/edgeworkers/relay
Build command(none)pnpm build (typecheck + tests)
Deploy commandpnpm exec wrangler deploypnpm exec wrangler deploy
Build watch pathsworkers/edge/*workers/relay/*

The Worker's name in the dashboard must equal name in that directory's wrangler.tomlopereo-cloudflare and opereo-relay. A push to main that touches the directory builds and deploys it. There is no CI in the repository.

Cost

The app itself is one Next.js server plus Postgres, and each engine costs whatever its provider charges for one box. The Cloudflare side is free for the edge Worker; the relay uses Durable Objects, which need Workers Paid ($5/mo) in production. Relay usage at realistic volume stays inside the plan's included allowances — an idle socket is a hibernated object and bills nothing.

Useful commands

pnpm dev / build / start
pnpm typecheck                 # tsc --noEmit
pnpm lint / format
pnpm db:generate / db:migrate / db:push / db:studio
pnpm db:seed                   # seed the content we ship
pnpm hermes:api                # regenerate the runtime API client
pnpm upstash:bake              # bake the Upstash snapshot

On this page