CLI — build, create, deploy
The tallpond CLI is the whole developer workflow: sign in once, register an app, and
ship schema + frontend with one command. No dashboard steps, no hand-provisioned OAuth
clients, no shared secrets.
Install#
The CLI is published as @tallpond/cli.
Run it without installing:
npx @tallpond/cli login
or install it globally so the tallpond command is on your PATH:
npm i -g @tallpond/cli
The rest of this page writes tallpond <cmd>; that's equivalent to npx @tallpond/cli <cmd> if you didn't install globally.
tallpond login # sign in (browser approval), saves a developer token
tallpond apps create "My App" # registers the app + its OAuth client, writes tallpond.json
tallpond dev # schema + functions → "dev" env + a test session
tallpond deploy # PRODUCTION: schema + functions + ./dist (confirms first)
A deployed app is served at https://<slug>.tallpond.app — where the slug ends
in your handle, so my-app-ada — with the schema live behind the gateway.
That's the entire path from empty directory to production, with the deliberate
speed bump that deploy means production and only
production (it confirms interactively; CI passes --yes), while the everyday
test loop lives on tallpond dev and can't touch prod at all.
tallpond login#
Starts a device-flow sign-in:
- The CLI prints (and opens) an approval URL containing a short confirmation code.
- You approve in the browser using your existing tallpond session.
- The CLI receives a developer token (
osgd_…) and saves it to~/.config/tallpond/credentials.json(mode 600), keyed by gateway URL.
The code in the URL can only approve — it can never mint a token, so a leaked link doesn't compromise the login. Tokens are stored server-side only as a SHA-256 hash.
TALLPOND_DEV_TOKEN overrides the saved token (CI). tallpond logout forgets it.
tallpond whoami prints the signed-in developer — your handle first, then the
user id, and the gateway only when it is not the default one (signing in
against a local gateway is a different identity, which is the case worth
flagging). --json prints { user_id, handle, display_name, gateway_url }.
@ada usr_2f9c…
tallpond version (also --version, -v) prints the CLI's own version — the
same number the update notice compares against the registry.
tallpond apps create <name> [--slug my-app-ada]#
Registers an app under your account:
- provisions the app's OAuth client (PKCE public client) at the platform IdP — this is
the
client_idyour app passes to the SDK, and the identity spend caps key on; - reserves the hosted subdomain
<slug>.tallpond.app, which is also the app's label: passing--slugrelabels the app to match rather than leaving a name pointing at a different address. Slugs are scoped to your handle: they end in-<your handle>, which is what lets two people both have an app called "notes". Derived from the name unless--slugis given (lowercase letters/digits/hyphens, and--is reserved as the environment separator). Leave the handle off and it is appended for you —--slug notesand--slug notes-adaare the same request; - allocates the app's stable deployment (its isolated Postgres schema);
- writes
tallpond.jsoninto the current directory:
{
"appId": "…",
"gatewayUrl": "https://api.tallpond.com"
}
Commit this file — it's how tallpond deploy knows which app this repo is. It contains
no secrets.
The app id is the only thing in it, deliberately. The id is the app's
identity and never changes, not even across a rename; the slug, name and URL
belong to the gateway and can change without this file knowing, so caching them
here would only produce a copy that quietly goes stale — including in the
production deploy prompt, which now asks the gateway what it is about to
overwrite. Look them up with tallpond apps list [--json], which shows
everything you own (20 apps per account); --json prints the raw array instead
of a table. Older configs carrying slug/name/url still work — those keys
are ignored, and dropped the next time the file is written.
A tallpond.json that exists but is unparseable or has no appId is an error
that says so, rather than being reported as "no app here".
tallpond apps rename <new-slug> [--app <id|slug|url>] [--yes] / --name "New Name"#
The label and the free address are one identity. Renaming by name derives the address from it; renaming by address derives the label. Pass one or the other, never both — each decides the other:
tallpond apps rename --name "Shared Notes" # → shared-notes-ada
tallpond apps rename shared-notes-ada # → "Shared Notes"
A label you type is kept exactly when it already agrees with the address, so
"iOS Tracker" stays "iOS Tracker" rather than being derived back into
"Ios Tracker". The slug rules are the same as at creation, including the
suffix: tallpond apps rename notes and tallpond apps rename notes-ada are
the same rename, because every address of yours ends in your handle and there
is no point typing it back. A slug is always completed into your own
namespace — it never lands on a bare pretty name, and never in someone else's.
Nothing about the app itself moves: the client_id is its identity and the
deployment holds every row, so data, releases and spend history are untouched.
What does not survive is everything the browser scopes to an origin — signed-in
sessions on the old address, anything in local storage, and an installed PWA,
which keeps pointing at the address it was installed from.
Because of that the first attempt is refused — including when you only meant to change the label, since the label is the address — and the refusal lists exactly what will break:
tallpond apps rename notes-ada
# Renaming my-app-ada → notes-ada will:
# - every signed-in user is signed out …
# - localStorage and IndexedDB are origin-scoped, so client-side state is gone …
# - installed PWAs stay pinned to the old origin …
#
# No application data moves. Re-run with --yes to continue.
tallpond apps rename notes-ada --yes
The old address then serves a page saying where the app moved, for as long as nobody else claims it.
By default the app being renamed is this directory's app, read from
tallpond.json. --app names any app you own — by id, slug, or hosted URL, the
same forms tallpond clone takes — so you can rename from anywhere:
tallpond apps rename notes-ada --app my-app-ada --yes
Nothing local is written either way: the config holds the app id, and a rename does not touch it.
--app, and which commands take it#
--app <id|slug|url> is accepted by every command that only needs to name an
app: apps rename, test-session, functions invoke, env list, env reset,
and logs. With it they run from any directory; without it they use
./tallpond.json, as before.
deploy, dev, check and typegen deliberately do not take it. They
compile .tallpond.schema.ts and ./functions out of the current directory, so
for them "which app" and "which directory" really are one question.
tallpond check [schemaFile] [--json]#
Validates the schema and ./functions locally — the same compile/bundle
steps dev/deploy run before ever touching the network, run standalone with
no auth and no gateway round-trip. Useful as a fast pre-flight, especially for
scripted or agent-driven workflows that want to catch a broken schema or
function file before spending a deploy call on it.
tallpond check # ✓/✗ per step, human-readable
tallpond check --json # { ok, schema: { checked }, functions: { checked, skipped }, errors }
Exits 0 when both steps are clean (or skipped, if the schema file/functions/
directory is absent), 1 otherwise.
tallpond dev [schemaFile] [--env dev] [--user name] [--no-session] [--json]#
The inner loop, in one command: deploys schema + functions to an
environment (default dev, created on first use) and mints an
environment-scoped test session for --user (default default). The token
alone goes to stdout so
TOKEN=$(tallpond dev) composes; --no-session deploys without minting.
Production is unreachable from here by construction — test tokens cannot name
it, and --env prod is an error. Static hosting is production-only for now,
so an env deploy skips the bundle: run your frontend locally against the
gateway with the printed session.
tallpond env list [--app id] [--json] / tallpond env reset <name> [--app id] [--json]#
env list shows the app's non-production environments (name + deployment id);
env reset <name> wipes an environment's data and deployed schema/functions
(redeploy with tallpond dev --env <name>). Both take --json for
machine-readable output instead of the human table/message.
tallpond deploy [schemaFile] [--yes] [--dir dist] [--no-bundle] [--json]#
Production — and only production. There is no --env; testing a change is
tallpond dev. Interactive runs print the app + prod URL and ask for
confirmation; non-interactive runs (CI, agents) must pass --yes explicitly,
so no script reaches production by accident. Three steps, all idempotent, run
in order:
- Schema — if
.tallpond.schema.ts(or the given file) exists, it is compiled locally and applied to the app's deployment. Safe changes apply automatically; destructive ones are blocked (409) pending an explicit migration — see Schema & deploy. - Functions — if
./functionsexists, every module in it (one function per file,export default async (ctx, args) => …) is bundled with the embedded worker runtime into a single script (≤ 1 MB) and uploaded with its manifest. The gateway validates the manifest against the schema'sviarules and pushes the new version live in the same call. See functions.md. - Release — the CLI captures an immutable source snapshot of the project and,
when
./dist(or--dir) exists, a separate static snapshot. It asks the gateway which content-addressed file blobs are missing for this app, uploads only those whole-file blobs, then creates one release linking the snapshots to the schema and function versions just deployed. If the release has a static snapshot, creation atomically sets the app'sactive_release_id; failed uploads or finalization leave the previously active site untouched. The static directory must containindex.html; files are capped at 25 MB each.
Unchanged files upload zero bytes. Renames and deletes also upload zero bytes, and identical content at multiple paths uploads once. A changed file uploads its complete new contents; patch/chunk uploads are not part of snapshot v1.
--no-bundle still captures and retains the source snapshot, but omits the static
snapshot and therefore does not change active_release_id. This is the source-only
release path, not a schema-only deploy.
Hosting behavior: HTML revalidates on every request, other public paths use a short
five-minute cache because URLs remain stable across releases, and extensionless paths
fall back to index.html for client-side routing.
Served HTML also gets the app's identity injected as window.__TALLPOND__ = { gatewayUrl, clientId }, so hosted apps call createClient() with no arguments —
there is no client id to copy into app code, and nothing in the bundle decides which
app it is (the serving domain does). On hosted origins the injected gatewayUrl is
"/_osg", so SDK traffic is same-origin and does not use credentialed CORS.
Deploys are authorized by ownership: your developer token can only deploy to apps you created. There is nothing to configure.
--json (also available on tallpond dev, which shares this same deploy path
for its env branch) suppresses the human prose and prints one JSON object at
the end instead. Production success includes { ok, env, schema, functions, release, upload }, where release contains the release and snapshot digests and
upload reports total/uploaded/reused blob counts and bytes. Failures use
{ ok: false, error: { code, message } }; schema compile errors, blocked
migrations, function bundle errors, blob upload errors, and release-finalization
errors all short-circuit the same way.
tallpond clone [app] [destination] [--release id] [--json]#
Restores an owned app's retained source snapshot. Outside an app directory, pass an
app id or slug; the destination defaults to the app slug. Inside an app directory,
tallpond clone <destination> clones the current app, and an explicit app plus
destination is also accepted. The destination must be absent or empty so clone never
overwrites a working tree.
By default clone selects the latest release; --release <id> selects an exact one.
The CLI downloads each distinct app-scoped blob, verifies its declared size and
SHA-256 digest, restores its project-relative path and executable mode, and removes
the partial destination if recovery fails. Any promoted app can be cloned, not
just your own — Discover already treats a promoted app as public, so the source
it was published from is too. Platform-native forks, which would record what a
project came from, are deferred.
tallpond typegen [schemaFile] [--out tallpond-env.d.ts]#
Generates row types from the schema for end-to-end type safety.
tallpond test-session [--user name] [--env dev] [--app id] [--json]#
Mints a bearer token for a synthetic test user of this project's app, for
programmatic end-to-end testing (see
authentication.md).
Test sessions are environment-only: the token pins an env's deployment
(default dev, which must have been deployed first), and production is never
reachable with a test token — prod data moves only for real signed-in users.
The token alone goes to stdout, so it composes:
tallpond dev --no-session # dev env exists & is current
ALICE=$(tallpond test-session --user alice) # pinned to dev
curl -H "Authorization: Bearer $ALICE" https://api.tallpond.com/v1/users/me
Test users have no wallet of their own — their usage bills your balance directly
(the minting developer's), for exactly what the tests use, bounded by a per-env
budget separate from the app's production budget. Re-minting a name just issues
a fresh 24h token for the same user. --json prints the full response
(user_id, expires_at, payer, env) instead.
tallpond functions invoke <name> [--args json] [--user name] [--env dev] [--app id]#
Invokes a deployed function as a fresh test user, the same way an app calling
it via the SDK would, and prints the JSON result. Useful for exercising a
function directly without wiring up a client. Mints a short-lived test
session under the hood (--user/--env behave like tallpond test-session),
so it's environment-only — there is no way to invoke a function against
production from the CLI.
tallpond functions invoke greet --args '{"name":"ada"}'
tallpond functions invoke greet --args '{"name":"ada"}' --user alice --env dev
--args must be valid JSON (default null); invalid JSON is a 400 invalid_args error before anything is invoked.
tallpond logs [--env dev] [--limit n] [--app id]#
Prints recent function invocations for the app as JSON (always — there's no
human-formatted mode). With no --env, returns invocations across every
deployment; --env <name> narrows to one environment. --limit caps the
result (default 50, max 200).
tallpond logs --env dev --limit 20
Each entry:
{
"invocation_id": "…",
"fn": "greet",
"version": "f1a2b3c4",
"wall_ms": 42,
"outcome": "ok",
"error_code": null,
"error_detail": null,
"error_stack": null,
"created_at": "2026-07-20T12:00:00.000Z"
}
Invocation records are diagnostics, not application data: they carry timing, outcome, and error metadata, but never a function's call arguments or return value. That's deliberate — the developer-facing debugging surface stays blind to user data by construction, the same way the rest of the platform treats user-owned content. See functions.md for the invocation model these logs come from.
HTTP surface (for tooling)#
Everything the CLI does is plain HTTP on the gateway, under /dev:
| Method & path | Auth | Purpose |
|---|---|---|
POST /dev/cli/start | none | begin device flow → { device_code, user_code, verification_url } |
GET/POST /dev/cli/approve | browser session | approval page + form |
POST /dev/cli/token | device_code | poll; 428 pending, then { token } once |
GET /dev/me | dev token | { user_id, handle, display_name } |
GET /dev/apps / POST /dev/apps | dev token | list / create apps |
POST /dev/apps/:appId/deploy | dev token (owner) | apply a compiled SchemaIR |
POST /dev/apps/:appId/artifact-blobs/missing | dev token (owner) | return app-scoped (sha256, size) descriptors not already verified |
PUT /dev/apps/:appId/artifact-blobs/sha256/:digest | dev token (owner) | upload and verify one missing whole-file blob |
GET /dev/apps/:appId/artifact-blobs/sha256/:digestHEAD /dev/apps/:appId/artifact-blobs/sha256/:digest | dev token (owner) | retrieve or inspect a retained source/static blob |
POST /dev/apps/:appId/releases | dev token (owner) | verify snapshots and create/optionally activate an immutable release |
GET /dev/apps/:appId/releases/:idOrLatest | dev token (owner) | read an exact or latest release for clone/recovery |
POST /dev/apps/:appId/test-users | dev token (owner) | mint a test-session bearer token |
GET /dev/apps/:appId/logs | dev token (owner) | recent function invocations (timing/outcome/error only, never args or results) |
Dev tokens go in Authorization: Bearer osgd_…. Owner checks return 404 for apps
that aren't yours, so app existence is not leaked. GET /dev/apps reports
active_release_id, the release whose static snapshot hosting currently serves (or
null before the first static release).
This is a direct protocol cutover: the removed timestamped
PUT /bundle/:version/:path plus /activate contract and live_version field are not
accepted by the current gateway. Apps published with that protocol must be redeployed
with a current CLI; old CLI versions are unsupported after the gateway cutover.