Docs

Benmore documentation

Benmore turns a data model, a little YAML config, and your frontend into a complete, hosted web app - with authentication, a typed REST + real-time API, role-based access, validation, file storage, and a generated client SDK. You describe what you want; Benmore runs it.

The mental model, in one line:

Schema in Prisma. Config in YAML. UI in TSX (auto-compiled). Server logic (when you need it) in YAML hooks, flows, and cron.

You can build entirely by describing your app in chat (connect Benmore to Claude, ChatGPT, or Cursor via MCP), from a terminal coding agent like Claude Code or Codex (the CLI), or by editing files yourself. Every app you create is hosted on <your-app>.benmore.ai with automatic HTTPS, daily backups, and per-app data isolation.

What you get, automatically

A typed API for every table
CRUD, search, batch, pagination, and a generated TypeScript SDK - no endpoints to hand-write.
Auth & access control
Email/password + Google, sessions, MFA, multi-role RBAC, and per-row scoping out of the box.
Real-time
SSE + WebSocket rooms; changes broadcast to the right clients with one line of SDK.
Server logic without a server
Declarative hooks, flows, workflows, and cron - plus sandboxed JS when math doesn't fit SQL.

Getting started

There are two ways to drive Benmore. Both start with a free account. Per-client walkthroughs (Claude on the web, Claude Desktop, Claude Code, Codex) live in the connect guide.

1 · Chat clients (MCP)

Add Benmore as a Model Context Protocol connector in your assistant, then describe the app you want. It builds and deploys for you.

MCP server URL
https://benmore.ai/mcp

2 · Claude Code & Codex (the CLI)

Install the CLI and your terminal coding agent builds, runs, and deploys Benmore apps from the command line.

macOS & Linux
brew install benmore-studio/benmore/benmore
benmore bootstrap        # set up ~/Benmore, install the skill, sign in, sync your apps

benmore bootstrap creates your ~/Benmore workspace, installs the Claude Code skill, signs you in, and pulls any apps you've already deployed into ~/Benmore/<app>. On a new machine, benmore sync restores the whole workspace.

App anatomy

An app is a directory. Here's everything it can contain - most files are optional.

