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.
Every method returns a promise and throws TallpondError ({ message, status, data })
on failure — see error codes.
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.members; // → membership methods (below)
await r.get(); // → ResourceInfo
await r.delete();
Resource management#
await tallpond.resource.create("room", { name: "General" }); // → ResourceInfo
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
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' }
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");
await m.remove("user_123");
await m.list(); // → MemberInfo[]
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
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.