KREO documentation
KREO is a hosted Postgres backend. Create a database, write your schema in SQL, and every table is instantly a REST API — with realtime, a cache, file storage and auth on top. No servers to run.
The API base URL is https://app.kreo.work. Every request to the data API authenticates with the x-kreo-api-key header (or a dashboard session cookie). All examples below are copy-paste runnable once you replace the key.
New here? Create a free workspace (no credit card) — you'll land in the dashboard with an isolated PostgreSQL database ready to use.
For beginners — start here
Never built a backend before? No problem. This guide explains everything in plain language and gets you to your first working API in minutes — zero experience required.
What is KREO, really?
Every app — a website, a mobile app — needs somewhere to store data (users, posts, orders…) and a way to read and write it over the internet. That's called a backend. Normally you'd set up a database, write server code, host it, secure it and back it up yourself. KREO does all of that for you: you say "I want a table called todos", and KREO instantly gives you a live database table and a web address (an API) to read and write it. That's a Backend-as-a-Service (BaaS).
The words you need to know
- Database — where your data lives. KREO gives you a real PostgreSQL database.
- Table — a collection of the same kind of thing, like a spreadsheet (a
userstable). - Row — one entry (one user). Column — one field of it (
email,name). - API — the web address your app calls to read and write data.
- API key — a secret that proves a call is allowed to touch your data.
- Endpoint — one specific API address, e.g.
/api/rest/todos. - JSON — the simple text format data travels in:
{ "title": "Buy milk", "done": false }.
Your first backend in 5 steps
- Create a free workspace. Sign up (no credit card) — you instantly get your own isolated database.
- Create a table. Dashboard → Database → new table
todoswith atitle(text) and adone(boolean) column — or paste the SQL below into the SQL Console. - Add a row. Click Insert row and type a title. That's your first piece of real data.
- Create an API key. Dashboard → API Keys → Create key. Copy it now — it's shown only once.
- Make your first request. Paste the command below in a terminal (replace the key). You just read your data over the internet. 🎉
-- Step 2 — in the SQL Console (optional; you can click instead):
CREATE TABLE todos (
id serial PRIMARY KEY,
title text NOT NULL,
done boolean DEFAULT false
);
# Step 5 — in your terminal (replace the key):
curl "https://app.kreo.work/api/rest/todos" \
-H "x-kreo-api-key: kreo_live_your_key_here"Your first API call, line by line
curl— a small tool to make a web request from your terminal (your browser does the same thing under the hood).https://app.kreo.work/api/rest/todos— the address of yourtodostable.-H "x-kreo-api-key: …"— attaches your key so KREO knows the call is yours.- KREO replies with your rows as JSON:
{ "data": [ { "id": 1, "title": "Buy milk", "done": false } ], "total": 1 }.
Where do I put my key?
- Server code (Node, Python, PHP…) → use a secret key
kreo_live_…, kept on your server, never visible to users. - Directly in a website or mobile app the user runs → use a public key
pk_live_…locked to your domain.
Beginner FAQ
- Do I need to know SQL? No — create tables and rows by clicking. SQL is there when you want more power.
- Is it free? Yes — KREO is 100% free during beta: 100,000 requests/month and 1 GB storage, no card.
- Which language can I use? Any — it's plain HTTP. JavaScript, Python, PHP, Swift, Kotlin, Go…
- How is this different from Firebase / Supabase? Same idea (a hosted backend), but KREO gives you a real PostgreSQL database with an instant REST API, auth, realtime, cache and file storage.
- I'm stuck. Read the Quickstart next, grab a recipe, or email contact@kreo.work.
Quickstart
Create a workspace, add a table in the Table Editor (or the SQL console), then create an API key under Dashboard → API Keys. Every table is immediately a REST resource.
# 1. Your key (Dashboard -> API Keys -> Create key)
export KREO_API_KEY="kreo_live_..."
# 2. Read rows from a table
curl "https://app.kreo.work/api/rest/customers?limit=5" \
-H "x-kreo-api-key: $KREO_API_KEY"
# -> { "data": [ ... ], "count": 5, "total": 1284, "limit": 5, "offset": 0,
# "table": "customers", "schema": "org_xxxxxxxx" }
# 3. Insert a row
curl -X POST https://app.kreo.work/api/rest/customers \
-H "x-kreo-api-key: $KREO_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "Ada Lovelace", "plan": "growth" }'Authentication
The data APIs accept two authentication methods:
- API key — for server-to-server calls. Send it in the
x-kreo-api-keyheader. Keys are stored only as SHA-256 hashes, can be givenreadand/orwritepermissions, locked to IP ranges (CIDR allowlists), and expired or revoked instantly. - Session cookie (JWT) — set by the dashboard after sign-in. HTTP-only,
Secure,SameSite=Lax, signed with HS256. Used automatically by the dashboard UI.
Accounts are protected by bcrypt password hashing and passkeys (WebAuthn / TouchID / FaceID / Windows Hello). Sign-in is rate-limited per IP and per account. Keep API keys server-side — never ship one in client-side code.
# Every data request carries the key header:
-H "x-kreo-api-key: $KREO_API_KEY"
# NOTE: it is x-kreo-api-key, NOT "Authorization: Bearer".REST API
Each table :table is exposed at /api/rest/:table. JSON in, JSON out.
| Method | Path | Description |
|---|---|---|
| GET | /api/rest/:table | List rows (cache-aware) |
| POST | /api/rest/:table | Insert a row (body = JSON object) |
| PUT | /api/rest/:table?id=… | Update the row with that id |
| DELETE | /api/rest/:table?id=… | Delete the row with that id |
# Update one row (by its id)
curl -X PUT "https://app.kreo.work/api/rest/customers?id=42" \
-H "x-kreo-api-key: $KREO_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "plan": "enterprise" }'
# -> { "data": { "id": 42, "plan": "enterprise", ... }, "table": "customers" }
# Delete one row
curl -X DELETE "https://app.kreo.work/api/rest/customers?id=42" \
-H "x-kreo-api-key: $KREO_API_KEY"
# -> { "deleted": true, "id": "42", "table": "customers" }Responses
Every method returns JSON — there is no 204 No Content. A DELETE returns 200 with a confirmation body, not an empty response.
| Method | Success | Body |
|---|---|---|
| GET | 200 | { data: [...], count, total, limit, offset, table, schema } |
| POST | 201 | { data: {…inserted row}, table, schema } |
| PUT | 200 | { data: {…updated row}, table, schema } |
| DELETE | 200 | { deleted: true, id, table, schema } |
Error responses carry the matching status code and a JSON body:
400— invalid table/column name, malformed JSON, or a PostgreSQL error (body includespgCode)401— missing or invalid credentials ·403— API key lacks the requiredread/writepermission, or IP not in allowlist404— no row matches theidon PUT/DELETE422— no valid fields in the body ·429— monthly quota exceeded (body includes aresets on …hint)
Every error body has the shape { "error": "...", "pgCode": "..." | null }.
Filtering & pagination
GET requests accept these reserved query params, plus simple column filters:
limit— page size (default 50, max 1000)offset— rows to skiporder— column to sort by (defaultid)dir—ascordesc(defaultdesc)- Any other
column=valuebecomes an exact-matchWHERE column = value(combined with AND)
# Paid orders, newest first, 20 per page:
curl "https://app.kreo.work/api/rest/orders?status=paid&order=created_at&dir=desc&limit=20" \
-H "x-kreo-api-key: $KREO_API_KEY"
# -> { "data": [ ... ], "count": 20, "total": 128, "limit": 20, "offset": 0,
# "table": "orders", "schema": "org_xxxxxxxx" }Filtering is exact equality only (no >/LIKE operators) — for anything richer, use the SQL console. Every GET response includes an X-Cache header (HIT, MISS or DISABLED); cache hits never count against your quota.
Vector search
If a table has a pgvector column, run similarity search against it — no separate vector database. POST /api/rest/:table/search.
curl -X POST https://app.kreo.work/api/rest/documents/search \
-H "x-kreo-api-key: $KREO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"column": "embedding",
"vector": [0.12, -0.04, 0.88, ... ],
"metric": "cosine",
"limit": 10,
"select": ["id", "title"]
}'
# -> { "data": [ { "id": 7, "title": "...", "distance": 0.04 }, ... ],
# "count": 10, "metric": "cosine", "table": "documents" }column— thevectorcolumn to searchvector— your query embedding (array of numbers, same dimensions as the column)metric—cosine(default),l2, orinner_productlimit— default 10, max 200 ·select— optional column allowlist
Rows are returned closest-first with a distance field. KREO can also generate the embeddings for you locally — enable auto-embed on a column in Settings → AI.
Public keys — Frontend & Mobile (Anon keys)
Public keys (pk_live_…) let you call KREO directly from your web or mobile app — no backend needed to hide a secret key (the Supabase/Firebase "anon key" model). Create one under Dashboard → API Keys → Public Configuration. They are the only kind of key that is safe to ship in client-side code.
How they stay secure
- Domain-locked (mandatory). Each public key is bound to one exact
https://origin. A request whoseOrigin(orRefererfallback) doesn't match is rejected with403. A key copied out of your page source is useless on any other site — or from a local script/cURL (noOrigin→403). - HTTPS only. The registered site URL must start with
https://; plainhttpor a malformed URL is refused at creation and on update. - Permission-scoped (ACL). A public key obeys the same read / write / delete permissions you set in the dashboard. Configure it READ-only to safely display content; a
DELETEis off by default and returns403unless you explicitly enable it. - Nothing else. A public key only authenticates against the REST data API (
/api/rest/:table). The SQL console, vector search, the OpenAPI spec and file storage all reject it outright.
Use it from the browser — fetch()
Pass the key in the x-kreo-api-key header, exactly like a secret key. The request must run on your locked https:// domain (or your mobile app), and the method must be allowed by the key's permissions — otherwise 403.
// Runs client-side on https://my-site.com (the locked origin), or in a mobile app.
// Read content — with a READ-scoped public key:
const res = await fetch("https://app.kreo.work/api/rest/articles?limit=10", {
headers: { "x-kreo-api-key": "pk_live_xxxxxxxx" },
});
const { data } = await res.json();
// Write content — with a WRITE-scoped public key:
await fetch("https://app.kreo.work/api/rest/messages", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-kreo-api-key": "pk_live_xxxxxxxx",
},
body: JSON.stringify({ email, message }),
});Rule of thumb: a public key is for client-side apps, scoped as tightly as possible. For anything privileged (SQL, unrestricted read/write from a trusted server), use a secret key (kreo_live_…) — never ship that one in the browser.
File storage
Store and serve files (images, PDFs, video…) with per-org isolation, public/private visibility and signed URLs. Files are organised into buckets. 50 MB per file; 1 GB of storage per organisation during beta.
| Method | Path | Description |
|---|---|---|
| POST | /api/storage/upload | Upload (multipart/form-data) |
| GET | /api/storage/files?bucket=… | List files |
| GET | /api/storage/files/:id | Metadata + ready-made URLs |
| PATCH | /api/storage/files/:id | { "visibility": "public" } |
| DELETE | /api/storage/files/:id | Delete a file |
| POST | /api/storage/sign | { "id", "expiresIn" } → signed URL (private; max 7 days) |
| GET | /api/storage/public/:id | Download a PUBLIC file (no auth) |
| GET | /api/storage/object/:id | Download a PRIVATE file (signed URL or same-org auth) |
# Upload a public image
curl -X POST https://app.kreo.work/api/storage/upload \
-H "x-kreo-api-key: $KREO_API_KEY" \
-F "file=@avatar.png" \
-F "bucket=avatars" \
-F "visibility=public"
# -> { "file": { "id": "…", "bucket": "avatars", ... },
# "url": "https://app.kreo.work/api/storage/public/…" } // public direct linkDo not send a Content-Type header for the upload — let the HTTP client set the multipart boundary. Uploaded files are served with safe headers (non-image types download rather than render).
SQL console
Run SQL against your own schema from the dashboard, or via POST /api/db/query with a query_string field. Each organisation runs under a dedicated PostgreSQL role scoped to its own schema — your queries can never reach another tenant's data or KREO's internal tables, enforced by PostgreSQL itself.
curl -X POST https://app.kreo.work/api/db/query \
-H "x-kreo-api-key: $KREO_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "query_string": "SELECT plan, count(*) FROM customers GROUP BY plan" }'A read-only API key (no write permission) may only run read statements. A generous per-statement timeout applies.
Realtime
Enable realtime per table (Settings → Realtime), then open a WebSocket to wss://app.kreo.work/api/realtime/ and subscribe to tables. You receive an event on every INSERT, UPDATE and DELETE, scoped to your org.
const ws = new WebSocket(
"wss://app.kreo.work/api/realtime/?apiKey=" + KREO_API_KEY
);
ws.onopen = () => {
// subscribe to a table; optional exact-match filter
ws.send(JSON.stringify({ type: "subscribe", table: "orders", filter: { status: "paid" } }));
};
ws.onmessage = (e) => {
const msg = JSON.parse(e.data);
// { type: "connected", schema } on connect
// { type: "subscribed", table, filter } after subscribe
// { type: "change", table, op, row } op = INSERT | UPDATE | DELETE
if (msg.type === "change") console.log(msg.op, msg.row);
};
// later: ws.send(JSON.stringify({ type: "unsubscribe", table: "orders" }))Max 20 subscriptions per connection. The key travels in the URL, so connect from a trusted/server context (or use a dedicated, IP-locked key) rather than exposing a privileged key in a public browser app.
Turbo-Cache
Toggle a Redis-backed read cache per table (Cache section). Cached GETs return in single-digit milliseconds and don't consume your monthly quota. Any write (POST/PUT/DELETE) automatically invalidates that table's cached reads. The cache TTL is 30 seconds during beta. Watch the X-Cache response header to see HIT / MISS / DISABLED.
Workflows
Fire a webhook whenever rows change in a table (Settings → Workflows). Use it to notify Slack, sync to another system, or kick off a job — no queue to run. KREO POSTs the row as JSON, signed with HMAC-SHA256 (X-KREO-Signature) so you can verify it. Internal/private webhook targets are blocked (SSRF protection).
Table: orders Event: INSERT
-> POST https://your-server.com/hook
Header: X-KREO-Signature: sha256=<hmac>
Body: { "event": "INSERT", "table": "orders", "record": { ... }, "old_record": null }AI assistant
From the dashboard you can ask questions in plain English; KREO generates a read-only SELECT and runs it against your schema (via Ollama locally, or Claude if configured). Available to signed-in dashboard users.
POST /api/ai/query (dashboard session)
{ "question": "How many paid orders did we have last month?" }
# -> { "sql": "SELECT ...", "rows": [ ... ], "columns": [ ... ], "method": "..." }JavaScript / TypeScript SDK
kreo-client is the official, dependency-free, fully-typed client — a real Postgres backend with an instant REST API, realtime, cache and file storage, in five lines. Works in the browser, Node 18+, Bun, Deno and edge runtimes.
Install
# npm (Node, bundlers, frameworks)
npm i kreo-client
# or import straight from KREO's CDN — Deno, Bun, the browser (no npm):
# import { createClient } from "https://kreo.work/sdk/client.js";Quickstart
import { createClient } from "kreo-client";
const kreo = createClient(process.env.KREO_KEY!); // or { apiKey, url }
const { data, error } = await kreo.from("todos").select().limit(10);
console.log(data);Every call returns { data, error } — it never throws on HTTP errors. Reads also return { count, total }.
Read
const { data } = await kreo
.from("orders")
.select()
.eq("status", "paid")
.order("created_at", "desc")
.range(0, 19); // first 20 rowsWrite
await kreo.from("todos").insert({ title: "Ship KREO", done: false });
await kreo.from("todos").update({ done: true }).eq("id", 42);
await kreo.from("todos").delete().eq("id", 42);Typed rows
type Todo = { id: number; title: string; done: boolean };
const { data } = await kreo.from<Todo>("todos").select();
// ^? Todo[] | nullRun npx kreo-cli types to generate exact types for all your tables — see the CLI below.
Realtime, storage & SQL
// realtime — enable the table in Settings -> Realtime first
kreo.channel("messages").on("INSERT", ({ row }) => console.log(row)).subscribe();
// file storage
const { data } = await kreo.storage.upload(file, { bucket: "avatars", visibility: "public" });
// raw SQL on your own schema
const { data: rows } = await kreo.sql("SELECT plan, count(*) FROM customers GROUP BY plan");Command-line tool — kreo
Generate types, inspect tables, run queries and scaffold apps straight from your terminal. Zero dependencies, Node 18+. It talks only to the public API.
Install
# npm — installs the `kreo` command globally
npm i -g kreo-cli
# or run without installing:
npx kreo-cli <command>Prefer no npm at all? Install straight from KREO — it drops the kreo command into your PATH:
# macOS / Linux
curl -fsSL https://kreo.work/install | sh
# Windows (PowerShell)
irm https://kreo.work/install.ps1 | iexCommands
| Command | What it does |
|---|---|
kreo lab | Interactive workspace — Les Sous-sols (below) |
kreo init | Create a .env with your key + URL |
kreo types | Generate kreo-types.d.ts for every table |
kreo tables | List your tables |
kreo query "SELECT …" | Run SQL against your schema |
kreo new <name> | Scaffold a new app (create-kreo-app) |
kreo init # set KREO_KEY once (read from .env afterwards)
kreo types # writes kreo-types.d.ts
kreo query "SELECT count(*) FROM todos"The CLI reads KREO_KEY (and optional KREO_URL) from a local .env or the environment; override per-command with --key / --url.
kreo lab — Les Sous-sols
An interactive terminal workspace: a live quota meter, an SQL playground, a table explorer, a guided table creator and one-key type generation — in a clean, metallic UI.
kreo labIt reads your key from .env (run kreo init first), or pass --key. Then pick a room by number:
- Explorer les tables — browse your schema and columns.
- Console SQL — a REPL; results render as a table.
- Créer une table — a guided
CREATE TABLEassistant. - Générer les types — writes
kreo-types.d.ts. - Usage & limites — your live request quota.
- Sécurité — the KREO Sys Security posture protecting you.
Scaffold a project — create-kreo-app
Spin up a ready-to-run KREO project with the SDK already wired in — like create-next-app.
npm create kreo-app my-app
cd my-app
cp .env.example .env # set KREO_KEY (Dashboard -> API Keys)
npm install
npm run devYou get a typed TypeScript project using kreo-client, a .env.example, and a demo in src/index.ts that connects and reads a table. Point it at your own tables and build from there.
Single-file SDK (dashboard-generated)
Generate a dependency-free, fully-typed TypeScript client from your current schema in Dashboard → API Keys → Generate SDK. It wraps the REST API with typed get / insert / update / remove / search methods per table, so you get autocomplete on your real columns. Regenerate it whenever your schema changes.
OpenAPI / Postman
KREO generates a standard OpenAPI 3.0 document from your live schema, covering every table's REST endpoints, request/response shapes and status codes. Import it into Postman or Insomnia, or feed it to an OpenAPI generator to scaffold a typed HTTP client in any language.
# Download openapi.json (works with a session cookie or an API key)
curl "https://app.kreo.work/api/openapi?download=1" \
-H "x-kreo-api-key: $KREO_API_KEY" \
-o kreo-openapi.json
# Then: Postman -> Import -> kreo-openapi.jsonThe spec is regenerated on every request, so it always matches your current tables and columns. Authentication is described as an x-kreo-api-key API-key security scheme, and the base server URL is https://app.kreo.work.
Security model
- Per-tenant database roles. Each org has its own PostgreSQL schema and a login role granted access to that schema only. Cross-tenant access fails at the database level, not in app code.
- Encryption at rest. Integration secrets are sealed with AES-256-GCM with a rotatable key.
- Account protection. bcrypt password hashing, WebAuthn passkeys, and per-IP / per-account rate limiting. Lost-password recovery uses a one-time secret phrase, not an email link.
- API-key hardening. Keys are stored as SHA-256 hashes, carry read/write permissions, can expire, and can be locked to CIDR ranges.
- Audit logs. Sensitive actions are recorded with IP and user agent.
Read the full public write-up at kreo.work/security.
Cookbook — copy-paste recipes
Real, common tasks you can adapt in a minute. Replace the key and table names with yours.
A public contact form (no backend needed)
Collect submissions straight from your website with a public key locked to your domain and scoped to write-only.
// On https://your-site.com — a public, WRITE-scoped key:
await fetch("https://app.kreo.work/api/rest/messages", {
method: "POST",
headers: { "Content-Type": "application/json", "x-kreo-api-key": "pk_live_..." },
body: JSON.stringify({ email: form.email.value, message: form.message.value }),
});The same read in three languages
// JavaScript (fetch)
const r = await fetch("https://app.kreo.work/api/rest/todos?limit=10", {
headers: { "x-kreo-api-key": KREO_API_KEY },
});
const { data } = await r.json();# Python (requests)
import requests
r = requests.get("https://app.kreo.work/api/rest/todos", params={"limit": 10},
headers={"x-kreo-api-key": KREO_API_KEY})
data = r.json()["data"]// PHP (curl)
$ch = curl_init("https://app.kreo.work/api/rest/todos?limit=10");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ["x-kreo-api-key: $KREO_API_KEY"]);
$data = json_decode(curl_exec($ch), true)["data"];Loop through every row (pagination)
let offset = 0; const all = [];
while (true) {
const r = await fetch("https://app.kreo.work/api/rest/orders?limit=1000&offset=" + offset, {
headers: { "x-kreo-api-key": KREO_API_KEY },
});
const { data } = await r.json();
all.push(...data);
if (data.length < 1000) break;
offset += 1000;
}Filter & sort
# Paid orders, newest first, 20 per page:
curl "https://app.kreo.work/api/rest/orders?status=paid&order=created_at&dir=desc&limit=20" \
-H "x-kreo-api-key: $KREO_API_KEY"Live updates (realtime)
const ws = new WebSocket("wss://app.kreo.work/api/realtime/?apiKey=" + KREO_API_KEY);
ws.onopen = () => ws.send(JSON.stringify({ type: "subscribe", table: "messages" }));
ws.onmessage = (e) => {
const m = JSON.parse(e.data);
if (m.type === "change") console.log(m.op, m.row); // INSERT / UPDATE / DELETE
};Errors & troubleshooting
Every error is JSON — { "error": "…" } — with the matching HTTP status. The most common ones and how to fix them:
| Status | Meaning | Fix |
|---|---|---|
401 | Unauthenticated | Missing or mistyped key. Send x-kreo-api-key — not Authorization: Bearer. |
403 locked to its site | Public key, wrong origin | Call from the exact https:// domain registered for that key. |
403 permission | Key lacks read/write/delete | Enable the permission on the key (Dashboard → API Keys), or use one that has it. |
400 | Invalid table/column or bad JSON | Check the table name and that the body is valid JSON. pgCode gives the Postgres detail. |
404 | Row not found (PUT/DELETE) | The ?id= doesn't match any row. |
429 | Quota / rate limit | Monthly quota hit (body says when it resets), or too many attempts — back off or upgrade. |
Still stuck? Email contact@kreo.work with the endpoint and the error body.
Rate limits & quotas
KREO is free during beta: every organisation gets 100,000 requests per month and 1 GB of storage — no card, no subscription. Requests are metered per month and reset at the start of each month; cached reads are always free and never count against your quota. Sign-in, OTP and recovery endpoints are rate-limited per IP and per account to keep the platform safe. If you exceed your monthly request quota, the API returns 429 with a resets on … hint.
Need help? Email contact@kreo.work.