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 — same API, idiomatic in each language. See Client SDKs.
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 });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
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
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()
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)
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 CLI
Manage any backlex instance from your terminal or CI — same REST API as the
SDK, authenticated with a personal API key (pak_…).
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 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 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)
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
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
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
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 5backlex 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
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
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
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 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)
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 findingDeploy. 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
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
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
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
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
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
The SDK is published as backlex (workspace today; NPM package
in a follow-up). To use locally:
bun add file:../backlex/packages/clientOr, once published:
bun add backlexThe SDK has zero dependencies beyond @backlex/core (types only) and
the runtime’s fetch / EventSource.