
The backend framework you own. Resources, policies, auth, devtools and an upgrade path for Node.js + MongoDB, with every line of framework code living in your repo.
npm create calmapi@latest my-api
cd my-api
npm run dev CalmAPI my-api v0.1.0 (development)
➜ API http://localhost:5001/api
➜ OpenAPI http://localhost:5001/api/openapi.json
➜ Devtools http://localhost:5001/_calm
Status: 2.0.0-beta. APIs may still change before 2.0. CalmAPI 1.x users: run
npx calmapi@next doctorinside your project (see Security). The 1.x source lives on thev1branch, andnpm install calmapistill gives 1.x until 2.0 is final.
Documentation: https://docs.calmapi.dev
- Nothing hidden in node_modules. The framework is copied into
calm/when you create a project. Read it, debug it, change it. A new project has two runtime dependencies:expressandmongoose. - Owned, but upgradeable.
calm.lockremembers the exact framework files you started from;calm upgradethree-way merges upstream fixes into your edited copy instead of overwriting it. - Declare a field once. One
f.*()line drives the Mongoose schema, input validation, what the API returns, which filters and sorts are allowed, and the OpenAPI schema. - Secure by default, and provably so. Hidden fields cannot leak (even through populated references),
every route has an access policy, list queries are allowlisted, sessions and reset tokens are hashed,
logins lock per account, common passwords are refused, and anything but
development/testruns as production..encrypted()fields are AES-256-GCM at rest.calm auditattacks your own API as every role on a throwaway database and proves the policies hold. - Devtools. A local dashboard at
/_calm: every request with its MongoDB queries and timings, explain plans, a privacy lens on your data, sample data, a mail catcher, push notifications on simulated phones, "act as" any user, what each block adds, and doctor findings. - Agent native, human visible. Projects ship with
AGENTS.md;calm mcpgives coding agents tools to scaffold, call the API as any user, run tests, doctor and the security audit. Everything an agent does (tool calls, file diffs, restarts, crashes, requests) streams live into devtools, so you watch it work. - Blocks, shadcn style.
calm add notificationscopies production code (in-app inbox plus push plus email, webhooks, API keys, feature flags, a job queue, file storage, realtime...) into your project. You own that code too, and building and sharing your own block takes one command.
// app/resources/post/post.resource.js
import { resource, f } from '#calm';
export default resource('post', {
fields: {
title: f.string().required().max(200).searchable().sortable(),
body: f.text().required(),
status: f.enum(['draft', 'published']).default('draft').filterable(),
tags: f.array(f.string()).filterable(),
author: f.ref('user').owner(), // set from the session, powers 'owner'
internalNote: f.string().hidden(), // stored, never returned or accepted
},
policy: {
list: 'public',
read: 'public',
create: 'authenticated',
update: 'owner | admin',
delete: 'owner | admin',
},
});That file alone gives you:
| Route | Notes |
|---|---|
GET /api/posts |
?status=published&tags[in]=a,b&q=hello&sort=-createdAt&page=2&limit=20&include=author&fields=title |
GET /api/posts/:id |
?include=author populates with the author's public fields only |
POST /api/posts |
validated, unknown/read-only fields rejected with 422, owner set automatically, 201 |
PATCH /api/posts/:id |
partial update; 403 for others who can read it, 404 for those who cannot |
DELETE /api/posts/:id |
204 |
Add business logic in a service, custom endpoints in a routes file:
// post.service.js
import { ResourceService, hasRole } from '#calm';
export default class PostService extends ResourceService {
scope({ user } = {}) { // applied to every query
if (hasRole(user, 'admin')) return {};
return user ? { $or: [{ status: 'published' }, { author: user.id }] } : { status: 'published' };
}
async beforeCreate(data) {
return { ...data, slug: slugify(data.title) };
}
}
// post.routes.js (mounted before CRUD, so /featured never hits /:id)
export default (r) => {
r.post('/:id/publish', { policy: 'owner | admin' }, (ctx) =>
ctx.service.update(ctx.params.id, { status: 'published' }, ctx));
};Handlers return values ({ data } responses, undefined is 204) and throw notFound(),
forbidden(), validationError({...}). Express 5 forwards async errors, so there is no try/catch.
f.string() text() email() url() password() number() integer() boolean() date() enum([...]) id() ref('name') array(f) object({...}) json()
Modifiers: .required() .default(v) .min(n) .max(n) .pattern(re) .unique() .index() .check(fn) .schema(zodOrAnyStandardSchema)
Visibility: .hidden() .writeOnly() .readOnly() .immutable() .writableBy('admin') .visibleTo('owner | admin')
Queries: .filterable([ops]) .sortable() .searchable() · Relations: .owner() .populate()
'public', 'authenticated', 'owner', 'admin' or any role, 'none', combined with |, or a
function ({ user, doc, action, req }) => boolean. Policies apply to CRUD, custom routes, field
visibility, realtime events and MCP tool calls alike.
calm create [dir] new project (npm create calmapi@latest)
calm dev [--agent-db] watch mode + devtools (--agent-db: run on a disposable copy of the database)
calm make resource <name> [fields...] e.g. product name:string+required price:number status:enum:draft,live=draft category:ref:category
calm add <block...> copy blocks into your project (calm add --list, gh:user/repo/name)
calm make block <name> start your own block
calm block list|check|pack your blocks: what changed, a safety review, a file to share
calm seed [resource...] sample records that look real (--count, --seed)
calm routes every route with its policy and source file
calm openapi write openapi.json
calm client [--out file] [--js] typed, dependency-free client for your web or mobile app
calm doctor [--prod] [--ci] [--audit] security and correctness checks
calm audit [--ci] test every route as every role on a throwaway database
calm audit --accept --reason "..." accept the exposure matrix as the budget (security.snapshot.json)
calm review [--sign] [--ci] what changed since the last signed review; sign it
calm diff [--api] [--ci] behavior and API contract changes (breaking, minor, patch)
calm replay [--writes] [--ci] replay recorded traffic against the current code
calm budgets [--ci] queries, p95 and response size per route against calm.config.js budgets
calm checkpoint [name] / calm restore save and restore the code and the data
calm about everything the framework does that you did not write
calm admin <email> [--revoke] grant the admin role
calm keygen [--write] encryption key for .encrypted() fields (rotates)
calm upgrade merge framework updates into calm/
calm mcp MCP server for coding agents (claude mcp add calm -- npx calm mcp)
| Block | What you get |
|---|---|
notifications |
ctx.notify(user, 'order-shipped', { order }): in-app inbox, push and email in one call, with preferences and quiet hours |
webhooks |
outgoing webhooks for resource events: signed (Standard Webhooks), retried, logged, SSRF safe |
api-keys |
personal and service API keys with scopes, hashed at rest, expiry and revoke |
magic-link |
passwordless sign-in by email, bound to the browser that asked |
feature-flags |
kill switches, gradual rollouts, targeting, variants, route gates |
privacy |
users export their data and delete their account (delete, anonymize or keep per resource) |
idempotency |
the Idempotency-Key header: safe retries for payments and other writes |
jobs |
MongoDB job queue and cron (no Redis): retries, schedules, event triggers, admin API |
storage-local / storage-s3 |
uploads with type/size allowlists and safe downloads; S3/R2/MinIO via presigned URLs |
realtime-sse |
live resource events, filtered per subscriber by the same policies |
mcp-server |
your resources as MCP tools for AI agents, enforced by your API's own rules |
audit-log |
who changed what, with field diffs (private values never logged) |
soft-delete |
softDelete: true per resource, trash and restore |
totp |
two-factor authentication with authenticator apps |
api-docs |
interactive reference at /docs |
mail-resend / mail-smtp / mail-postmark |
email delivery |
push-expo / push-fcm |
push notification delivery through Expo or Firebase Cloud Messaging |
rate-limit-mongo |
shared rate limits across instances |
oauth |
sign in with Google, GitHub or any OpenID Connect provider (PKCE, verified ID tokens, safe account linking) |
payments-stripe |
Stripe Checkout, subscriptions and the customer portal, verified webhooks, plan-gated routes |
teams |
organizations, roles, invitations and team-scoped resources (team: true) with strict isolation |
email-verification |
verify emails after sign-up and changes; a verified policy |
captcha |
Turnstile, hCaptcha or reCAPTCHA v3 on sign-up, sign-in and reset |
cache |
per-identity response caching with invalidation, ETag and 304 |
sms-twilio |
SMS and phone verification codes |
slack / sentry |
events and security alerts to Slack; errors to Sentry |
search-meilisearch |
full-text search that respects your policies |
waitlist / export / maintenance |
launch waitlists with invites; CSV and JSON export and import; maintenance and read-only modes |
// app/blocks/maintenance/index.js (npx calm make block maintenance)
import { definePlugin, CalmError } from '#calm';
export default definePlugin({
name: 'maintenance',
env: { MAINTENANCE: 'Set to "on" to answer 503 on every route' },
setup(calm) {
calm.guard(() => {
if (calm.env('MAINTENANCE') === 'on') throw new CalmError(503, 'MAINTENANCE', 'Back soon');
});
},
});Blocks can add routes, resources, guards (calm.guard, which can stop or answer a request), other
credentials (calm.authenticate), named policies (calm.policy), login challenges, push and mail
transports, doctor checks and devtools panels. calm block pack turns a block into a registry file; others
install it with calm add gh:you/repo/maintenance, after a plain-language review of what its code does.
Blocks use the shadcn registry item format, and private registries
work too: add "calm": { "registries": { "@acme": "https://acme.dev/r/{name}.json" } } to package.json
and run calm add @acme/thing. Building blocks
| Exposure matrix | per resource and role (anonymous, user, owner, admin, custom roles): allowed actions and visible/writable fields, computed from your code |
calm audit |
calls every route as every role on a throwaway database; checks statuses, owner-only lists, field leaks, mass assignment, query injection, token handling |
.encrypted() |
AES-256-GCM at rest, transparent on read/write/populate, key rotation with calm keygen --write |
| Auth hardening | hashed opaque sessions, scrypt, per-account lockout without account enumeration, common/personal password rules, single-use reset tokens |
calm add totp |
authenticator-app 2FA with hashed recovery codes and replay protection |
calm doctor |
config, policies, personal data exposure, hardcoded secrets, .env in git, npm audit, advisories |
security.* events |
failed logins, lockouts, rejected writes, forbidden access, rate limits, bad tokens: live in devtools, hook them for alerts |
POST /api/auth/register · login · logout · logout-all · password · password/forgot · password/reset,
GET|PATCH /api/auth/me, GET|DELETE /api/auth/sessions. Opaque session tokens (SHA-256 at rest),
scrypt password hashing from node:crypto, no account enumeration, single-use reset tokens that
revoke every session, rate limited. Apps register push tokens with POST|GET /api/auth/devices.
await ctx.push.send({ to: order.customer, title: 'Your order shipped', body: 'Arrives Thursday', url: `/orders/${order.id}` });In development nothing is delivered: devtools shows each notification on an iPhone lock screen, an
Android notification shade and a desktop browser, with the APNs, FCM, Expo and Web Push payloads, size
and content warnings and a delivery log. Give any user simulated devices that behave like real ones
(offline, muted, uninstalled, invalid token, rate limited), rotate or revoke their tokens, and open each
one as a phone with its own inbox. calm add push-expo and calm add push-fcm deliver for real, together
if you like. Tests get t.pushes.
Push notifications
import { createTestApp } from '#calm/testing.js';
const t = await createTestApp({ root }); // throwaway database, random port
const alice = await t.signup();
assert.equal((await t.post('/posts', { title: 'Hi', body: '...' }, alice)).status, 201);Runs on node --test. Generated resources come with a test file.
Report vulnerabilities privately (see SECURITY.md). calm doctor checks your project against
published CalmAPI advisories, including inside CalmAPI 1.x projects, and explains each fix.
npm install
npm run lint
MONGODB_TEST_URI=mongodb://127.0.0.1:27017 npm testSee CONTRIBUTING.md.
Layout: core/ (the framework copied into projects as calm/), templates/, registry/blocks/,
src/cli/, packages/devtools (@calmapi/devtools), packages/create-calmapi.
MIT License.