Vennbase

Build multi-user apps without writing a single access rule.

Vennbase is a TypeScript client-side database for collaborative, local-first web apps. It frees developers from running a backend, paying for servers, or writing finnicky access control rules. Users sign up for a Puter account to store their data. Your app sees the subset of the data shared with a user.
Write your frontend. Vennbase handles the rest.
  • User brings standardized backend — no server to run, no infrastructure bill
  • No access rules to write — share a link, they're in; that's the whole model
  • Optimistic updates — instant writes built-in
  • Local-first support — app data syncs via CRDT automatically
  • NoSQL, open source
  • Auth, server functions — via Puter, one login for your whole app
  • User-pays AI — Puter's AI APIs are billed to the user, not you; build AI features with zero hosting cost
  • Agent-friendly — the explicit-grant model is simple enough that AI coding agents get it right first time
tsx
// Write
const board = db.create("boards", { title: "Launch checklist" }).value;
db.create("cards", { text: "Ship it", done: false, createdAt: Date.now() }, { in: board });

// Read (React)
const { rows: cards = [] } = useQuery(db, "cards", {
  in: board,
  orderBy: "createdAt",
  order: "asc",
});

// Share
const { shareLink } = useShareLink(db, board, "all-editor");

For coding agents

Paste this into your coding agent so it uses Vennbase and pulls the package docs instead of inventing a backend.

Vennbase documentation

DocumentDescription
packages/todo-appSmall working board-and-cards app mirrored by this README.
PATTERNS.mdRecipe-style app patterns for blind inboxes, index-key projections, resource claims, and other real-world Vennbase designs.

Setup

Create one Vennbase instance for your app and pass it an appBaseUrl so that share links point back to your app:

ts
import { Vennbase } from "@vennbase/core";
import { schema } from "./schema";

export const db = new Vennbase({ schema, appBaseUrl: window.location.origin });

Auth and startup

tsx
import { useSession } from "@vennbase/react";

function AppShell() {
  const session = useSession(db);
  const signedIn = session.status === "success" && session.data?.signedIn === true;

  if (session.status === "loading") {
    return <p>Checking session…</p>;
  }

  if (!signedIn) {
    return <button onClick={() => void session.signIn()}>Log in with Puter</button>;
  }

  return <App />;
}

Creating rows

ts
// Create a top-level row
const board = db.create("boards", { title: "Launch checklist" }).value;

// Create a child row — pass the parent row or row ref
db.create("cards", { text: "Write README", done: false, createdAt: Date.now() }, { in: board });
db.create("cards", { text: "Publish to npm", done: false, createdAt: Date.now() }, { in: board });

create and update are synchronous optimistic writes. In normal app code, use .value on the returned receipt when you want the row handle immediately. Only await .committed when another client must be able to rely on the write right away, or when you explicitly need remote confirmation.

To update fields on an existing row:

ts
db.update("cards", card, { done: true });

Membership

Only all-* role members can inspect the member list, and only all-editor can revoke direct members:

ts
// Flat list of usernames
const members = await db.listMembers(board);

// With roles
const detailed = await db.listDirectMembers(board);
// → [{ username: "alice", role: "all-editor" }, ...]

// Grant access by sending an invite link
const editorLink = await db.createShareLink(board, "all-editor").committed;

// Later, revoke an already-joined direct member
await db.removeMember(board, "eve").committed;

Membership inherited through a parent row is visible via listEffectiveMembers. For app-level "memberships" that need discovery, ordering, or extra metadata, prefer modeling them as rows and parent links instead of username-only grants. A common pattern is sharing a readable parent row with content-submitter, then letting each user create their own child join row beneath it.

Real-time sync (CRDT)

Vennbase includes a CRDT message bridge. Connect any CRDT library to a row and all members receive each other's updates in real time.

Sending CRDT updates requires "content-editor" or "all-editor" access. Any readable role (content-* or all-*) can poll and receive them.

In React, here is the recommended Yjs integration:

tsx
import * as Y from "yjs";
import { createYjsAdapter } from "@vennbase/yjs";
import { useCrdt } from "@vennbase/react";

const adapter = createYjsAdapter(Y);
const { value: doc, flush } = useCrdt(board, adapter);

// Write to doc normally, then push immediately when needed
await flush();

@vennbase/yjs uses your app's yjs instance instead of bundling its own runtime, which avoids the multi-runtime Yjs failure mode.

Example apps

packages/todo-app is the working version of this README — boards, recent boards, cards, and share links. Start with src/schema.ts, src/db.ts, and src/App.tsx. Try it live, or run it with:

bash
pnpm --filter todo-app dev

For a fuller picture of how the pieces fit together in a real app, read packages/woof-app. It uses CRDT-backed live chat, user-scoped history rows for room restore, child rows with per-user metadata, and role-aware UI — the patterns you'll reach for once basic reads and writes are working. Try it live.

bash
pnpm --filter woof-app dev

packages/appointment-app is the clearest example of the Vennbase access-control philosophy in a full app: explicit grants, a blind booking inbox, and minimal anonymous sibling visibility via select: "indexKeys". It demonstrates convergent client-side claim resolution, not hard capacity enforcement. Read PATTERNS.md for a recipe-style walkthrough of each pattern. Try it live.

bash
pnpm --filter appointment-app dev