Quickstart

Build and ship an app: declare a schema, deploy it with the CLI, and talk to it with the TypeScript SDK. Nothing else — no servers, no billing code, no secrets, and no waiting list: anyone can create and deploy apps.

1. Sign in with the CLI#

sh
bunx tallpond login

This opens a browser approval page (sign in to your tallpond account if you aren't already — new accounts get a welcome credit). Confirm the code shown in your terminal and the CLI saves a developer token to ~/.config/tallpond/credentials.json.

2. Create your app#

sh
bunx tallpond apps create "My App"

One command registers everything an app needs:

3. Declare your app's schema#

An app's entire backend is one file, .tallpond.schema.ts — tables, resource types, and default-deny access rules. This is the demo chat app's real schema, trimmed:

ts
import { defineSchema } from "@tallpond/schema";

export default defineSchema({
  tables: {
    // Top-level tables are creator-owned: each row lives in its
    // author's namespace and can be published into resources.
    messages: (table) => {
      table.text("body").notNull();
      table.timestamps();
      table.index(["createdAt"]);
    },
  },

  resources: {
    // A "room" anyone can discover and join.
    room: (room) => {
      room.visibility("discoverable");
      room.defaultRole("writer");

      // Messages are shared into rooms: authors keep ownership,
      // admins can unlink (moderate) without deleting the author's copy.
      room.shares("messages", (m) => {
        m.onMemberRemove("remove");
        m.onOwnerDelete("tombstone");
        m.access({
          read: "reader",
          create: "writer",
          update: "creator",
          delete: "creator",
          unlink: "admin",
        });
      });
    },
  },
});

Access is default-deny — a verb you don't grant is denied. See Schema & deploy for the full DSL.

4. Deploy#

sh
bunx tallpond deploy

One command does both halves:

Optionally generate row types for end-to-end type safety:

sh
bunx tallpond typegen   # → tallpond-env.d.ts

5. Talk to it from your app#

ts
import { createClient } from "@tallpond/sdk";

// Zero config on the platform: the serving domain identifies your app, and
// the gateway injects { gatewayUrl, clientId } into the page at serve time.
// (During local dev, pass them explicitly: createClient({ gatewayUrl, clientId }).)
const tallpond = createClient();

// Sign in (OAuth + PKCE; tokens stay in httpOnly cookies).
await tallpond.auth.signIn();

// Private data — scoped to the signed-in user, paid by them.
await tallpond.table("notes").insert({ title: "Hello" });

// Shared data — create a room, post into it.
const room = await tallpond.resource.create("room", { name: "General" });
await tallpond.resource(room.id).table("messages").insert({ body: "hi!" });

// Read across every room you're a member of, as one query.
const latest = await tallpond
  .resources({ type: "room" })
  .table("messages")
  .orderBy("createdAt", "desc")
  .limit(50);

// AI — no key in your app; the caller pays their own tokens.
const res = await tallpond.ai.chat({
  model: "openai/gpt-4o",
  messages: [{ role: "user", content: "Hello" }],
});

Every call above is authenticated, access-checked, and metered by the gateway. If the user's balance can't cover an operation, it fails with 402 before any spend.

What you didn't write#

No auth pages, no session handling, no billing integration, no API keys, no row-level security bugs, no servers, no hosting config. The user's dashboard — balance, itemized spend, exports, per-app spend caps — exists already and is the same for every app.

Running the platform locally (optional)#

The whole platform is one repo; you can run your own gateway with Bun and a local Postgres:

sh
git clone https://github.com/CarsonJoe/tallpond.git
cd tallpond && bun install
cp packages/gateway/.env.example packages/gateway/.env   # Postgres + Ory config
bun run db:migrate
bun run dev:gateway    # gateway on http://localhost:3000

Point the CLI and SDK at it with --gateway-url http://localhost:3000 / gatewayUrl: "http://localhost:3000".

Next steps#