myapp/
myapp/
├── app.yaml          # config: theme, auth, features, access, roles, aggregates
├── schema.prisma     # data model → compiled to SQLite + migrations
├── src/bm.d.ts       # generated TypeScript types for your schema (don't hand-edit)
├── static/           # YOUR frontend
│   ├── index.html    # served at /        (CSRF meta + import-map injected)
│   ├── login.html    # served at /login   (clean-URL routing)
│   ├── app.tsx       # entry - compiled on the fly → /static/app.js
│   └── styles.css
├── flows.yaml        # optional: custom HTTP routes (or flows/*.yaml)
├── hooks.yaml        # optional: before/after-CRUD side effects
├── workflows.yaml    # optional: state machines
├── cron.yaml         # optional: scheduled jobs
├── encrypted.yaml    # optional: field-level encryption
├── emails/*.html     # optional: email templates
├── i18n/<lang>.yaml  # optional: translations
├── env.yaml          # secrets (never deployed; set via `benmore env set`)
└── data.db           # SQLite - created automatically

The build loop

  1. Describe or scaffold. Ask your AI to build something, or run benmore new myapp.
  2. Edit. Change schema.prisma, app.yaml, or static/*.tsx. TSX recompiles on the next request (~10 ms).
  3. Deploy. benmore deploy (or the MCP tool) ships it to <app>.benmore.ai. Schema changes migrate automatically with a pre-migration backup.
  4. Iterate. Every push auto-commits to the app's git history - benmore git log/show/revert to roll back.

Data model - schema.prisma

Standard Prisma, compiled to SQLite. Every model becomes a table with a full CRUD API at /api/<table>.

schema.prisma
model Note {
  id        Int      @id @default(autoincrement())
  title     String
  body      String   @default("")
  status    String   @default("draft")
  userId    Int      @map("user_id")
  createdAt DateTime @default(now()) @map("created_at")
  updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
  deletedAt DateTime? @map("deleted_at")   // presence enables soft-delete

  @@index([userId, updatedAt])
  @@fulltext([title, body])                // enables /api/notes/search?q=...
}

Auto-CRUD API

For every table, with no code:

MethodPathWhat it does
GET/api/<table>List (auto-scoped to the caller)
GET/api/<table>/{id}Single row
POST/api/<table>Create (user_id auto-set); returns the full inserted row
PATCH/api/<table>/{id}Partial update
DELETE/api/<table>/{id}Delete (soft if a deleted_at column exists)
POST/PATCH/DELETE /api/<table>/batchBulk operations
POST /api/<table>/ingestNDJSON streaming ingest (rate-limited)
GET /api/<table>/search?q=…FTS5 search (needs @@fulltext)

Protected fields - user_id, role, password_hash, created_at, updated_at are stripped from any client body; the server sets them. Dropped fields are listed in the X-Stripped-Fields response header. Mutations from cookie sessions require an X-CSRF-Token header (auto-handled by the SDK); Bearer-auth requests don't.

Querying & pagination

List endpoints accept these query params:

GET /api/notes?…
?orderBy=created_at:desc      # sort
&limit=50&page=2              # offset pagination
&cursor=XYZ&limit=50          # keyset pagination (stable under inserts)
&where[status]=active         # equality filter
&where[amount__gt]=100        # operators: __gt __lt __gte __lte __in __like
&q=keyword                    # FTS5 (on @@fulltext tables)
&count=true                   # {count: N} - true total, ignores the row cap
&as_of=2026-01-01T00:00:00Z   # point-in-time (versioned tables)
&include=author               # expand a relation

List responses cap at 50 rows by default - use ?count=true for a true total, or paginate. Filters on encrypted columns are restricted to equality (via the blind index); range/LIKE on ciphertext is dropped rather than returning misleading results.

Versioning, soft-delete & concurrency

FeatureHow
Soft deleteAdd a deleted_at column → DELETE flips it; reads hide deleted rows; restore via the SDK.
Optimistic concurrencySend _expected_updated_at on PATCH; a stale value returns 409 Conflict instead of silently overwriting.
Content versioningGET /api/<t>/{id}/versions, POST …/revert/{version}, and ?as_of=<iso> point-in-time reads.
Record lockingPessimistic locks: POST/DELETE/GET /api/<t>/{id}/lock with automatic expiry.
IdempotencySend X-Idempotency-Key on POST to make double-submits safe.
Audit trailEvery mutation is logged with actor + before/after values; query at GET /api/_audit (admin).
Retentionretention.yaml sets TTL policies that auto-clean old rows.

Field-level encryption & blind index

Declare sensitive columns in encrypted.yaml. Values are AES-GCM encrypted at rest and unmasked only for authorized roles.

encrypted.yaml
paychecks:
  fields: [wages_cents, ssn]   # works on TEXT, INTEGER, and REAL columns
  blind_index: [ssn]           # deterministic HMAC sibling → equality search
  unmask_roles: [admin, payroll]

Aggregates, computed & custom fields

app.yaml
aggregates:
  total_revenue:
    sql: "SELECT SUM(amount) AS v FROM orders WHERE status='paid'"
    refresh: "5m"

Configuration - app.yaml

One file holds all app config.

app.yaml
site_name: "My App"
theme: zinc          # color theme
mode: dark           # light | dark
font: "Inter"

auth:
  identifier: email          # email | phone | username
  session_duration: "30d"
  signup_fields: "first_name,last_name"
  require_verified: false
  oauth:
    google: true

roles:
  viewer: "contacts:read"
  manager: "contacts:*, reports:read"
  admin: "*"

features:
  testing: false             # in-app visitor-feedback widget
  analytics: true

access:
  contacts: { read: self, write: self, update: self, delete: admin }
  announcements: { read: anon, write: admin }

groups:                      # multi-tenant data isolation
  table: organizations
  key: org_id
  user_field: org_id

seo:
  description: "…"
  image: "https://myapp.benmore.ai/static/og.png"   # link-unfurl preview

backup: { interval: "24h", keep: 7 }

Other keys: brand, nav.style, pwa.{name,icon,offline}, auth.{otp,domain,redirect,mfa}, features.{admin,sse,ws,ws_anonymous}, auto_memberships, frontend.stack (html default, or gotmpl for SSR/SEO pages).

Auth & sessions

Access control & RBAC

Authorization is enforced server-side. Set per-table modes in app.yaml's access: block:

ModeWho can
publicAnyone, including signed-out visitors (read-only feeds)
anonSigned-out writes allowed (e.g. a contact form)
selfOnly the row's owner (user_id match)
adminAdmins only
role:a,bAny of the listed roles
owner_or_role:a,bTiered: the row's owner sees their own; a listed role sees the whole group (the common "owner sees own, manager sees more" shape)
member-of:<table>(<join>,<user>)Visible only to members of the parent row (membership-table EXISTS guard on every per-row surface)
perm:<resource>.<action>Granular permission against the unioned scope set

Multi-tenant groups

The groups: block isolates each customer/team/org's data. Set a key column (e.g. org_id) on tenant-scoped tables and the framework filters every query by the caller's effective group - including admins acting on behalf of a tenant. auto_memberships can auto-join every active user to rows of a parent table.

The frontend - TSX & the build pipeline

Your UI lives in static/. The framework serves files with clean-URL routing (/contactsstatic/contacts.html), compiles *.tsx on the fly via embedded esbuild (no Node, no build step), and injects three things into every served HTML page:

Want React/Preact? import it from a CDN in your .tsx - the framework treats static/ as opaque. For SEO-critical marketing pages, set frontend.stack: gotmpl for server-rendered HTML.

The bm SDK

A thin, fully-typed wrapper over the REST/SSE/WS surface - generated from your schema, so bm.table('typo') is a compile error and per-table features drive conditional methods.

static/app.tsx
import bm, { type User, type Post, type ChangeEvent } from 'bm';

const me: User | null = await bm.auth.me();
if (!me) { location.href = '/login.html'; }

const posts: Post[] = await bm.table('posts').list({ limit: 50 });
await bm.table('posts').create({ title: 'Hello' });   // returns the new row

// Real-time: refetch when the table changes (also refetch after your own writes)
bm.live('posts', (ev: ChangeEvent<Post>) => refresh());

await bm.flows.publishPost({ id: posts[0].id });       // custom flow, typed
const total = await bm.aggregate('total_posts');       // materialized aggregate

const room = bm.room('lobby');                          // WebSocket room
room.on('hello', (payload, from) => console.log(from));
room.send({ kind: 'hello' });
MethodNotes
bm.auth.{me, signIn, signUp, signOut}Session lifecycle
bm.table(t).{list, get, create, update, delete}Typed CRUD
bm.table(t).{restore, versions, revertTo, list({q})}Conditional on table features
bm.table(t).count(opts?)True row count (list caps at 50)
bm.live(t, cb) · bm.live.scoped(t, fn, opts)SSE; refetch-safe variant
bm.room(name)WS room - chat / presence / signaling
bm.broadcast.{publish, subscribe, stop}SFU livestream (1:N)
bm.flows.<name>(params, body)One typed method per HTTP flow
bm.workflow(t, id).{transitionTo, available, current}State machine
bm.aggregate(n) · bm.aggregates.all()Materialized aggregates
bm.notifications.{list, markRead, onNew}In-app inbox
bm.upload(file, opts)Multipart upload → {path}
bm.api.optimistic({apply, request, snapshot, revert})Optimistic mutation + reconcile
bm.presence(slug) · bm.cache.namespaced(n, v)Live presence; self-busting cache
bm.permissions.{share, revoke, list} · bm.signedUrl(p, ttl)Per-row ACLs; signed URLs
bm.markdown(t) · bm.t(key, vars) · bm.mfa.*Safe Markdown; i18n; TOTP
bm.api.{get, post, patch, delete}Raw escape hatch

Components & auto-served libraries

Drop-in script tags, served from the platform (no CDN needed): TailwindHTMXAlpineChart.jsMermaidLucide at /_internal/*. A schema-driven component layer (DataTable, RecordForm, charts, etc.) can introspect /api/_schema to render tables and forms directly from your column types.

Hooks - hooks.yaml

React to data changes. before_* hooks are synchronous and can abort a mutation; on_* hooks run async side effects (SQL, webhook, email, notify).

hooks.yaml
before_insert:
  orders:
    - error: "Amount must be positive"
      when: "{{amount}} <= 0"

on_update:
  orders:
    - notify: "{{user_id}}"
      title: "Order {{id}} is now {{status}}"
      when: "{{status}} != {{old_status}}"
    - sql: "UPDATE inventory SET qty = qty - {{qty}} WHERE sku = {{sku}}"

Hook templates see session context ({{user_id}}, {{user_email}}, {{user_role}}, {{user_group_id}}) and update deltas ({{old_status}}, …).

Flows - custom HTTP routes

A flow is a named HTTP route built from ordered steps. Define in flows.yaml (or flows/*.yaml); each flow becomes a typed bm.flows.<name>() method.

flows.yaml
publish_post:
  on: { method: POST, path: /api/posts/{id}/publish }
  steps:
    - run: sql
      query: "UPDATE posts SET status='published' WHERE id={{params.id}} AND user_id={{user_id}}"
    - run: api
      url: "https://hooks.example.com/notify"
      method: POST
      body: { post: "{{params.id}}" }
      success_when: "{{ steps.notify.outputs.status }} == 200"
    - run: respond
      json: { ok: true, id: "{{params.id}}" }

Server-side compute

For logic that doesn't fit SQL - iterative math, financial models, scoring, simulation - a run: compute step runs a TS/JS function from static/<module>.ts server-side in a sandboxed engine (no fetch, fs, or process; 5 s timeout). The same module can import client-side for instant recompute and run server-side for the authoritative result - one algorithm, byte-identical math on both sides.

Outbound signing & inbound verify

Workflows - state machines

workflows.yaml defines states, transitions, role guards, timeouts, and on-enter hooks. Drive them with POST /api/<table>/{id}/transition (or bm.workflow(...)); GET …/transitions returns the valid next states for the current user.

workflows.yaml
orders:
  field: status
  states: [draft, submitted, approved, shipped]
  transitions:
    - { from: draft, to: submitted }
    - { from: submitted, to: approved, role: manager }
    - { from: approved, to: shipped, role: admin }

Real-time - SSE & WebSocket

WebRTC & livestream

Files, uploads & PDF

Notifications & webhooks

Edges

An edge is a single self-contained HTML document hosted on its own isolated origin (edge-<slug>.benmore.ai) - a shareable mini-tool, calculator, form, or demo. Because the origin is distinct, an edge can't read the parent app's cookies or storage. The only channel back is a strict window.bm.api bridge (anonymous by default; acting as the viewer requires explicit consent against owner-declared scopes).

Dev / prod environments

One logical app can run two instances - dev (<sub>-dev.benmore.ai) and prod (<sub>.benmore.ai) - each with its own data, git history, and secrets.

CommandWhat it does
benmore promote <app>Ship code dev → prod (never data/secrets); prod data migrated with a pre-migrate backup. Refuses destructive schema changes without --force; --dry-run previews.
benmore seed-dev <app>Give an existing prod app a dev instance (code, fresh data). Prod untouched.
benmore refresh-dev <app>Pull prod → dev (files / data / both), with schema reconciliation.
benmore use dev|prodPin which environment every command targets; --env overrides per call.

Secrets and env vars are per-environment by default (dev holds test keys, prod the live ones); collaborators, billing, and custom domains stay per logical app. Custom domains always map to prod.

Deploy & hosting

Security

Security controls are enforced by the runtime, not left to app code:

CLI reference

common commands
# setup
benmore bootstrap            # workspace + skill + login + sync apps
benmore login / logout / whoami
benmore sync                 # pull all your deployed apps into ~/Benmore/

# build & ship
benmore new myapp            # scaffold
benmore serve myapp          # run locally
benmore deploy               # go live on benmore.ai
benmore push           # ship a single file
benmore pull  [dir]     # pull deployed source

# inspect
benmore apps                 # list deployed apps
benmore logs  / tail 
benmore sql  "SELECT …" [--write]
benmore api  [--at PATH] [--call BODY]
benmore probe    [--as EMAIL]
benmore describe  
benmore status  / diff    # dev↔prod drift (files, schema, env/payment keys)
benmore doctor                 # release-readiness PASS/WARN report

# environments
benmore use dev|prod
benmore promote  / seed-dev  / refresh-dev 

# ops
benmore env  set K=V / unset K
benmore domain  
benmore collaborators  [add|remove user@x]
benmore git log|show|revert 
benmore disable  / enable 

MCP reference

The hosted MCP server at https://benmore.ai/mcp exposes the same surface to AI clients. Key tools:

Once connected, just describe what you want - "build me a CRM with contacts, deals, and a Kanban board" - and the agent uses these tools to scaffold, edit, and deploy. api(at:<topic>) is the source of truth for every feature's exact request/response shape.

Questions? [email protected] · Create a free account →