Dynamic schema, permissions DSL, REST + GraphQL, realtime, edge functions, vector search, and a built-in MCP server for AI agents — one codebase that runs on Bun, Workers, Vercel, and Netlify. Self-host or use the managed Cloud.
No black box. The collection editor in the admin runs the same DDL your migrations would have. The permission DSL the admin writes is the same string your client sends as ?filter.
Define collections in the admin, each one becomes a physical c_<slug> table. CREATE TABLE runs against your live DB, no redeploy.
JSON predicates that compile to Drizzle SQL fragments AND match in-process for realtime. Same syntax in roles, REST ?filter, and GraphQL.
Every collection auto-exposes /api/items/:slug with filter/sort/expand/projection. GraphQL schema synthesised from the same metadata.
SSE in Bun, Durable Object + WebSocket on Workers. Per-event permission filter means subscribers see only what their role can read.
sse · durable objectsJavaScript sandbox with a host bridge for ctx.db / fetch / email. Bun worker thread, QuickJS-WASM, or remote HTTP — auto-selected per runtime.
Local FS in dev, R2 / S3 on edge. Image resize via Bun.Image, CF Image Resizing, or passthrough — same URL shape.
HMAC-signed outbound webhooks per event, plus a visual flow builder for branching multi-step automations. Fire-and-forget — never blocks API responses.
items.* · auth.* · cronbetter-auth wrapper: email, OAuth (Google/GitHub/Apple), magic link, OTP, passkey, SAML 2.0 SSO, LDAP / AD. First user gets admin.
One activity table, dot-namespaced actions (item.update, auth.login). Every CRUD writes a row to revisions — point-in-time restore included.
The same apps/web source compiles to Bun, Cloudflare Workers, Vercel Functions, and Netlify Functions. context.ts::buildContext picks the right adapter at boot.
Self-host on a VPS, Docker, or your laptop. Single long-running process.
Cloudflare edge. Free tier covers most of it. DO-backed realtime.
Functions on Node 22. Neon HTTP for DB. Built via Build Output API.
Functions on Node 22. Scheduled functions tick cron. Same Neon HTTP.
Deploy to Cloudflare Workers and your API runs in 330+ cities on six continents. The request never crosses an ocean to find its origin — D1 reads, Durable Object subscribers, and R2 object fetches happen at the city closest to the user.
SQLite-shaped reads run at the colo that holds the replica. Writes ship to the primary asynchronously; reads never cross-region.
Each items:<slug> channel pins to a DO. Subscribers connect to the closest DO via WebSocket; SSE bridges to clients.
Object reads served from R2 with zero egress. Image transforms run via Cloudflare Images at the PoP; payload never travels twice.
Same MIT codebase, same admin, same API — running on Cloudflare's Workers + D1 + R2. 30 seconds from signup to first deploy, 50ms first request, 330+ edge cities. One-click export if you ever want to bring it back home.
3 regions by default (IAD, FRA, IST). Up to 11 on Scale. Optional region pinning for data residency.
anycast · region pinDaily D1 snapshots, 7-day rollback (Pro), 30 days (Scale). One-click restore from the admin.
d1 snapshot · r2 versioningBring your own domain. Certificates rotate on Cloudflare. No add-on fee, no config.
api.acme.app · cnameGit push → ephemeral environment. Same admin, isolated DB and storage. Auto-cleanup on PR close.
preview-247.workeros.devRequests, CPU, D1 r/w, R2, bandwidth — live in dashboard. Email at 80% and 100%, no surprise bills.
slack · email · webhooksOne-click full export — schema + data + storage. Same MIT workeros, so your own server runs it in minutes.
sql + r2 archive · 1-clickMIT, on GitHub. Your server, your credentials.
Same workeros, we run it. On Cloudflare.
Numbers from Cloudflare's vendor docs — same numbers you see here. No marketing copy.
| Resource | Free | Pro | Scale | Powered by |
|---|---|---|---|---|
| Monthly requests | 100K | 10M | 100M | CF Workers |
| CPU / request | 10ms | 30s | 5min | CF Workers |
| DB size (D1) | 500 MB | 5 GB | 50 GB | CF D1 |
| Storage (R2) | 1 GB | 25 GB | 500 GB | CF R2 |
| Egress | ∞ | ∞ | ∞ | R2 zero egress |
| Realtime conn. | 50 | 5K | 50K | CF DO |
| Postgres option | — | — | Neon add-on | Neon |
SAML SSO, SCIM provisioning, SOC 2 (in progress), DPA + MSA, dedicated single-tenant cluster with region pinning. Annual invoicing for finance, redlines for legal.
# clone, install, run — one terminal $ git clone github.com/your/workeros && cd workeros $ bun install $ cp apps/web/.dev.vars.example apps/web/.dev.vars # migrate local sqlite, then start vite + miniflare $ bun run db:migrate:sqlite $ bun run dev ▸ ready in 2.3s on http://localhost:5173 ▸ sign up the first user at /sign-up ▸ first user auto-gets the admin role
# list items from the auto-generated REST endpoint $ curl 'http://localhost:5173/api/items/posts?limit=10' \ --cookie "$(cat /tmp/cookie.txt)" # filter via the permissions DSL $ curl "http://localhost:5173/api/items/posts?filter=$(echo \ '{"status":{"_eq":"published"},"views":{"_gt":100}}' | jq -sRr @uri)&sort=-views" # or graphql $ curl -X POST http://localhost:5173/api/graphql \ -H 'content-type: application/json' \ -d '{"query":"{ posts(sort:\"-views\", limit:5) { id title views } }"}'
// 1. generate types from your running instance $ bun workeros gen-types http://localhost:5173 --out src/types.ts // 2. use the SDK — fully typed import { createClient } from "@workeros/client"; import type { Posts } from "./types"; const wks = createClient({ url: "http://localhost:5173" }); const r = await wks.from<Posts>("posts").list({ filter: { status: { _eq: "published" } }, sort: "-views", limit: 10, }); r.data[0].title; // string — inferred from your schema
admin.
better-auth's databaseHooks.user.create.after counts users; if it's 1, the role is admin, else authenticated. No bootstrap script, no env var, no command-line invitation flow to manage.
From there, define a collection in the admin and the API endpoint exists immediately — no redeploy, no codegen step, no schema versioning gymnastics. Drop a column, the column is gone. Rename a collection, the API path moves with it.
Natural-language prompts compile into the same Directus-shaped filter your client sends — every plan is reviewable, every tool call runs through your permissions DSL. A streamable MCP server ships in the box, so Claude, Cursor, or any agent can read & write through the same guards.
app_users with revenue sort. No write semantics detected — read-only plan.
{ "collection": "app_users", "filter": { "last_order_at": { "_gte": $NOW(-30 days) } }, "sort": ["-lifetime_value"], "limit": 10, "fields": ["id", "name", "email", "lifetime_value", "last_order_at"] }
| name | lifetime_value | last_order_at | |
|---|---|---|---|
| Lena Voss | lena@hexagon.studio | $28,142 | 2026-05-22 |
| Theo Ranganathan | theo@frostgate.io | $22,005 | 2026-05-19 |
| Yuki Okafor | yuki@altair-labs.com | $18,830 | 2026-05-23 |
| Mira Halvorsen | mira@palebrook.co | $17,210 | 2026-05-21 |
| Salim Tessari | salim@northbore.org | $14,902 | 2026-05-18 |
Every admin capability is also an MCP tool — grouped, type-validated, and DSL-filtered before it runs.
Bind an allowlist of tools and a read-only flag to any personal access key. Agents inherit the guards of the key they authenticated with.
# patch the active key's guards $ curl -X PATCH $URL/api-keys/$id/mcp-guards \ -d '{"mcpReadOnly":true, "mcpTools":["ai.query", "collections.list",…]}'
Stateless POST /mcp for tenant agents, /api/admin/mcp for ops bots. JSON-RPC over HTTP — no SSE, no socket churn.
{ "mcpServers": { "workeros": { "command": "bun", "args": [ "workeros", "mcp", "--url", "$URL/mcp", "--key", "pak_…" ] } } }
@modelcontextprotocol/sdk · model picker per workspace · falls back to ANTHROPIC_API_KEY Every cross-runtime concern hides behind an interface in @workeros/core/adapters. Pick the runtime, the adapters fall into place.
Other tools do parts of this. workeros's design choice is to keep every part under one source tree, behind one API, deployable to four runtimes — without forking.
| Capability | workeros | Supabase | Directus | Roll-your-own |
|---|---|---|---|---|
| Self-host, MIT licensed | ✓ | partial | ✓ | ✓ |
| Runs on Cloudflare Workers | ✓ | no | no | if you build it |
| Dynamic schema (runtime DDL) | ✓ | migrations only | ✓ | depends |
| Adopt existing tables without DDL | ✓ | no | limited | depends |
| Permissions DSL that compiles to SQL | ✓ | RLS | ✓ | build it |
| Per-event realtime permission filter | ✓ | RLS-based | no | build it |
| Edge functions sandbox + host bridge | ✓ | ✓ | no | build it |
| Visual flow builder | ✓ | no | ✓ | no |
| SAML 2.0 + LDAP / AD | ✓ | enterprise | enterprise | build it |
| Vendor-managed cloud option | self-host only | ✓ | ✓ | — |
No vendor-managed cloud is on purpose — every team we've talked to wanted to own the DB credentials themselves. If you'd like one, fork and run it.
Everything an operator needs lives behind one URL. The schema editor writes the same DDL your migrations would have. The permission DSL the admin writes is the same string your client sends as a query parameter. There is no second layer to learn.
— from docs/DESIGN.md Five-minute getting-started, the permission DSL spec, four deploy walkthroughs, and a sandbox guide. All in one place.