Documentation

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.

Special guide

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).

In one line: KREO is like renting a fully-furnished apartment (database + API + auth + storage, ready to live in) instead of buying land and building a house from scratch.

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 users table).
  • 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

  1. Create a free workspace. Sign up (no credit card) — you instantly get your own isolated database.
  2. Create a table. Dashboard → Database → new table todos with a title (text) and a done (boolean) column — or paste the SQL below into the SQL Console.
  3. Add a row. Click Insert row and type a title. That's your first piece of real data.
  4. Create an API key. Dashboard → API Keys → Create key. Copy it now — it's shown only once.
  5. 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 your todos table.
  • -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.
Golden rule: never paste a secret key into a web page or mobile app — anyone could read it. That is exactly what public keys are for.

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-key header. Keys are stored only as SHA-256 hashes, can be given read and/or write permissions, 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.

MethodPathDescription
GET/api/rest/:tableList rows (cache-aware)
POST/api/rest/:tableInsert 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.

MethodSuccessBody
GET200{ data: [...], count, total, limit, offset, table, schema }
POST201{ data: {…inserted row}, table, schema }
PUT200{ data: {…updated row}, table, schema }
DELETE200{ 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 includes pgCode)
  • 401 — missing or invalid credentials · 403 — API key lacks the required read/write permission, or IP not in allowlist
  • 404 — no row matches the id on PUT/DELETE
  • 422 — no valid fields in the body · 429 — monthly quota exceeded (body includes a resets 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 skip
  • order — column to sort by (default id)
  • dirasc or desc (default desc)
  • Any other column=value becomes an exact-match WHERE 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 — the vector column to search
  • vector — your query embedding (array of numbers, same dimensions as the column)
  • metriccosine (default), l2, or inner_product
  • limit — 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 whose Origin (or Referer fallback) doesn't match is rejected with 403. A key copied out of your page source is useless on any other site — or from a local script/cURL (no Origin403).
  • HTTPS only. The registered site URL must start with https://; plain http or 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 DELETE is off by default and returns 403 unless 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.

MethodPathDescription
POST/api/storage/uploadUpload (multipart/form-data)
GET/api/storage/files?bucket=…List files
GET/api/storage/files/:idMetadata + ready-made URLs
PATCH/api/storage/files/:id{ "visibility": "public" }
DELETE/api/storage/files/:idDelete a file
POST/api/storage/sign{ "id", "expiresIn" } → signed URL (private; max 7 days)
GET/api/storage/public/:idDownload a PUBLIC file (no auth)
GET/api/storage/object/:idDownload 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 link

Do 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": "..." }
New

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 rows

Write

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[] | null

Run 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");
Which key? Use a secret key on a server; use a domain-locked public key in the browser. Never ship a secret key in client-side code.
New

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 | iex

Commands

CommandWhat it does
kreo labInteractive workspace — Les Sous-sols (below)
kreo initCreate a .env with your key + URL
kreo typesGenerate kreo-types.d.ts for every table
kreo tablesList 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.

Signature

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 lab

It 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 TABLE assistant.
  • 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.
Guarded by KREO Sys Security. Everything in the lab runs through the same guard as the public API — tenant isolation, IP allowlist, per-key ACL, rate-limit, anomaly detection and the quota sentinel. Nothing you do can leave your org or exceed your request limits.
New

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 dev

You 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.json

The 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:

StatusMeaningFix
401UnauthenticatedMissing or mistyped key. Send x-kreo-api-key — not Authorization: Bearer.
403 locked to its sitePublic key, wrong originCall from the exact https:// domain registered for that key.
403 permissionKey lacks read/write/deleteEnable the permission on the key (Dashboard → API Keys), or use one that has it.
400Invalid table/column or bad JSONCheck the table name and that the body is valid JSON. pgCode gives the Postgres detail.
404Row not found (PUT/DELETE)The ?id= doesn't match any row.
429Quota / rate limitMonthly quota hit (body says when it resets), or too many attempts — back off or upgrade.
CORS error in the browser console? You're likely calling from a domain that isn't the one your public key is locked to — or using a secret key in the browser (don't). Use a domain-locked public key on the right origin.
"Invalid API key"? The key was revoked, expired, or copied incompletely. Create a fresh one and copy the whole string.

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.