Quickstart

Use HTTP, the CLI, or MCP to create and query data, then deploy and call a function.

Docs menu

Run each command below on a fresh server. The outputs shown are trimmed; ids and times differ on yours. You need curl, jq and, for the CLI, Node 20+.

Each path starts with an app key and covers creating a collection, inserting, querying, and updating data, and deploying and invoking a function. The CLI section also covers obtaining a key with substrio signup.

Keys are secrets: each one goes into a file only you can read (umask 077), never onto the command line or into a committed file. The outputs below show the app key as $SUBSTRIO_KEY; get one from substrio.com /keys, or follow the CLI section's substrio signup first.

export SUBSTRIO_URL=https://myapp.substrio.com
export SUBSTRIO_KEY=...        # from substrio.com /keys, or the CLI section below
curl -s $SUBSTRIO_URL/health
# {"status":"ok"}

GET /v1/_whoami tells you what a key is and which app it belongs to:

curl -s $SUBSTRIO_URL/v1/_whoami -H "Authorization: Bearer $SUBSTRIO_KEY"
# {"key":"tenant","account_id":1,"tenant":"demo","tenant_id":2,"scopes":["tenant_admin"]}

The app key selects its app by itself: no X-Substrio-Tenant header or app host is needed.

HTTP (curl)

1. Collection, insert, query

curl -s -X POST $SUBSTRIO_URL/v1/_meta/collections -H "Authorization: Bearer $SUBSTRIO_KEY" \
  -H 'content-type: application/json' -d '{"name":"notes"}'
# {"name":"notes","created":true,"acl_flags":0,"undo_token":"..."}
# Every undoable change answers undo_token: a token when it changed something, or
# "undo_token":null with "unchanged":true when it left the resource as it was, or
# with "pending":true when it was accepted (202) and not yet applied.

curl -s -X POST $SUBSTRIO_URL/v1/notes -H "Authorization: Bearer $SUBSTRIO_KEY" \
  -H 'content-type: application/json' -d '{"title":"hello","n":1}'
# {"_id":"Qs7HRQnMTg","status":"created"}
curl -s -X POST $SUBSTRIO_URL/v1/notes -H "Authorization: Bearer $SUBSTRIO_KEY" \
  -H 'content-type: application/json' -d '{"title":"world","n":5}'
# {"_id":"Qs7HRVu8oq","status":"created"}

curl -s -X POST $SUBSTRIO_URL/v1/notes/_query -H "Authorization: Bearer $SUBSTRIO_KEY" \
  -H 'content-type: application/json' -d '{"where":{"n":{"$gt":2}}}'
# {"data":[{"_id":"Qs7HRVu8oq","_createdAt":1790799172221,"_updatedAt":1790799172221,"_version":1,"title":"world","n":5}],"count":1,"scanned":1,"has_more":false}

The insert body is the document itself, with no wrapper.

2. Update

data.update accepts update operators including $set, $unset, $inc, and $push.

ID=$(curl -s -X POST $SUBSTRIO_URL/v1/notes/_query -H "Authorization: Bearer $SUBSTRIO_KEY" \
  -H 'content-type: application/json' -d '{"where":{"title":"hello"}}' | jq -r '.data[0]._id')

curl -s -X PATCH $SUBSTRIO_URL/v1/notes/$ID -H "Authorization: Bearer $SUBSTRIO_KEY" \
  -H 'content-type: application/json' -d '{"$set":{"title":"hello again"},"$inc":{"n":10}}'
# {"status":"updated","fieldsUpdated":2,"fieldsDeleted":0}

curl -s $SUBSTRIO_URL/v1/notes/$ID -H "Authorization: Bearer $SUBSTRIO_KEY"
# {"_id":"Qs7HRQnMTg","_createdAt":1790799172203,"_updatedAt":1790799172279,"_version":2,"title":"hello again","n":11}

3. Deploy a function and invoke it

A function's source evaluates to a function of the event. It reads the app's data through the db global.

cat > hello.js <<'JS'
(function (event) {
  const notes = db.collection("notes").count();
  return { hello: event.who || "world", notes: notes };
})
JS

jq -n --rawfile script hello.js '{name: "hello", script: $script}' \
  | curl -s -X POST $SUBSTRIO_URL/v1/functions -H "Authorization: Bearer $SUBSTRIO_KEY" \
      -H 'content-type: application/json' -d @- | jq '{success, function_id}'
# { "success": true, "function_id": "demo/hello" }

