OpenMetal is the universal compute gateway for AI agents. This repository implements a hosted control plane that provisions CPU sandboxes, routes creation across supported compute providers, manages lifecycle operations, and bills managed usage.
OpenMetal is OpenRouter for cloud sandboxes and GPUs:
- Managed OpenMetal capacity is the default. OpenMetal owns provider contracts and credentials.
- Customers have one OpenMetal account and organization-scoped balance.
- API keys are scoped per project.
- Every provider resource maps back to one OpenMetal organization and project.
- Customers pay OpenMetal; OpenMetal reconciles and pays providers.
- The public API and TypeScript SDK hide provider-specific lifecycle, usage, and billing differences.
The current provider set is Blaxel, Cloudflare, CodeSandbox, Daytona, E2B, Freestyle, Modal, Northflank, Prime Intellect, Runloop, and Vercel. Provider integrations remain behind one capability-oriented OpenMetal API; they are not separate customer-facing products.
OpenMetal is expanding one portable compute interface in stages:
- Available now: CPU sandboxes, lifecycle operations, processes, files, leased HTTP endpoints, provider routing, managed credits, BYOK, TypeScript SDK, CLI, and Agent Skill.
- Coming next: GPU and accelerator workloads for training, inference, and computer use.
- Planned: browser computers and persistent machines through the same API and capability model.
apps/web --Auth/Realtime--> Supabase
apps/web --MetalClient----> apps/api --Drizzle--> PostgreSQL
apps/api --outbox---------> apps/worker
PostgreSQL --realtime.send--> Supabase Realtime --Broadcast--> apps/web
apps/webis the Next.js dashboard. It uses Supabase only for login, logout, confirmation, session refresh, and authorized private Realtime channels.apps/apiis the only public product API. It authenticates Supabase JWTs, authorizes every org/project operation, and writes product state, a durable event, its private Broadcast, and any outbox jobs in one transaction.apps/workerclaims outbox jobs withFOR UPDATE SKIP LOCKEDand runs provider, billing, and webhook work. Events it records are broadcast the same way.packages/sdk-typescript(@openmetal/sdk) is the only way the dashboard talks to the OpenMetal API.packages/contractsholds runtime Zod schemas shared by the API and SDK.packages/billingholds credit purchase fees, the organization ledger, Stripe Checkout, and automatic top ups.- One Supabase project per environment provides Auth, PostgreSQL, and Realtime.
Prepaid billing uses Stripe Checkout, a 5.5% purchase fee with an $0.80 minimum, and pass-through provider usage. See docs/billing.md.
- Node.js 22 or later
- pnpm 11.13.1
- Bun 1.3.11 (for standalone CLI builds)
- Docker (for local Supabase)
- Supabase CLI 2.105.0 or later (
supabase --version)
pnpm install --frozen-lockfileThe private @openmetal/cli workspace builds the standalone openmetal binary. It supports browser login for control-plane administration; project API keys for sandbox lifecycle, argv-based processes, binary filesystem operations, and leased HTTP endpoints; streamed process output; and --json/--no-input automation. Interactive PTY/terminal sessions and general SSH, WebSocket, or connection APIs remain outside the public surface.
pnpm --filter @openmetal/cli build
apps/cli/dist/openmetal --helpTagged cli-v* releases publish checksum-verified binaries for macOS, Linux, and Windows plus a bundled Node.js package at @openmetal/cli. See docs/cli.md for installation, authentication, command examples, profiles, npm bootstrap, and CI usage, and docs/runtime.md for the process, filesystem, and endpoint HTTP contracts.
Install the OpenMetal skill to give compatible coding agents detailed CLI and TypeScript SDK guidance for configuring, provisioning, observing, and managing sandboxes:
npx skills add superagent-ai/openmetal --skill openmetalThe skill follows the open Agent Skills specification and works with OpenCode, Claude Code, Codex, Cursor, and other clients supported by the Skills CLI. Its source and reference material are in skills/openmetal.
Shared development secrets live in the committed, encrypted .env. That file is OpenMetal-only: do not mix variables from other repositories. The private decryption key is stored on the superagent-team Dotenvx Armor org, not in git.
Machine-specific values (local Supabase URLs and keys) live in gitignored .env.local.
pnpm exec dotenvx armor login
pnpm supabase:start
pnpm env:localpnpm env:local writes .env.local and apps/web/.env.local from supabase status. It also copies shared Google and GitHub OAuth values from encrypted .env into supabase/.env. Never commit tokens, database passwords, or secret keys.
To change a shared secret:
pnpm exec dotenvx set STRIPE_SECRET_KEY sk_test_... -f .envdotenvx set encrypts the value in place. After the first encrypt, store this repo's private key on superagent-team with pnpm env:armor. pnpm dev and the integration test scripts load .env.local over encrypted .env through dotenvx.
pnpm supabase:start
pnpm supabase:reset
pnpm supabase:stop
pnpm db:typesStudio is at http://127.0.0.1:55323. Local mail (magic links) is at http://127.0.0.1:55324. The API gateway is at http://127.0.0.1:55321 and Postgres is at 127.0.0.1:55322. These host ports avoid clashing with another local Supabase stack.
Supabase migrations in supabase/migrations/ are the canonical schema history. Do not generate a second migration stream with Drizzle Kit.
supabase migration new <name>- Edit the generated SQL
pnpm supabase:reset- Update
packages/db/src/schema.tsso it matches the SQL pnpm db:typesand commit the generatedpackages/db/src/database.types.ts
The dashboard uses @supabase/ssr with a browser client, a server client, and src/proxy.ts session refresh.
- Magic links land on
/auth/confirm, which exchangestoken_hashfor a cookie session. - Google and GitHub OAuth land on
/auth/callback, which exchanges the PKCEcodefor a cookie session.
The dashboard then constructs MetalClient with the current Supabase access token. The API verifies that token with supabase.auth.getClaims() and authorizes from the verified sub claim. It never trusts user_metadata.
OAuth client IDs and secrets belong to Supabase Auth, not the Next.js app. Do not set them as NEXT_PUBLIC_* variables. NEXT_PUBLIC_SITE_URL must be the public app origin so redirectTo points at /auth/callback.
Local Auth reads credentials from supabase/.env via config.toml env() substitution. After changing config.toml, run pnpm supabase:stop then pnpm supabase:start. pnpm supabase:reset is not enough.
Local Auth OAuth
These values live in encrypted .env and are copied to supabase/.env by pnpm env:local.
| Variable | Source |
|---|---|
SUPABASE_AUTH_EXTERNAL_GOOGLE_CLIENT_ID |
Google Cloud OAuth Web client ID |
SUPABASE_AUTH_EXTERNAL_GOOGLE_SECRET |
Google Cloud client secret |
SUPABASE_AUTH_EXTERNAL_GITHUB_CLIENT_ID |
GitHub OAuth App client ID |
SUPABASE_AUTH_EXTERNAL_GITHUB_SECRET |
GitHub OAuth App client secret |
OpenMetal's local Auth callback is http://127.0.0.1:55321/auth/v1/callback, not the CLI callback port 54389.
Google Cloud (Web application)
- Create an OAuth client of type Web application.
- Authorized JavaScript origins:
http://localhost:3100and the production origin. - Authorized redirect URIs must be the Supabase Auth callback, not the Next.js
/auth/callbackroute:- Local:
http://127.0.0.1:55321/auth/v1/callback - Hosted:
https://<project-ref>.supabase.co/auth/v1/callback
- Local:
- Scopes:
openid,userinfo.email,userinfo.profile. - Paste the client ID and secret into encrypted
.env(pnpm exec dotenvx set ... -f .env) and into hosted Authentication → Sign In / Providers → Google.pnpm env:localcopies those values intosupabase/.env. Leave Skip nonce check off in hosted; localconfig.tomlsetsskip_nonce_check = true.
GitHub OAuth App
GitHub allows one callback URL per app, so use separate apps for local and hosted, or configure only the environment you are testing.
- Homepage URL: the app origin.
- Authorization callback URL:
- Local:
http://127.0.0.1:55321/auth/v1/callback - Hosted:
https://<project-ref>.supabase.co/auth/v1/callback
- Local:
Hosted Supabase Dashboard → Authentication → URL Configuration
- Site URL: production app origin (must match
NEXT_PUBLIC_SITE_URL). The dashboard defaulthttp://localhost:3000is wrong for OpenMetal. - Redirect URLs: add
https://<prod>/auth/callback,https://<prod>/auth/callback**, and keep/auth/confirmfor magic links. Addhttp://localhost:3100/auth/callbackandhttp://localhost:3100/auth/callback**if you test hosted Auth from local web.
Enable Google and GitHub under Providers and paste the same credentials. No schema migration is required.
Hosted Auth email (Resend SMTP)
Production Auth emails (magic links, confirmations, and invites) send through Resend. Local Auth still uses Mailpit at http://127.0.0.1:55324.
| Setting | Value |
|---|---|
| Sender | OpenMetal <updates@openmetal.sh> |
| Host | smtp.resend.com |
| Port | 465 |
| Username | resend |
| Password | RESEND_API_KEY in encrypted .env (also stored in hosted Auth SMTP settings) |
Do not run supabase config push after changing hosted SMTP; local config.toml keeps SMTP disabled so developers keep Mailpit.
The API never calls providers. Realtime broadcasts are written by Postgres in the same transaction as their domain event, so they go out only after commit and never for rolled-back work. Duplicate job delivery is safe: domain events are append-only and unique on event_id, and the dashboard deduplicates by event ID and cursor.
import { MetalClient } from "@openmetal/sdk";
const metal = new MetalClient({
baseUrl: "https://api.openmetal.sh",
accessToken: async () => currentSupabaseAccessToken,
});
await metal.health();
await metal.organizations.create({ name: "Northwind", slug: "northwind" });
await metal.projects.create(organizationId, { name: "Alpha", slug: "alpha" });
await metal.events.list({ projectId, after: cursor });The SDK also exposes processes, filesystem, runtimeOperations, and endpoints namespaces for ready sandboxes. The workspace package remains private, while pnpm --filter @openmetal/sdk build:npm produces the self-contained public npm artifact. See packages/sdk-typescript/README.md for SDK usage and publishing details.
Every domain event is broadcast to its private topic, organization:<id> or project:<id>, by calling realtime.send inside the transaction that records it. Supabase Realtime delivers the message only after that transaction commits, so no service needs the Supabase secret key to publish. Clients subscribe with { private: true }. On subscribe or reconnect they call GET /v1/events?project_id=&after=. Realtime is never the only copy of an event.
pnpm lint
pnpm typecheck
pnpm test
pnpm test:integration
pnpm test:rls
pnpm test:realtimeIntegration, RLS, and Realtime tests require local Supabase plus pnpm env:local so .env.local has this machine's keys.
- API not ready: start Supabase before
pnpm dev.GET /readychecks PostgreSQL. - 401 from the API: the dashboard session expired. Refresh the page so proxy can rotate cookies.
- Realtime CHANNEL_ERROR: confirm the user is a member of the project organization and that the channel is private.
- Magic link missing: open Mailpit at
http://127.0.0.1:55324rather than a real mailbox. - OAuth redirect mismatch: provider callback URLs must be
http://127.0.0.1:55321/auth/v1/callbacklocally. The app route is/auth/callback. - Port already allocated:
pnpm supabase:stopthen start again. API uses 4000, web uses 3100. - Encrypted
.envwill not decrypt: runpnpm exec dotenvx armor loginas a member of superagent-team. Do not copy keys from other repositories into this.env. supabase startmissing private key: usepnpm supabase:start, not baresupabase start. The CLI reads the encrypted root.envand needs dotenvx to decrypt OAuth secrets first.
pnpm dev
pnpm build
pnpm lint
pnpm typecheck
pnpm test
pnpm test:integration
pnpm test:rls
pnpm test:realtime
pnpm format
pnpm format:check
pnpm supabase:start
pnpm supabase:stop
pnpm supabase:reset
pnpm env:local
pnpm env:encrypt
pnpm env:armor
pnpm db:typesSee CONTRIBUTING.md for development setup, validation, and pull request guidelines.
Report vulnerabilities privately through GitHub Security Advisories. See SECURITY.md for the reporting policy.
OpenMetal is available under the MIT License.