Blaze gives every app a database without SQL, connection strings, or direct Postgres access. You declare tables in TypeScript, push them with the CLI, and read/write through a typed client backed by auto-generated REST endpoints. Isolation between apps is enforced by the database itself — see Multi-tenancy & Isolation.
What works today
| Feature | Status |
|---|---|
CRUD (list / get / create / update / delete) | ✅ Shipped |
Schema push via nuble db push | ✅ Shipped |
limit / offset pagination on list() | ✅ Shipped |
| Server-side filters, sort, joins | ⏳ Roadmap |
| SSE subscriptions (real-time) | ⏳ Roadmap |
Anything marked Roadmap returns an error if you try it today. Track progress on the roadmap.
Get up and running
1. Install the SDK
npm install @nublestation/blaze
# or
pnpm add @nublestation/blaze
2. Define your schema
// schema.ts
import { defineSchema, t } from "@nublestation/blaze";
export const schema = defineSchema({
tasks: {
title: t.string().required(),
status: t.enum(["pending", "in_progress", "done"]).default("pending"),
},
});
3. Push it
nuble db push
This creates the table, applies tenant isolation, and writes TypeScript types to .nuble/types.ts. See nuble db push for flags.
4. Create a client and query
import { createBlazeClient } from "@nublestation/blaze";
import { schema } from "./schema";
const { db } = createBlazeClient({
baseUrl: "http://api.clinic.local", // Gateway base URL
apiKey: "nbl_<key_id>.<secret>", // API key from the Console
schema, // enables typed table names + row shapes
});
const task = await db.tasks.create({ title: "Review X-ray" });
const tasks = await db.tasks.list();
That’s it — you have a working multi-tenant database.
Everyday usage
Every table in your schema gets the same five methods:
// List all rows for your app (optional pagination)
const tasks = await db.tasks.list();
const page2 = await db.tasks.list({ limit: 20, offset: 20 });
// Read one row by id — throws if not found
const task = await db.tasks.get(taskId);
// Create — returns the persisted row with server-generated id
const created = await db.tasks.create({ title: "Order supplies" });
// Update — partial; only supplied fields are written
const updated = await db.tasks.update(taskId, { status: "done" });
// Delete
await db.tasks.delete(taskId);
Filtering today is client-side. list() returns all of your app’s rows; filter and sort in JavaScript after fetching:
const pending = (await db.tasks.list()).filter((t) => t.status === "pending");
Under the hood each method maps 1:1 to a REST endpoint (GET/POST /v1/blaze/db/{table}, GET/PATCH/DELETE /v1/blaze/db/{table}/:id) routed through the Gateway — you can call them directly with Authorization: Bearer <api key> if you’re not using the SDK.
Schema DSL reference
Field types (t.*):
| Builder | Postgres type | Notes |
|---|---|---|
t.string() | text | |
t.number() | numeric | JS number |
t.decimal() | decimal | string at the boundary, avoids float loss |
t.boolean() | boolean | |
t.uuid() | uuid | |
t.timestamp() | timestamptz | ISO-8601 string; .default("now") ⇒ now() |
t.json() | jsonb | optionally typed: t.json<MyShape>() |
t.enum([...]) | enum | at least one value required |
t.ref("table") | uuid FK | { onDelete: "cascade" } optional |
Modifiers (chainable on any field):
| Modifier | Effect |
|---|---|
.required() | NOT NULL |
.unique() | unique constraint |
.index() | single-column index |
.default(value) | column default |
For composite indexes, wrap the table in t.model({...}).index("a", "b") or .unique("a", "b").
Every table automatically gets an id (uuid, primary key) and an app_id (uuid, tenant discriminator) — never declare those yourself. Built-in names like users and files are reserved and rejected at push time.
Roadmap: server-side queries
⚠ Not shipped — do not use yet
Everything in this section is the design target, not implemented. Sending these query parameters today returns an error. Filter client-side as shown above until they land.
The planned list() query surface is PostgREST-inspired:
?status=eq.pending status equals 'pending'
?title=ilike.*urgent* title contains 'urgent' (case-insensitive)
?order=created_at.desc sort descending
?select=id,title,status projection
JSONB filters, relations, computed columns, and named queries follow the same release track — see the roadmap.
Limits & gotchas
list()returns all rows for your app (subject tolimit/offset). No server-sideWHEREyet — large tables mean large payloads.- Table names are reserved org-wide. A resource name like
tasksmaps to one physical table shared across apps (rows separated per app). The first app to pushtasksdefines its columns; an incompatible second definition is rejected at push time. - Migrations are append-only. Each push generates a checksummed migration; editing an already-applied migration is refused.
- Concurrent pushes are serialized per app via a Postgres advisory lock — a second push waits, it doesn’t corrupt.
- Reserved names (
users,files,notifications,audit_log) cannot be used as table names in your schema. - Per-request quotas: max 1000 rows, 100 KB body, 30 s execution.
update()drops unknown fields silently — it does not error on extra keys.
Go deeper
Full SDK reference
All methods, field builders, exports, and the complete type reference.
Multi-tenancy & Isolation
What a tenant is and why apps can never see each other's rows.
Row-Level Security
How app_id isolation is enforced inside Postgres.
CLI commands
nuble db push and the rest of the CLI.
Environment variables
| Variable | Description |
|---|---|
DATABASE_URL | Postgres connection string (via PgBouncer) |
INTERNAL_HMAC_SECRET | Shared HMAC secret — must match the gateway |