curl -s -X POST $SUBSTRIO_URL/v1/functions/hello/_invoke -H "Authorization: Bearer $SUBSTRIO_KEY" \
  -H 'content-type: application/json' -d '{"who":"Ada"}'
# {"hello":"Ada","notes":2}

CLI

Install the CLI with npm install -g @substrio/cli (see install). Use substrio <group> <verb>, or substrio op NAME for any operation. Pass input as key=value arguments or --data '{...}'. Use --json for JSON output. substrio login reads the key from stdin (--key -) and saves a profile in ~/.substrio/config.toml. substrio login --offline saves it with no round trip to the server: --tenant is then required (there is no way to look it up), and the profile is saved marked unchecked until a later command reaches the server.

1. Sign up, make an app

substrio signup opens a sign-in page. After you pass the bot check and verify your email, a new address receives an account and a free plan. The CLI saves a refreshable session in a profile. Create an app after signing in:

substrio signup --email [email protected]
substrio app create demo
# ✓ Tenant demo created; its key is saved to profile demo.
export SUBSTRIO_PROFILE=demo

app create saves the app's key to a profile. The profile is named after the slug unless --save-profile specifies another name. To use that key in the HTTP or MCP examples, export it to a private file and load it into your shell. Keep the file out of version control.

substrio key export --env .substrio-key.env
source .substrio-key.env

2. Collection, insert, query

substrio collections create name=notes --json
# { "name": "notes", "created": true, "acl_flags": 0, "undo_token": "..." }

substrio data insert notes --data '{"title":"hello","n":1}' --json
# { "_id": "Qs7JBXFP0a", "status": "created" }
substrio data insert notes --data '{"title":"world","n":5}' --json
# { "_id": "Qs7JC4k4MC", "status": "created" }

substrio data query notes --data '{"where":{"n":{"$gt":2}}}' --json
# { "data": [ { "_id": "Qs7JC4k4MC", ..., "title": "world", "n": 5 } ], "count": 1, "scanned": 1, "has_more": false }

--data is the document itself, with no wrapper (over MCP the same document goes in the document argument). To load many documents, import a file:

printf '%s\n' '{"title":"a","n":1}' '{"title":"b","n":2}' > old-notes.ndjson
substrio data import old_notes --file old-notes.ndjson --json
# {"event":"done","imported":2,"skipped":0,"replaced":0,"failed":0,"elapsed_ms":12,"checkpoint":null}

substrio data import COLLECTION --file FILE streams NDJSON (one document per line), a JSON array, {"documents":[...]}, or CSV. Use --types to specify CSV column types. The command sends the document bytes without loading the whole file, parsing the documents, or re-serializing them. The extension (.ndjson, .json, or .csv) selects the format unless you set --format. Use --format when reading from standard input with --file -.

Each rejected document is reported as it occurs, with its position (a line, element, or row number starting at 1), code, and message. A schema error also lists the broken rules in violations. Other documents are still written. The command exits with code 8 for a partial import and 0 when every document was stored. Documents that specify _id keep it; the others receive a generated id. Responses with status 429 or 503 are retried with backoff. With --json, a retry line is emitted before each wait.

Use --checkpoint and --resume to continue an interrupted import without resending completed work. They require --upsert or --on-conflict skip at any concurrency, so a batch that is sent again updates its documents instead of adding them twice. See bring-your-project.md.

A collection can have a schema. strict defaults to true, which means a field not declared by a rule is refused. datetime accepts an RFC 3339 string or epoch milliseconds, and field names starting with _ are kept as written. See project-files.md for schema, rule, and index files, and bring-your-project.md for moving a backend.

3. Update

ID=$(substrio data query notes --data '{"where":{"title":"hello"}}' --json | jq -r '.data[0]._id')
substrio data update notes $ID --data '{"$set":{"title":"hello again"},"$inc":{"n":10}}' --json
# { "status": "updated", "fieldsUpdated": 2, "fieldsDeleted": 0 }
substrio data get notes $ID --json
# { "_id": "Qs7JBXFP0a", ..., "_version": 2, "title": "hello again", "n": 11 }

4. Deploy a function and invoke it

substrio init creates a project. Its substrio.toml names the profile and does not contain a key. substrio deploy uploads everything in functions/.

mkdir myapp && cd myapp
substrio init --tenant demo --yes
cp ../hello.js functions/hello.js      # the function from the HTTP section
substrio deploy
# Functions (1 file)
#   ✓ hello (0.1 KB)
# ✓ Deployed 1 function successfully.

substrio invoke hello --data '{"who":"Ada"}' --json
# { "hello": "Ada", "notes": 2 }

