Your schema is
the
api.
Define a table and its REST endpoints already exist. Add logic in TypeScript, secure every read with one rule, and ship it as a file you own in git. No codegen. No redeploy.
The same primitives back a Saturday prototype and a company's system of record.
$ curl -fsSL https://get.oikapi.com/cli | sh $ oi login https://hq.oikapi.com ✓ authorized, saved to keychain $ oi init crm && cd crm $ oi dev ▸ watching crm/, syncing on save → https://hq.oikapi.com/crm
Add a field. It's live on the api before you tab away.
The API isn't generated from your schema, it is your schema. Metadata-driven, so there's no client to regenerate and nothing to drift. Save the file; the endpoint changes with it.
# edit tables/01_leads.json, add one field: { "name": "score", "type": "integer" } # …save. no build, no migration, no redeploy. ✓ tables/01_leads.json +1 field (score) ▸ published, live in ~120ms
$ oi api GET '/api/apps/crm/tables/leads/records/LEAD-000042' { "number": "LEAD-000042", "status": "qualified", "score": null // ← there instantly, typed }
Full CRUD over REST on every table, typed responses, a real filter DSL, cursor pagination. API docs →
An app is files you version in git.
Tables, rules, roles, and permissions are plain JSON and TypeScript on disk. oi pull the whole app to your machine, review a change in a PR, and ship a signed .oikapp. Self-host the server, export your data whenever you want. Nothing is trapped.
# the whole app is JSON + TypeScript on disk crm/ oikapi.json tables/01_leads.json rules/lead_score.ts roles/ permissions/ form_views/ $ git commit -m "crm: score leads" && oi push crm ✓ 2 tables · 1 rule · 3 grants published
Logic runs inside the write.
Business rules are TypeScript in a locked-down sandbox with a real standard library, permission-checked reads, exact-decimal money, an SSRF-guarded fetch. Mutate the record in place; throw to roll the whole write back. No separate function service to deploy.
// `record` is the row being written; mutate it in place. const customer = get("customers", record.customer_id); // permission-checked const rate = customer.tier === "gold" ? "0.10" : "0.05"; record.discount = Decimal.mul(record.subtotal, rate); // exact money, as strings if (Decimal(record.total).greaterThan(customer.credit_limit)) { throw new Error("Credit limit exceeded"); // ← rolls back the write }
triggers on create · update · delete · schedule · webhook · email, the sandbox reference →
Governed by construction.
Declare access once, a row-level predicate over $user, column masks, app scope. The same predicate runs whether the read comes from the REST API, a rule script, a rollup, search, or an AI agent. Denied by default, an integration you ship can't leak across apps or surface a masked column.
table grant 500 > per-record 300 > app grant 100 > deny 0 · permissions →
# grant on the "deals" table row_filter: eq(owner, $user.id) # enforced identically for: GET /api/apps/crm/tables/deals/records # REST list("deals", { … }) # a rule rollup(deals.count) # a rollup oikapi-mcp # an agent
An agent is just another principal.
The shipped MCP server exposes the whole platform to your assistants, and an agent authenticates as a first-class principal under the same RBAC, row-level security, and audit as a person. Its authority is exactly its grants, with a budget and a kill-switch. No shadow API, no implicit elevation.
$ oi mcp-login ✓ authorized, same account, same permissions $ oikapi-mcp ▸ platform tools registered list_records · create_record · search · start_workflow … … scoped by the agent's capability profile // an agent reads and writes only what its grants allow, // audited like anyone.
Define a table. Ship the rest.
Install the CLI, log in, and have a table live over the API in a couple of minutes.