bunpaas-db

A schema-agnostic, multi-tenant backend for prototyping apps. Built on Bun and PostgreSQL with JSONB storage.

Try It

Tenant ID
Read-Only Token
Read-Write Token

Open Demo →

Quick Start

// Create a tenant
const res = await fetch("https://your-app.example.com/_tokens", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ name: "My App" })
});
const { tenant, tokens } = await res.json();
// tokens.readOnly  - can only read
// tokens.readWrite - can read and write

API

MethodEndpointAuthDescription
POST/_tokensNoneCreate tenant
GET/_tokensWriteGet tokens
POST/_usersWriteCreate user
PATCH/_users/:idWriteChange password
DELETE/_users/:idWriteDelete user
POST/_signinReadAuthenticate user
GET/:collectionReadList items
GET/:collection/:idReadGet item
POST/:collectionWriteCreate item
PUT/:collection/:idWriteReplace item
PATCH/:collection/:idWriteMerge update
DELETE/:collection/:idWriteDelete item

Authentication

All requests (except tenant creation) require a Bearer token:

Authorization: Bearer <token>

Creating a tenant returns a readOnly token (can only GET) and a readWrite token (full access).

Data Model

Items are stored with flexible JSONB data:

FieldTypeDescription
idUUIDAuto-generated
collectionTEXTe.g., "tasks", "users"
parent_idUUIDOptional parent reference
owner_idUUIDOptional owner reference
order_keyTEXTOptional sort key
dataJSONBYour custom data
created_atTIMESTAMPTZAuto-set
updated_atTIMESTAMPTZAuto-updated

CRUD

Create

const res = await fetch("https://your-app.example.com/tasks", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${TOKEN}`,
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    data: { title: "My Task", status: "pending" },
    parentId: "optional-uuid",
    orderKey: "a"
  })
});
const { data: task } = await res.json();

Read

// List items
const listRes = await fetch("https://your-app.example.com/tasks", {
  headers: { "Authorization": `Bearer ${TOKEN}` }
});
const { data: tasks, pagination } = await listRes.json();

// Get single item
const getRes = await fetch(`https://your-app.example.com/tasks/${id}`, {
  headers: { "Authorization": `Bearer ${TOKEN}` }
});
const { data: task } = await getRes.json();

Update

PUT replaces the entire item. PATCH merges with existing data.

// PUT - replaces data entirely
await fetch(`https://your-app.example.com/tasks/${id}`, {
  method: "PUT",
  headers: {
    "Authorization": `Bearer ${TOKEN}`,
    "Content-Type": "application/json"
  },
  body: JSON.stringify({ data: { title: "New Title" } })
});

// PATCH - merges with existing data
await fetch(`https://your-app.example.com/tasks/${id}`, {
  method: "PATCH",
  headers: {
    "Authorization": `Bearer ${TOKEN}`,
    "Content-Type": "application/json"
  },
  body: JSON.stringify({ data: { status: "done" } })
});

Delete

await fetch(`https://your-app.example.com/tasks/${id}`, {
  method: "DELETE",
  headers: { "Authorization": `Bearer ${TOKEN}` }
});

Query Parameters

ParameterExampleDescription
limit?limit=10Max items (default: 25)
offset?offset=20Skip items
orderBy?orderBy=updated_atSort field
parentId?parentId=uuidFilter by parent
ownerId?ownerId=uuidFilter by owner
filter?filter={"status":"done"}JSON field filter
expand?expand=parent,ownerInclude related items

Expand Relationships

Inline parent, owner, or children in a single request:

// Expand parent and owner
const res = await fetch(
  `https://your-app.example.com/tasks/${id}?expand=parent,owner`,
  { headers: { "Authorization": `Bearer ${TOKEN}` } }
);
const { data: task } = await res.json();
// task._expanded.parent, task._expanded.owner

// Expand children from a collection
const boardRes = await fetch(
  `https://your-app.example.com/boards/${boardId}?expand=children:cards`,
  { headers: { "Authorization": `Bearer ${TOKEN}` } }
);

// Nested expand: board -> columns -> cards -> owner
const deepRes = await fetch(
  `https://your-app.example.com/boards/${boardId}?expand=children:columns.children:cards.owner`,
  { headers: { "Authorization": `Bearer ${TOKEN}` } }
);

User Management

// Create user
const userRes = await fetch("https://your-app.example.com/_users", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${WRITE_TOKEN}`,
    "Content-Type": "application/json"
  },
  body: JSON.stringify({ email: "user@example.com", password: "secret" })
});
const { data: user } = await userRes.json();

// Sign in
const signinRes = await fetch("https://your-app.example.com/_signin", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${READ_TOKEN}`,
    "Content-Type": "application/json"
  },
  body: JSON.stringify({ email: "user@example.com", password: "secret" })
});

// Change password
await fetch(`https://your-app.example.com/_users/${userId}`, {
  method: "PATCH",
  headers: {
    "Authorization": `Bearer ${WRITE_TOKEN}`,
    "Content-Type": "application/json"
  },
  body: JSON.stringify({ password: "newpassword" })
});

// Delete user
await fetch(`https://your-app.example.com/_users/${userId}`, {
  method: "DELETE",
  headers: { "Authorization": `Bearer ${WRITE_TOKEN}` }
});

JavaScript Client

import { bunpaasDb } from "/assets/js/client.js";

// Create tenant
const { tokens } = await bunpaasDb("https://your-app.example.com").createTenant("My App");

// Use the API
const db = bunpaasDb("https://your-app.example.com", tokens.readWrite);

// CRUD
const { data: task } = await db.collection("tasks").create({ data: { title: "Test" } });
const { data: tasks } = await db.collection("tasks").list({ filter: { status: "done" } });
await db.collection("tasks").update(task.id, { data: { status: "done" } });
await db.collection("tasks").delete(task.id);