MCP

MCP is JSON-RPC 2.0 at POST /mcp (alias POST /v1/_mcp). The app key is the credential; the server picks the app from it. Get the key with step 1 of the HTTP or CLI section.

mcp() { curl -s -X POST $SUBSTRIO_URL/mcp -H "Authorization: Bearer $SUBSTRIO_KEY" \
  -H 'content-type: application/json' -d "$1"; echo; }

mcp '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}' \
  | jq -c '.result | {protocolVersion, serverInfo}'
# {"protocolVersion":"2025-06-18","serverInfo":{"name":"demo","version":"1"}}

mcp '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' | jq '.result.tools | length'
# one count per platform tool the app key reaches plus any the app's own manifest declares

For account-level operations and credentials, see accounts and apps and agent sessions.

An MCP client (Claude Code, Cursor) connects to the same URL: claude mcp add --transport http demo $SUBSTRIO_URL/mcp --header "Authorization: Bearer $SUBSTRIO_KEY". See mcp.md; substrio mcp config prints this connection with the key read from an environment variable.

Collection, insert, query

A tool's arguments include its route parameters (collection, id), query options, and body fields. User data such as a document or event is a separate argument, so its fields are not interpreted as parameters.

mcp '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"substrio_collection_create","arguments":{"name":"notes"}}}'
# {"jsonrpc":"2.0","id":3,"result":{"content":[...],"structuredContent":{"name":"notes","created":true,"acl_flags":0,"undo_token":"..."},"isError":false}}

mcp '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"substrio_insert","arguments":{"collection":"notes","document":{"title":"hello","n":1}}}}'
# {...,"structuredContent":{"_id":"Qs7M0DqCZc","status":"created"},"isError":false}}
mcp '{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"substrio_insert","arguments":{"collection":"notes","document":{"title":"world","n":5}}}}'
# {...,"structuredContent":{"_id":"Qs7M0JnmIy","status":"created"},"isError":false}}

mcp '{"jsonrpc":"2.0","id":6,"method":"tools/call","params":{"name":"substrio_query","arguments":{"collection":"notes","where":{"n":{"$gt":2}}}}}'
# {...,"structuredContent":{"data":[{"_id":"Qs7M0JnmIy",...,"title":"world","n":5}],"count":1,"scanned":1,"has_more":false},"isError":false}}

Update

ID=$(mcp '{"jsonrpc":"2.0","id":7,"method":"tools/call","params":{"name":"substrio_query","arguments":{"collection":"notes","where":{"title":"hello"}}}}' \
  | jq -r '.result.structuredContent.data[0]._id')

mcp '{"jsonrpc":"2.0","id":8,"method":"tools/call","params":{"name":"substrio_update","arguments":{"collection":"notes","id":"'$ID'","$set":{"title":"hello again"},"$inc":{"n":10}}}}' \
  | jq -c .result.structuredContent
# {"status":"updated","fieldsUpdated":2,"fieldsDeleted":0}

mcp '{"jsonrpc":"2.0","id":9,"method":"tools/call","params":{"name":"substrio_get","arguments":{"collection":"notes","id":"'$ID'"}}}' \
  | jq -c .result.structuredContent
# {"_id":"Qs7O8JlUcS",...,"_version":2,"title":"hello again","n":11}

substrio_update accepts only the operators: an unknown argument such as update is refused with INVALID_ARGUMENT ("Unknown field (strict mode enabled)"), and nothing is written.

Deploy a function and invoke it

jq -n --rawfile script hello.js \
  '{jsonrpc:"2.0",id:10,method:"tools/call",params:{name:"substrio_function_deploy",arguments:{name:"hello",script:$script}}}' > deploy.json
mcp @deploy.json | jq -c '.result.structuredContent | {success, function_id}'
# {"success":true,"function_id":"demo/hello"}

mcp '{"jsonrpc":"2.0","id":11,"method":"tools/call","params":{"name":"substrio_function_invoke","arguments":{"name":"hello","event":{"who":"Ada"}}}}' \
  | jq -c .result.structuredContent
# {"hello":"Ada","notes":2}

substrio_function_invoke takes name (the function) and event, the JSON event, whatever its field names.

Add a jet to your app

A jet is a ready-made service such as SendJet or MapJet. One command adds it to your app and writes its key to a file only you can read:

substrio jet use sendjet

The answer names the endpoint to call. Your MCP client does the same with substrio_jet_start, and the console has a Get started button on every jet. See Starting with a jet.

Next