SDK & CLI
The Backlex typed fetch wrapper and the Backlex CLI for project scaffolding.
Two packages ship for client-side and developer-side use.
Not on TypeScript? Backlex also ships native clients for Python, Go, Rust, Java, Kotlin, Swift, Dart/Flutter, .NET, Ruby, and PHP. They wrap the core surface — CRUD, the query builder, auth, realtime, storage, and a uniform error type — not the admin namespaces this page’s
client.flows.*/client.templates.*examples use, which stay TypeScript-only. See Client SDKs for the exact split.
backlex
Section titled “backlex”Typed fetch wrapper, browser + Node.
import { createClient } from "backlex";
const wks = createClient({ url: "https://api.your.app", // For server-to-server / CI; browser apps use the cookie session and skip: apiKey: process.env.BACKLEX_API_KEY,});
// CRUDconst list = await wks.from<Posts>("posts").list({ filter: { published: { _eq: true } }, sort: ["-views", "title"], fields: ["id", "title", "views"], limit: 25, offset: 0, meta: "filter_count",});const one = await wks.from<Posts>("posts").one("uuid");const created = await wks.from<Posts>("posts").create({ title: "hi" });const updated = await wks.from<Posts>("posts").update("uuid", { views: 42 });// Optimistic concurrency: pass the updatedAt you loaded — a concurrent save by// someone else then yields 409 CONFLICT instead of being silently overwritten.await wks.from<Posts>("posts").update("uuid", { views: 43 }, { ifUnmodifiedSince: one.data.updatedAt as string });await wks.from<Posts>("posts").delete("uuid");
// Realtime (SSE)const off = wks.subscribe<Posts>("items:posts", (e) => { console.log(e.event, e.data); // created | updated | deleted});// later:off();
// Authawait wks.auth.signUp({ email, password, name });await wks.auth.signIn({ email, password });await wks.auth.signOut();const session = await wks.auth.getSession();
// OAuth: returns { url } the browser navigates to (provider id from auth.providers()).const { url } = await wks.auth.signInSocial("google", { callbackURL: "/" });// Magic-link sign-in (requires the magic-link plugin on the workspace).await wks.auth.signInMagicLink({ email, callbackURL: "/" });// Describes the sign-in surface (provider list + policy flags) for rendering UI.const surface = await wks.auth.providers();
// Workspace token (app mode) — persist across reloads and restore via createClient({ token }).const token = wks.auth.getToken();wks.auth.setToken(token);
// Storage — folderId is optional and scopes the file under a system_folders row.await wks.storage.put("avatars/me.png", file, "image/png", folderId);const res = await wks.storage.download("avatars/me.png");const blob = await res.blob();await wks.storage.list("avatars/");await wks.storage.delete("avatars/me.png");
// Flows (visual workflows) — admin-scoped; mirrors `/api/flows`, the MCP// `flows.*` tools, and GraphQL `flows`/`runFlow`.const flow = await wks.flows.create({ name: "notify", trigger: "manual:", operations: [{ type: "log", message: "hi" }],});await wks.flows.list();await wks.flows.get(flow.data.id);await wks.flows.update(flow.data.id, { active: false });const run = await wks.flows.run(flow.data.id, { hello: "world" }); // { ok, error? }await wks.flows.delete(flow.data.id);
// Schema templates — admin-scoped; mirrors `/api/admin/templates`, the MCP// `templates.*` tools, and GraphQL `templates`/`applyTemplate`/etc. Full// guide: docs/templates.md.const catalog = await wks.templates.list(); // { data, defaultTemplateId, hasCollections, sampleSeeds }const seeded = await wks.templates.apply("blog"); // idempotent — groups + sample data + bundled roles/dashboards// seeded.data → { templateId, created[], skipped[], seeded, roles[], dashboards[] }const tpl = await wks.templates.extract(); // workspace schema in template formatawait wks.templates.applyCustom(tpl.data); // …applied elsewhere (same idempotent semantics)await wks.templates.clearSamples(); // remove every template-seeded sample rowErrors
Section titled “Errors”Failed requests throw BacklexError:
import { BacklexError } from "backlex";try { await wks.from("posts").create({});} catch (e) { if (e instanceof BacklexError) { e.status // 422 e.code // "VALIDATION" e.message // 'Field "title" is required' e.details // raw response payload }}Type generation
Section titled “Type generation”Pair the SDK with auto-generated types so wks.from<Posts>("posts") is
type-safe:
bun run backlex gen-types https://api.your.app --out src/backlex-types.ts# Or with API key:bun run backlex gen-types https://api.your.app --key pak_xxx --out src/backlex-types.tsOutput:
export interface Posts { id: string; _status: string; // only when the collection is versioned _publishedAt: string | null; ownerId: string | null; // only when owner-scoped createdAt: string; updatedAt: string; title: string; body: string | null; status: "draft" | "live" | null; // dropdown fields → string-literal union category: string; // relation → FK id (expanded shape below) tags: string[] | null; // relation_many → array of FK ids}// + 1 interface per collectionexport interface Collections { posts: Posts; // ...}Field names match the wire exactly. System columns (
id,createdAt,updatedAt,ownerId,_status,_publishedAt) are camelCased the way the REST API serializes them; user-defined fields keep theirsnake_casename — the API never camelCases them. (Earlier codegen camelCased user fields, sorow.featuredImagetype-checked but wasundefinedat runtime against afeatured_imagecolumn. Regenerating closes that bug class.)
Typed expand()
Section titled “Typed expand()”For every relation, codegen also emits a <Slug>Relations map and a
<Slug>Expanded convenience type, plus a generic Expand<> helper — so an
expanded read is fully typed:
export interface PostsRelations { category: Categories; // relation → the target row tags: Categories[] | null; // relation_many → an array of target rows}export type PostsExpanded = Expand<Posts, PostsRelations>;
// Read with expansion — the FK fields are now the related objects:const { data } = await client.from<PostsExpanded>("posts") .query().expand("category", "tags").list();data[0].category.id; // typed: Categoriesdata[0].tags?.[0].id; // typed: Categories[]
// Expand just one relation:type PostWithCategory = Expand<Posts, PostsRelations, "category">;Typed SDK (--sdk)
Section titled “Typed SDK (--sdk)”Add --sdk to also emit a typed client factory, so you skip the manual
<T> on every call:
bun run backlex gen-types https://api.your.app --sdk --out src/backlex.tsThe output adds an import of backlex plus:
export const createTypedClient = (opts: ClientOptions): TypedClient<Collections> => typedCollections<Collections>(createClient(opts));Use it — every collection is keyed by slug and fully typed:
import { createTypedClient } from "./backlex";
const db = createTypedClient({ url: "https://api.your.app", apiKey: "pak_…" });
const { data } = await db.collections.posts.list(); // data: Posts[]await db.collections.posts.create({ title: "Hello" }); // typed Partial<Posts>
// The raw client surface (auth, storage, from<T>, …) is still available:await db.auth.signIn({ email, password });db.collections.<slug> is a thin proxy over db.from(slug) — no
per-collection runtime code is generated; the types live in Collections.
backlex/react wraps the primitives above as hooks — a persisted session, live
queries, optimistic writes and resumable uploads:
const backlex = createClient({ url: "", workspace: "acme", persist: true });
const { status, user } = useSession(backlex);const { data } = useLiveQuery(backlex, "todos", { sort: "-created_at" });Full reference: React bindings.
backlex CLI
Section titled “backlex CLI”Manage any Backlex instance from your terminal or CI — same REST API as the
SDK, authenticated with a personal API key (pak_…).
Installing
Section titled “Installing”The npm package is @backlex/cli; the installed command is backlex.
(The bare backlex npm package is the SDK, not the CLI.)
# one-off, no installnpx @backlex/cli login --url https://api.your.app --key pak_xxxbunx @backlex/cli login --url https://api.your.app --key pak_xxx
# or globallynpm i -g @backlex/clibacklex whoamiInside this monorepo it runs straight from source as bun backlex <cmd> (the
root maps bun run backlex …), no install needed.
migrateis Bun-only (it usesbun:sqlite) and meant for self-hosting; every other command runs under Node, sonpx/global installs work everywhere.
backlex helpbacklex login [--url <url>] [--key <pak_...>|-] [--tenant <id>] [--profile <name>] verify a key against /api/me and save a profilebacklex logout [--profile <name>] [--all] clear saved credentials (--all removes the profile)backlex whoami [--profile <name>] [--json] show the identity behind the resolved keybacklex profile <list|use|add|remove> manage saved connection profilesbacklex collections <list|get|export-schema|drop-field|fts-reindex|vectorize> inspect the schema + rebuild search indexesbacklex items <list|get|create|update|delete|export|import|search> <slug> data-plane CRUD + bulk export/import + searchbacklex backup <list|now|download|restore|config> logical backups + restore + schedulebacklex users <list|grant|revoke> workspace users + role assignmentbacklex roles list roles in the active workspacebacklex flags <list|set|delete> feature flags / remote configbacklex settings <get|set> workspace settings (whitelisted keys)backlex functions <list|deploy|invoke|delete> sandboxed JS functionsbacklex flows <list|get|run|create|delete> visual workflow builderbacklex agents <list|get|create|update|delete|threads|run> AI agent definitions + one-off runsbacklex agents rooms <list|new|add|remove> chat rooms several agents sharebacklex agents say <roomId> --message "@handle …" post in a room; the @handle picks who answersbacklex templates <list|apply|extract|clear-samples> schema-template catalog (apply seeds groups + samples + bundles; apply --file for custom; extract exports the workspace)backlex webhooks <list|create|test|deliveries|retry|resume|delete> outbound webhooksbacklex jobs <list|get|retry|cancel|remove|enqueue> durable job queuebacklex advisor [--kind …] [--fail-on error|warn] security/perf checks (CI gate)backlex advisor insights [--days N] slowest endpoints + list trafficbacklex init [dir] [--force] scaffold a TypeScript consumer starterbacklex sdk [lang] discover the official native client SDKsbacklex migrate [db-path] apply SQLite migrationsbacklex import-db <inspect|plan|run|sources|start|status|...> migrate an external DB (Postgres/MySQL/MongoDB/Firestore/DynamoDB/SQLite) INTO backlex (docs/migrating-in.md)backlex gen-types <api-url> [--out <file>] [--key <pak_...>] [--sdk] generate TS types (+ typed client with --sdk)backlex gen-openapi [--out <file>] fetch the live OpenAPI specbacklex mcp --url <mcp-url> --key <pak_...> [--tenant <id>] stdio MCP server proxying to a remote /mcp endpointConnection context (login / whoami / mcp / gen-types)
Section titled “Connection context (login / whoami / mcp / gen-types)”Commands that hit the API resolve their connection the same way, with this precedence per field:
- explicit flag —
--url/--key/--tenant - environment —
BACKLEX_URL/BACKLEX_API_KEY/BACKLEX_TENANT - the saved profile —
--profile <name>, otherwise the active profile
backlex login stores { url, key, tenant } under a named profile (default
"default") in ~/.backlex/config.json (override with $BACKLEX_CONFIG). The
file holds API keys, so it’s written 0600. After that, every other command
reads the active profile — no repeated --url/--key:
backlex login --url https://api.your.app --key pak_xxx # prompts hidden if --key omitted on a TTYecho "$PAK" | backlex login --url https://api.your.app --key - # CI-friendly: key from stdinbacklex whoami # user, roles, tenantbacklex profile add staging --url https://staging.your.app --key pak_yyybacklex profile use stagingbacklex profile listPass --json for machine-readable output (scripts / CI).
Discovering what you can do
Section titled “Discovering what you can do”backlex help lists every command. Each command group also prints its own
focused usage when invoked with no subcommand (or an unknown one) — so you can
drill in without leaving the terminal:
backlex collections # → the collections subcommands + flagsbacklex items # → the items subcommands + flagsFor this instance specifically, backlex collections list is the live
“what’s reachable” view — every collection the current key can read:
backlex collections listbacklex collections get posts # fields + flags of one collectionbacklex collections
Section titled “backlex collections”Reads GET /api/collections. export-schema dumps the full field metadata as
JSON — commit it and diff across environments (the seed for a future
apply-schema):
backlex collections export-schema --out schema.jsonbacklex items
Section titled “backlex items”Data-plane CRUD over the SDK’s from(slug) client, so it speaks the same query
DSL as the REST API. Payloads accept inline JSON, @file, or - (stdin):
backlex items list posts --filter '{"published":{"_eq":true}}' --sort -views --limit 10backlex items get posts <id> --expand authorbacklex items create posts --data '{"title":"Hello"}'echo '{"views":42}' | backlex items update posts <id> --data -backlex items delete posts <id>
# Bulk export / import (JSON or CSV) — round-trips through the read filtersbacklex items export posts --format csv --out posts.csvbacklex items import posts posts.csv --format csv
# Relevance search (fts / vector / hybrid, per the collection's capabilities)backlex items search posts -q "launch" --mode hybrid --limit 5
# Incremental changefeed — what changed since a cursor (see docs/offline-sync.md).# --shape follows only a subset; rows that leave it come back as `_shape_exit`.backlex items changes posts --jsonbacklex items changes tasks --shape '{"status":{"_eq":"open"}}' --follow --jsonbacklex items changes tasks --since <cursor> --fields title,statusbacklex backup
Section titled “backlex backup”Logical JSONL backups + restore + the auto-backup schedule (see
docs/backup-restore.md). restore is confirm-gated server-side, so the CLI
also demands an explicit --confirm:
backlex backup now --label pre-migrationbacklex backup listbacklex backup download <id> --out backup.jsonlbacklex backup restore <id> --confirmbacklex backup config --schedule daily --retain 30 # omit flags to read the current schedulebacklex users / roles
Section titled “backlex users / roles”Admin-plane user management. users grant is the supported replacement for the
manual INSERT INTO user_roles — it resolves a role by name (or id) and
attaches it:
backlex roles list # find the role id/namebacklex users listbacklex users grant <userId> admin # by name; an id works toobacklex users revoke <userId> editorbacklex flags / settings
Section titled “backlex flags / settings”Feature flags / remote config and the whitelisted workspace settings. --global
targets the global flag scope instead of the active tenant:
backlex flags listbacklex flags set new-checkout --enabled true --rollout 25backlex flags set api-config --value '{"maxItems":50}' --globalbacklex flags delete new-checkout
backlex settings getbacklex settings set i18nDefaultLocale enThe public, per-caller evaluated flag map (rollout + targeting applied) is
served at GET /api/flags — that’s what the SDK’s client.flags reads.
backlex functions / flows
Section titled “backlex functions / flows”Sandboxed JS functions and the visual workflow builder (see docs/sandbox.md,
docs/jobs.md). functions deploy is create-or-update by name — the verb
to wire into a deploy step:
backlex functions deploy resize-avatar --file ./fns/resize.js --trigger event --pattern 'items:avatars:created'backlex functions invoke welcome --data '{"userId":"u_1"}'backlex functions list
backlex flows listbacklex flows get <id> > flow.json # exportbacklex flows create --data @flow.json # import into another envbacklex flows run <id>backlex agents
Section titled “backlex agents”Agent definitions, plus the chat rooms several agents share
(docs/agents.md). run names its agent and prints the answer; say posts in
a room and lets the room decide who replies — an @handle addresses one
directly, and a room set to mention-only simply records the message when
nobody is named:
backlex agents create --data '{"name":"Sales bot","tools":["collections.list"]}'backlex agents run <agentId> --message "how many orders last month?"
backlex agents rooms new --title "Weekly numbers" --agents <id1>,<id2>backlex agents rooms listbacklex agents say <roomId> --message "@sales-bot @data-buddy compare these"backlex webhooks / jobs
Section titled “backlex webhooks / jobs”Outbound webhooks (docs/webhooks.md) and the durable job queue
(docs/jobs.md). webhooks resume re-enables a hook the circuit breaker
auto-disabled after repeated failures:
backlex webhooks create --name orders --url https://hooks.acme.com/x --events 'items:orders:created'backlex webhooks test <id>backlex webhooks deliveries --limit 20backlex webhooks retry <deliveryId>backlex webhooks resume <id>
backlex jobs list --status failedbacklex jobs retry <id>backlex advisor (CI gate)
Section titled “backlex advisor (CI gate)”Runs the security / performance rule checks. --fail-on turns it into a CI
gate — non-zero exit when a finding at or above the level is present:
backlex advisor # list all findingsbacklex advisor --kind security --json # machine-readablebacklex advisor --fail-on error # exit 1 on any error-level findingbacklex advisor --days 30 # widen the traffic window (default 7)The fixable column marks findings the server can remediate itself. Apply one
by id — only the id is sent, and the server re-derives the statement it runs:
backlex advisor --apply perf-hot-filter-index-posts-statusinsights prints the raw aggregation those traffic-derived rules read: slowest
endpoints (p50/p95/p99 + 5xx rate) and per-collection list traffic with the
columns it filters and sorts on. Counts are spans actually recorded — a note
under each table says when sampling or span retention bounded the window:
backlex advisor insights --days 30backlex advisor insights --json | jq '.collections[0].filters'Deploy. There is no
backlex deploycommand. Deployment goes through each platform’s native git integration (Cloudflare Workers Builds, Vercel, Netlify) or the repo’sbun run deploy(CF) — the CLI would only duplicate those. Seedocs/deployment.md.
backlex init / sdk
Section titled “backlex init / sdk”Getting a consuming app off the ground. init scaffolds a self-contained
TypeScript client (non-destructive — pass --force to overwrite); sdk lists
the official native clients (Python, Go, Rust, … — see
Client SDKs) with their install commands:
backlex init ./my-app # writes backlex.ts + .env.examplebacklex sdk # list every SDK + install commandbacklex sdk python # install + quickstart for one languagebacklex gen-openapi
Section titled “backlex gen-openapi”Fetches the live OpenAPI spec (/api/openapi.json, admin-readable) — generated
from the collection schemas + route decorators, so it always matches the running
instance. Use it for openapi-generator, Postman, or the typed-model step of a
native SDK:
backlex gen-openapi --out openapi.jsonopenapi-generator generate -g python -i openapi.json # typed models beside the SDKbacklex migrate
Section titled “backlex migrate”Applies the same Drizzle migrations the API uses. Default path is
./.data/backlex.sqlite (or $DATABASE_PATH).
bun run backlex migratebun run backlex migrate /var/lib/backlex/data.sqliteFor Postgres, use bun run db:migrate:pg directly — the CLI is
SQLite-only for now.
backlex gen-types
Section titled “backlex gen-types”Fetches /api/collections (admin-readable; --key for API key auth)
and emits a TypeScript module. Wire into your build:
{ "scripts": { "predev": "backlex gen-types $BACKLEX_URL --out src/types/wks.ts" }}Re-run after schema changes. The output is deterministic — safe to commit.
backlex mcp
Section titled “backlex mcp”Runs a stdio MCP server that proxies
JSON-RPC over stdin/stdout to a remote Backlex /mcp HTTP endpoint. This lets
local agents (Claude Desktop, Cursor, IDE plugins) talk to a deployed Backlex
instance through a single auth path — the pak_… key — while permissions,
rate-limits, activity logging, and the tenant boundary stay identical to every
other Backlex caller. The URL defaults to http://localhost:8787/mcp; the key
falls back to $BACKLEX_API_KEY.
// Claude Desktop / Cursor MCP config{ "mcpServers": { "backlex": { "command": "bun", "args": ["backlex", "mcp", "--url", "https://api.your.app/mcp", "--key", "pak_xxx"] } }}The per-key MCP tool allowlist (mcpTools) and read-only flag (mcpReadOnly)
set when the key was issued govern what the agent can call — see
docs/api-keys-and-email.md.
Adding the SDK to a separate repo
Section titled “Adding the SDK to a separate repo”The SDK is published as backlex on npm (and on JSR):
bun add backlexTo develop against an unreleased local checkout instead:
bun add file:../backlex/packages/clientThe SDK has zero dependencies beyond @backlex/core (types only) and
the runtime’s fetch / EventSource.