TypeScript SDK — @tallpond/sdk
The browser/JS client over the gateway API. It runs the OAuth/PKCE flow, sends
credentialed requests, and exposes a chainable query builder. Session tokens live in
httpOnly cookies, so the SDK can only observe "signed in or not" — never the token.
Setup#
import { createClient } from "@tallpond/sdk";
// On the hosted platform: no arguments. The gateway injects
// window.__TALLPOND__ = { gatewayUrl, clientId } into served HTML — the
// serving domain decides which app this is, not the bundle. Hosted apps get
// gatewayUrl: "/_osg", the same-origin edge mount.
export const tallpond = createClient();
Local dev and self-hosted setups pass config explicitly:
export const tallpond = createClient({
gatewayUrl: "http://localhost:3000",
clientId: "your-oauth-client-id",
redirectUri: window.location.origin, // optional
});
The app never configures the issuer directly — signIn() redirects to the gateway's
/auth/authorize, which 302s to Ory. See
authentication.md.
gatewayUrl may be absolute ("https://api.example.com") or, in browsers,
relative ("/_osg"). Relative URLs are resolved against window.location.origin;
non-browser callers must pass an absolute gateway URL.
Non-browser clients#
Two bearer modes replace the cookie session when there is no browser:
// A dev-minted test session (`tallpond test-session`). Static: a 401 is final.
createClient({ gatewayUrl, accessToken });
clientId is optional here (the gateway derives the app from the token), no
cookies are sent, and no CSRF header is required.
Every method returns a promise and throws TallpondError
({ message, status, data, requestId }) on failure — see
error codes. requestId is worth surfacing in any error
report; it's what a gateway log line keys on.
auth#
await tallpond.auth.signIn(); // redirect to Ory
await tallpond.auth.handleRedirectCallback(); // on ?code return; resolves a Session | null
const { authenticated, userId } = await tallpond.auth.getSession();
await tallpond.auth.refreshSession(); // → boolean
await tallpond.auth.signOut();
Data#
Private tables#
tallpond.table("notes"); // → TableQuery (private scope)
Resource handle#
const r = tallpond.resource(roomId);
r.table("messages"); // → TableQuery (resource scope)
r.files("photos"); // → FileHandle (resource scope)
r.members; // → membership methods (below)
r.rooms; // → create/list rooms (assignment, below)
r.room(id); // → one room: its tables, files and grants
await r.get(); // → ResourceInfo
r.members.live(); // → LiveSubscription<MembershipChange> — this resource's roster, live
No r.delete() — deleting a resource is dashboard-only (destructive for every
member, no undo), not an SDK call. See
resources.md.
Membership, live#
// Your own membership across every resource in the app — new invites, accepted
// requests, role changes, removals, from any device. This is the feed that
// answers "was I just invited?"
tallpond.resources.live();
Both r.members.live() and tallpond.resources.live() return the same
LiveSubscription<MembershipChange> every other .live() returns — see
resources.md for the row shape and
realtime.md for why membership
needs its own rule.
Resource management#
await tallpond.resource.create("room", { name: "General" }); // → ResourceInfo
// Each row carries currentMember { role, state } — the caller's own membership —
// so role-aware UI needs one request, not a members.list() per resource.
await tallpond.resource.list({ type: "room" }); // → ResourceInfo[] (active memberships)
await tallpond.resource.browse({ type: "room", query, limit, cursor }); // → Page<ResourceInfo>
await tallpond.resource.static("feed", "global"); // → ResourceInfo
await tallpond.resource.invitations({ type: "room" }); // → InvitationInfo[] (my pending invites)
Federated read#
tallpond.resources({ type: "room" }).table("messages"); // → TableQuery (federated, read-only)
Atomic batch#
await tallpond.batch(
tallpond.table("notes").insert({ title: "a" }),
tallpond.table("notes").insert({ title: "b" })
); // pass unawaited builders
TableQuery#
Chainable and thenable — awaiting it runs the query. Building selectors:
await tallpond.table('notes')
.select('id, title, createdAt') // projection; default '*'
.eq('archived', false)
.gt('rank', 3).gte(…).lt(…).lte(…)
.in('status', ['a', 'b'])
.like('title', 'draft%')
.isNull('deletedAt')
.orderBy('createdAt', 'desc')
.limit(20)
.after(cursor) // opaque cursor pagination
Mutations & terminals
await tallpond.table("notes").insert({ title: "Hi" });
await tallpond.table("notes").update({ title: "Edited" }).eq("id", id); // filter required
await tallpond
.table("notes")
.upsert({ id, title: "x" }, { onConflict: ["id"] });
await tallpond.table("notes").delete().eq("id", id); // filter required
await tallpond.table("notes").count(); // → number
await tallpond.table("notes").eq("id", id).single(); // → Row (404 if none, 409 if >1)
await tallpond.table("notes").eq("id", id).maybeSingle(); // → Row | null
await tallpond.table("notes").select().limit(20).page(); // → { rows, nextCursor }
Shared-table verbs (resource scope)
await tallpond.resource(id).table("photos").link(rowId); // publish an existing owned row here
await tallpond.resource(id).table("photos").unlink(rowId); // withdraw from this context (moderation)
await tallpond.table("photos").contexts(rowId); // → string[] of resource ids (private scope)
Thenable reads resolve to Row[]; the next-page cursor is also attached as .nextCursor
on the returned array.
Resources & membership#
const m = tallpond.resource(roomId).members;
await m.invite("user_123", { role: "writer" }); // → { ok, state: 'invited' }
// A shareable link for someone with no account, or whose id you cannot name.
// Single use, 7 days, admin+. You never learn who claims it until they do.
await m.createInviteLink({ role: "writer" }); // → { url, role, expiresAt }
await m.request({ role: "writer" }); // → { ok, state: 'requested' }
await m.accept(); // accept my invite (or accept(userId) as admin)
await m.reject(); // reject(userId) as admin
await m.join(); // public self-join → { ok, state: 'active' }
await m.leave();
await m.setRole("user_123", "admin"); // active member or pending invite
await m.remove("user_123"); // remove an active member or cancel a pending invite
await m.list(); // → MemberInfo[]
Rooms#
A room partitions one resource. members is consent — who is in at all; rooms
is what role they hold over which content. See
resources.md.
const ws = tallpond.resource(workspaceId);
await ws.rooms.create({ name: "Leadership" }); // → RoomInfo
await ws.rooms.list(); // rooms you hold, with your role
const room = ws.room(roomId);
await room.grants.set("user_123", "writer"); // admin only
await room.grants.remove("user_123");
await room.grants.list(); // the ACL — admin only
room.table("documents").select(); // that room alone
room.files("photos").upload(path, blob); // uploads land in that room
tallpond.rooms.live(); // granted / role changed / revoked, app-wide
Reads through the resource scope span every room you can read; writes target the
default room. moveRoom relocates content between rooms and needs admin where it
currently is:
await ws.table("documents").moveRoom([rowId], roomId);
await ws.files("photos").moveRoom([path], roomId);
Files#
const b = tallpond.files("avatars"); // private: owner-only, no rooms
const rb = tallpond.resource(id).files("photos"); // resource: room-partitioned
await b.upload("me.png", blob, { contentType: "image/png", upsert: true });
await b.download("me.png"); // → Blob
b.url("me.png"); // same-origin cookie-authed URL, for <img src>
await b.list("prefix/"); // → FileMetadata[]
await b.metadata("me.png");
await b.updateMetadata("me.png", { cacheControl: "public, max-age=3600" });
await b.move("old.png", "new.png"); // metadata-only
await b.copy("a.png", "b.png"); // new blob, charged as a write
await b.delete("me.png");
await rb.link(path); // publish one of your private files here
await rb.unlink(path); // withdraw it; the file survives
Full semantics, pricing and the room rules: files.md.
AI#
const res = await tallpond.ai.chat({
model: "openai/gpt-4o",
messages: [{ role: "user", content: "Hi" }],
});
const { models } = await tallpond.ai.models(); // catalog: id, rates, output limits
Functions#
// Runs a deployed function as the signed-in user; resolves its return value.
const move = await tallpond.functions.invoke("submitMove", {
resourceId: room.id,
args: { move: { to: "e4" } },
});
The function executes with exactly the caller's authority and bills the caller;
{ role, via } schema rules make it the mandatory write path. Failures reject with
TallpondError (function_error, function_limit, function_unavailable). See
functions.md.
Wallet & apps (dashboard)#
await tallpond.wallet.get(); // → Wallet
await tallpond.wallet.topup(25); // → { url }
await tallpond.apps.list(); // → { apps }
await tallpond.apps.setCap("app_client_id", 20); // USD/month; 0 blocks
Escape hatch#
await tallpond.gateway.request<T>("/v1/…", { method, headers, body });
Sends a credentialed request with the session cookie + CSRF header and a single transparent refresh-on-401 retry.
Type safety (typegen)#
Row types are loose (Record<string, unknown>) until an app augments the Register
interface via a generated declare module '@tallpond/sdk'. The SDK is fully usable
without the build step — just not statically typed to your schema.