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#
npx @tallpond/cli 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#
npx @tallpond/cli apps create "My App"
One command registers everything an app needs:
- an OAuth client id — what your frontend passes to the SDK,
- a hosted subdomain —
https://my-app-ada.tallpond.app. Addresses end in your handle, which is what lets two people both ship an app called "notes", - an isolated Postgres deployment for your schema,
- a
tallpond.jsonin the current directory tying this repo to the app (commit it — it holds no secrets).
3. Declare your app's schema#
An app's data model and access rules live in .tallpond.schema.ts — tables, resource
types, and default-deny permissions. This is the demo chat app's real schema, trimmed:
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. Test in an environment, then deploy#
npx @tallpond/cli dev # → isolated "dev" environment + a test session
tallpond dev is the everyday loop: it applies your schema and functions to
an isolated dev environment (its own database, its own data — production
untouched) and mints a test-session token so you or your coding assistant can
exercise the app end to end with no browser. Test sessions can never touch
production — that isolation is enforced by the platform, not convention.
When it works, ship it:
npx @tallpond/cli deploy # → PRODUCTION (asks for confirmation; --yes in CI)
One command publishes one immutable release:
- Schema and functions — compiled locally and applied to production. Safe schema changes (new tables/columns) apply automatically on redeploy; destructive ones are blocked until you write an explicit migration — and you'll have hit that in the dev env first.
- Source and frontend snapshots — the project source is retained as a canonical
snapshot. If a built
./distexists, it becomes a separate static snapshot. The CLI uploads only content-addressed whole-file blobs the app does not already have, so unchanged files, renames, and deletes upload no bytes. Release creation atomically updates static hosting, and your app serves athttps://my-app-ada.tallpond.app.
Use --no-bundle for a source-only release that leaves the currently hosted static
release unchanged. Restore retained source later with
tallpond clone [app] [destination] [--release <id>] — any promoted app, not
just your own.
Optionally generate row types for end-to-end type safety:
npx @tallpond/cli typegen # → tallpond-env.d.ts
5. Talk to it from your app#
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, data controls, and 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:
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#
- CLI — login, app registration, deploys, and the underlying HTTP surface.
- Database — scopes, ownership models, filters, pagination.
- Resources & membership — invites, roles, moderation.
- TypeScript SDK — the full client surface.
- A complete worked example — the app running at
chat.tallpond.app — lives in its own repo:
tallpond-chat.