Skip to content

Getting Started

This guide is also rendered (with the rest of the docs) at iondrive.dev/docs/getting-started.

Ion Drive is the self-hostable, MCP-native backend an AI agent stands up in minutes — with domain blocks you own as editable code. This guide goes from an empty machine to an agent creating and querying real data, in about five minutes. You own the project it creates: a thin server.ts plus a /blocks directory, with the platform arriving as npm dependencies (see Framework mode).

Terminal window
npx @ion-drive/cli init my-app
cd my-app
docker compose up -d # PostgreSQL
npm install
npm run dev # tsx watch server.ts
  • API: http://localhost:3000
  • Admin console: http://localhost:3000/admin

Open the admin console and create your account — the first user to sign up becomes the admin. (.env was generated with fresh secrets and ION_REQUIRE_AUTH=true, so the API is authenticated from the first request. Production hardening knobs are documented in .env.example.)

Ion Drive will refuse to boot in production (NODE_ENV=production) with authentication disabled, unless you set ION_ALLOW_OPEN=true to acknowledge a deliberately open deployment. Keep ION_REQUIRE_AUTH=true for anything reachable by others.

Deploying somewhere public? Don’t race to be the first signup — provision the admin account from the environment instead:

Terminal window
ION_ADMIN_EMAIL=you@example.com
ION_ADMIN_PASSWORD=a-strong-password # or ION_ADMIN_PASSWORD_FILE=/run/secrets/pw

On a fresh database the server creates that admin at boot (before it starts listening, so there is no exposure window) and public signup starts locked — no second trip into your host’s env settings afterwards. Once any user exists the variables are ignored with an info log, so they are safe to leave set. Set ION_DISABLE_SIGNUP=false explicitly if you really want public signup to stay open alongside a bootstrapped admin. See Authentication for details.

Contributor path: working on Ion Drive itself? Clone jaredgrabill/ion-drive and pnpm dev — see CONTRIBUTING.

In the admin console go to API Keys and create a key with a role assigned (e.g. admin) — a key with no role has no permissions. Then add the MCP server to your agent:

Terminal window
# Claude Code, for example:
claude mcp add --transport http ion-drive http://localhost:3000/api/v1/mcp \
--header "X-API-Key: iond_…"

Any MCP-compatible client works the same way — it’s a Streamable HTTP server at /api/v1/mcp (see MCP server). The scaffolded project also contains an AGENTS.md, so coding agents working in the repo already know the endpoints, the query language, and the schema-change etiquette.

Ask your agent something like:

Create a contacts object with a required full name, an email, and a status enum. Add a few sample records, then show me everyone whose status isn’t archived.

The agent has MCP tools for all of it — create_object, create_record, query_data, modify_field (preview-first), relationship management, and the tools of any installed block. The moment an object is created it is live on every surface, no restart:

  • REST: GET/POST/PATCH/DELETE /api/v1/data/contacts
  • GraphQL: contacts, create_contacts, … at /api/v1/graphql
  • MCP: introspection + CRUD tools at /api/v1/mcp
  • OpenAPI: reflected in /api/v1/openapi.json

Open Objects → contacts in the admin console and the records are there, in an editable grid.

Instead of defining every object by hand, install a ready-made building block — a bundle of objects, relationships, seed data, tasks, roles, and (optionally) vendored logic:

Terminal window
npx ion-drive list # the registry catalog (crm, invoicing, …)
npx ion-drive add crm # schema-only: objects + APIs light up immediately
npx ion-drive add invoicing # vendored logic: its code lands in blocks/invoicing/

Blocks with logic are your code — edit blocks/invoicing/stripe.ts and the dev server hot-reloads. Their actions are live at POST /api/v1/blocks/invoicing/actions/create_payment_link, in the OpenAPI spec, and as MCP tools. See Building Blocks and Actions & hooks. To author and ship your own block, see Publishing a block.

Everything the agent does is a plain HTTP API you can drive yourself. Requests are authenticated with the same header (X-API-Key: iond_…):

Terminal window
# Create an object
curl -X POST http://localhost:3000/api/v1/schema/objects \
-H 'content-type: application/json' -H 'x-api-key: iond_…' \
-d '{
"name": "contacts",
"displayName": "Contacts",
"fields": [
{ "name": "full_name", "displayName": "Full Name", "columnType": "text", "isRequired": true },
{ "name": "email", "displayName": "Email", "columnType": "email" },
{ "name": "status", "displayName": "Status", "columnType": "enum" }
]
}'
# Insert a record
curl -X POST http://localhost:3000/api/v1/data/contacts \
-H 'content-type: application/json' -H 'x-api-key: iond_…' \
-d '{ "full_name": "Ada Lovelace", "email": "ada@example.com", "status": "active" }'
# Query: full-text search plus per-property operators, sorting, pagination
curl -H 'x-api-key: iond_…' \
"http://localhost:3000/api/v1/data/contacts?search=example&sort=-created_at&page=1"
curl -H 'x-api-key: iond_…' \
"http://localhost:3000/api/v1/data/contacts?status[neq]=archived&created_at[gt]=2020-10-10"

See the Querying guide for the full operator list.

Building a play-first / try-first app? Set ION_ANONYMOUS_AUTH=true and visitors can start with zero sign-up friction — POST /api/auth/sign-in/anonymous (SDK: ion.auth.signInAnonymously()) mints a real, RBAC-governed guest user that can later upgrade to an email account and keep its data. See Authentication & guest users.

For anything scripted (seed scripts, CI, cron), use an API key — send it as X-API-Key: iond_… like the examples above. API keys skip cookies and CSRF entirely; that is the recommended path. Mint one in the admin console under API Keys (assign a role — a key with no role has no permissions).

The cookie-session auth endpoints (/api/auth/*) are different: they are protected by Better Auth’s CSRF check, which requires an Origin header that matches the server’s base URL (ION_PUBLIC_URL, or http://localhost:3000 by default) or another trusted origin. Browsers send it automatically; a script or curl must add it explicitly or the request fails with 403 {"code":"MISSING_OR_NULL_ORIGIN"}:

Terminal window
# Sign in from a script: the Origin header satisfies the CSRF check.
curl -c cookies.txt -X POST http://localhost:3000/api/auth/sign-in/email \
-H 'content-type: application/json' \
-H 'Origin: http://localhost:3000' \
-d '{ "email": "admin@example.com", "password": "…" }'

That one sign-in is all a script ever needs the Origin header for — including minting the first API key without touching the admin console. With the session cookie, list roles and create a role-bound key (the plaintext key is returned exactly once):

Terminal window
curl -b cookies.txt http://localhost:3000/api/v1/roles # find the admin role id
curl -b cookies.txt -X POST http://localhost:3000/api/v1/api-keys \
-H 'content-type: application/json' \
-d '{ "name": "scripts", "roleId": "<role-id>" }'

From then on, use the key and forget cookies.

Install the zero-dependency client SDK and use the fluent query builder:

Terminal window
npm install @ion-drive/client
import { IonDriveClient } from '@ion-drive/client';
const ion = new IonDriveClient({
baseUrl: 'http://localhost:3000',
apiKey: 'iond_…',
});
// Awaiting the fluent chain executes it (Supabase-style):
const { data, pagination } = await ion.from('contacts')
.select('id, full_name, email')
.search('example')
.neq('status', 'archived')
.gt('created_at', '2020-10-10')
.order('created_at', { ascending: false })
.range(0, 24);
console.log(data, pagination.totalCount);