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:

sh
npx @tallpond/cli login

or install it globally so the tallpond command is on your PATH:

sh
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.

sh
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:

  1. The CLI prints (and opens) an approval URL containing a short confirmation code.
  2. You approve in the browser using your existing tallpond session.
  3. 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 }.

text
@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:

json
{
  "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:

sh
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:

sh
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:

sh
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.

sh
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:

  1. 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.
  2. Functions — if ./functions exists, 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's via rules and pushes the new version live in the same call. See functions.md.
  3. 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's active_release_id; failed uploads or finalization leave the previously active site untouched. The static directory must contain index.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:

sh
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.

sh
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).

sh
tallpond logs --env dev --limit 20

Each entry:

json
{
  "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 & pathAuthPurpose
POST /dev/cli/startnonebegin device flow → { device_code, user_code, verification_url }
GET/POST /dev/cli/approvebrowser sessionapproval page + form
POST /dev/cli/tokendevice_codepoll; 428 pending, then { token } once
GET /dev/medev token{ user_id, handle, display_name }
GET /dev/apps / POST /dev/appsdev tokenlist / create apps
POST /dev/apps/:appId/deploydev token (owner)apply a compiled SchemaIR
POST /dev/apps/:appId/artifact-blobs/missingdev token (owner)return app-scoped (sha256, size) descriptors not already verified
PUT /dev/apps/:appId/artifact-blobs/sha256/:digestdev token (owner)upload and verify one missing whole-file blob
GET /dev/apps/:appId/artifact-blobs/sha256/:digest
HEAD /dev/apps/:appId/artifact-blobs/sha256/:digest
dev token (owner)retrieve or inspect a retained source/static blob
POST /dev/apps/:appId/releasesdev token (owner)verify snapshots and create/optionally activate an immutable release
GET /dev/apps/:appId/releases/:idOrLatestdev token (owner)read an exact or latest release for clone/recovery
POST /dev/apps/:appId/test-usersdev token (owner)mint a test-session bearer token
GET /dev/apps/:appId/logsdev 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.