Palbase
Sign inGet started

CLI

Overview

The palbase CLI is a single Go binary — not an npm package — that does everything you do to a Palbase project from a terminal: create it, bind a checkout to it, ship code and schema to it, read and write its settings, generate typed clients from its contract, and watch what it is doing. There is one cloud and one address space, so there is no mode to pick and no endpoint to configure; what varies is which project a directory is bound to, and that is a file the CLI writes when you run palbase link. This page is the map: what to install, how to sign in, what a directory acts on, the full command surface, and — because the v2 cutover deleted ten top-level commands with no aliases — what the verbs in an old script became.

Quick example

palbase login
# Signed in as you@example.com

palbase project create todoapp
# Created todoapp — k3xq81w4m (Running)
#
# Link it with:
#   palbase link todoapp

palbase link todoapp
# ▸ no client app here (…) — linking the backend only
# no contract yet: https://k3xq81w4m.palbase.studio cannot describe itself: nothing is deployed yet — a backend is what makes a contract, so this ends with `palbase push`
#   the link is recorded; `palbase spec` fills the contract in once something answers
#
# linked to todoapp (proj_01)
#   contract read from main; each verb resolves its own environment
# commit palbase/project.json

palbase plan
palbase push
# built 3 controller file(s) → 3 controller(s)  [bun 1.3.9]
# sending /Users/you/todoapp (412 KB)
# live: 14 endpoint(s), 57788ca062dc

Five commands, and the third one is the load-bearing one. palbase link writes the committed palbase/project.json, and from then on every target-relative verb in that directory — push, plan, status, secret, storage, flags, test-user, apikey — acts on that binding without being told again. See Linking a Checkout.

Install

macOS and Linux, with the install script:

curl -fsSL https://raw.githubusercontent.com/palgroup/palbase-cli/main/install.sh | sh

Or with Homebrew:

brew install palgroup/tap/palbase

# later, to upgrade:
brew upgrade palbase

Verify the install:

palbase --version
# palbase version <version>

Note: The install script supports macOS and Linux on amd64 and arm64, and places the binary at /usr/local/bin/palbase. Windows builds are published as zip archives on the GitHub releases page of palgroup/palbase-cli. There is no CLI self-upgrade command — upgrading is Homebrew, a re-run of the install script, or a fresh release archive.

Warning: The install script's closing line prints palbase backend init my-app. That is a stale string in the script: there is no palbase backend command, and palbase init takes no arguments. The correct first line is palbase init.

Some commands shell out to other tools. Nothing else in the CLI has a runtime dependency.

ToolNeeded byWhy
Bunpalbase push, palbase planthe stack runs Bun, so the bundle is built by the engine that will run it
Node.js + npmpalbase init, palbase buildinit asks npm which @palbase/backend is newest; build runs the same staging the deploy runs
gitthe Homebrew formula declares it, and its comment blames palbase pull's 3-way merge. That merge does not exist: pull overwrites the tree from the management API and runs no git process. Nothing in the CLI needs git on the documented rail
Docker Composepalbase start, palbase stop, palbase db, local palbase logsthe stack on this machine is containers

One cloud, one address space

There is one Palbase cloud, compiled into the binary. There is no --mode flag, no PALBASE_MODE variable, no palbase mode command and no prod/dev split — --mode is banned on every command in the tree by a test that runs on every release. palbase doctor reports the four addresses this CLI acts against, alongside checks for login, the project link and local tools:

palbase doctor
#   ✓ cloud      studio https://palbase.studio, auth https://api.palbase.studio, api https://api.palbase.studio, projects <ref>.palbase.studio

A project's address is https://<ref>.palbase.studio — the only address set. The Studio is https://palbase.studio; your account's API and the OIDC issuer are https://api.palbase.studio.

The Studio, auth and platform API addresses can be overridden for a self-hosted deployment with PALBASE_STUDIO_URL, PALBASE_AUTH_URL and PALBASE_PLATFORM_URL. The project host cannot: a project's host has no environment override, so a project's address is always its ref under palbase.studio. Only a non-default deployment announces itself, so if palbase login prints no deployment line, you are on the default cloud.

Note: ~/.palbase/config.json may still exist on a machine that ran an older CLI. The CLI never reads or writes it. Deleting it changes nothing.

Log in

palbase login

palbase login is a browser OAuth sign-in — authorization code with PKCE, scopes openid profile email offline_access. The CLI never handles a password:

  1. The CLI opens a loopback listener on the first free port between 54321 and 54325 — before authorization is asked for — and opens the authorization page in your browser. If the terminal cannot open one it prints (this terminal could not open a browser — use the link), and the handoff link is already on screen.
  2. You sign in on the panel, over TLS, on a page whose address you can read.
  3. The authorization page redirects back to the loopback listener, the CLI exchanges the code with the verifier it generated and never sent, and prints Signed in as <email>.

The wait for the browser is bounded at 5 minutes. The one flag is --create, which opens a new account on this cloud instead of signing in to an existing one; a Palbase account is usable the moment it is created, with no verification, confirmation or activation step in the way.

Two credentials deliberately do not come through login: a project running on this machine (palbase start writes that one itself) and a stack you host yourself (palbase link <url> --token-stdin).

CommandWhat it does
palbase loginbrowser sign-in; writes ~/.palbase/session.json
palbase logoutforgets the linked project's credential, then revokes the cloud session
palbase whoamiprints the signed-in user and the auth address

palbase logout does both halves, in this order: it forgets the credential for whatever this checkout is linked to (printing forgot the credential for <target>), then calls the server's logout and deletes ~/.palbase/session.json regardless of what the server said, printing ✓ Logged out. The project credential goes first on purpose — doing it after a failing cloud logout would leave it behind silently. Credentials supplied through environment variables remain under the caller's control.

palbase whoami prints two lines and nothing else:

palbase whoami
# User:   you@example.com (usr_…)
# Auth:   https://api.palbase.studio

There is no Mode: line — it went with --mode. (The command's own one-line summary still says "and mode"; that is a stale string, not stale behaviour.) With PALBASE_ACCESS_TOKEN set, the identity is asked of the server rather than decoded locally, and the Auth: line names the variable.

Headless and CI

The browser flow does not work on a build server. Export a machine credential instead:

export PALBASE_ACCESS_TOKEN="pat_…"

palbase project list
palbase push

Two chains resolve credentials, and they are separate on purpose.

Your account — used by project, members, apikey reveal/rotate, cloud logs, and the name lookups in link and clone:

  1. PALBASE_ACCESS_TOKEN
  2. PALBASE_ACCOUNT_TOKEN, read only when the first is absent
  3. the access token in ~/.palbase/session.json, refreshed in place when it has expired (an access token lives 30 minutes)

With none of them: not authenticated — run palbase login (or, for headless use, export PALBASE_ACCESS_TOKEN).

A project's own management surface — used by push, plan, pull, spec, status, deploys, rollback, secret, run, storage, flags, egress, notifications, auth, test-user, apikey list, db:

  1. a stack running on this machine answers for itself, read from its own state directory
  2. a hand-written entry in ~/.palbase/credentials.json, keyed by URL — what palbase link --token-stdin writes
  3. PALBASE_ACCESS_TOKEN, but only when it is a project key (pb_…) or the address is not a project of this cloud — an account session must never be presented to a project
  4. the service-role key is brokered per command and cached nowhere, so a rotation cannot leave a stale copy behind

How a credential is presented depends on what it is, and the transport never silently downgrades one to another:

CredentialShapeHeader
project keypb_…apikey: <value>
machine identitypat_…Authorization: DPoP <value> plus an RFC 9449 proof
persona JWTAuthorization: Bearer <value>

A pat_… machine token minted by the account SDK is bound to that SDK's key. Supply the same private JWK in PALBASE_DPOP_KEY; a missing or malformed key stops the request. The CLI does not generate a signing key or load one from local storage. See palbase login.

What a directory is bound to

Almost every verb acts on one environment of one project, and which one is decided by one committed file and two machine-local records that never enter the checkout:

FileCommitted?Written byRole
palbase/project.jsonyespalbase linkthe authority — the project this code belongs to
~/.palbase/checkouts/<hash>/local.jsonnot in the checkoutpalbase start, or palbase link to an address on this machinewins while it exists — the stack this checkout acts on right now
~/.palbase/checkouts/<hash>/selection.jsonnot in the checkoutpalbase env usewhich of the project's environments this machine acts on

<hash> is the first 8 bytes (hex) of the SHA-256 of this checkout's own absolute path, so every checkout gets its own record without writing anything into the tree. palbase stop removes that record, which is what makes "work locally, then push" a two-word switch instead of a re-link. Build output (esm, jobs, hooks) never touches the checkout either — palbase plan and palbase push build it into a temporary directory that lives for one command and is gone before the next. With neither a link nor a running local stack, a target-relative verb refuses and names the ways out rather than guessing.

The global flag is --env. It names one of the linked project's environments — by name or by ref — for one command, and the PALBASE_ENV variable does the same; palbase env use <name> remembers one for this checkout on this machine. With none of them, a command acts on a stack palbase start runs here, else the project's only environment, and refuses with the list when there are several:

palbase project list          # every project, with its environments and their refs
palbase link todoapp          # bind this directory to the project
palbase env use staging       # act on staging from now on, on this machine
palbase deploys --env main    # act on main for this one command

Every verb that leaves this machine prints its destination first, to stderr, before doing the work:

▸ todoapp/staging

Stderr, not stdout, so palbase status --json | jq still works — the banner is commentary, not output, and the scripted run is the one that most needs it. The full story, including --token-stdin, the 503-until-serving retry and what link writes, is on Linking a Checkout.

The command surface

Thirty-seven top-level commands, plus cobra's help and completion. The list is pinned by a golden test, so this is the whole surface.

Your account and this machine

CommandWhat it doesDocs
loginbrowser sign-in on this cloudthis page
logoutforget this machine's credentialsthis page
whoamishow the signed-in userthis page
doctorshow cloud addresses and diagnose the CLI environmentthis page
openopen Studio in a browserthis page

Projects on the cloud

CommandWhat it doesDocs
projectcreate, list, status, delete — exactly four verbsProjects & Linking
clonedownload a project's deployed source by name or refProjects & Linking
apikeylist, reveal, rotate — the two keys a project holdsthe two API keys
memberslist, add <email>, remove <user-id>Projects & Linking

Binding a directory

CommandWhat it doesDocs
initscaffold a backend project here — no arguments, no flagsProjects & Linking
linkbind this checkout to a project and write every environment's client inputsLinking a Checkout
envlist, use, create, delete — the linked project's environmentsProjects & Linking
unlinkremove the checkout's project bindingLinking a Checkout
startbring a stack up on this machine and point this checkout at itRunning It Locally
stopshut it down and point the checkout back at its projectRunning It Locally

Shipping code

CommandWhat it doesDocs
testrun the project's tests with disposable test identitiesTest Users
buildvalidate the backend locally the way a deploy wouldDeploying
planshow what palbase push would changeDeploying
pushship code and schema to the linked stackDeploying
statusthe active version and deploy stateDeploying
deploysdeploy history, newest firstDeploying
rollbackserve a stored version againDeploying
pullupdate this directory to the deployed versionDeploying

The schema

CommandWhat it doesDocs
dbplan, apply, query — against the stack running on this machine onlyDatabase

Settings on the stack

Every one of these writes the linked stack's own management API and lands immediately — there is no file to commit and no deploy to wait for. The surface is identical on a stack you host yourself.

CommandWhat it doesDocs
secretset, list, remove — the value is never printedSecrets
runrun one command with this project's secrets in its environmentSecrets
storagebuckets on the stackStack Settings
flagsflag definitions, and per-user overridesStack Settings
egressthe outbound-HTTP allowlistOutbound Network
notificationsthis project's notification sendersStack Settings
authsettings, providers, sessions, audit, templates, MFAAuth Settings
test-usercreate, list, clone and delete disposable test usersTest Users
rolesmanage roles and user assignmentsAuth Settings

Clients and contracts

CommandWhat it doesDocs
specrefresh the contract, and regenerate the committed Swift clientCodegen

Watching

CommandWhat it doesDocs
versionsapp-version adoption by installation, with an optional daily curvethis page
logsthe server's view: a deployment's backend logsDeploying
debugthe client's view: tail a simulator's console, or attach <code> to a deviceDebug

Diagnosing

palbase doctor is informative only and always exits 0 — it diagnoses; the failing command still owns its error.

palbase doctor
# palbase 0.48.2
#   ✓ cloud      studio https://palbase.studio, auth https://api.palbase.studio, api https://api.palbase.studio, projects <ref>.palbase.studio
#   ✓ login      session token valid
#   ✓ pat        not set (fine for interactive use; CI needs a Dashboard-issued PAT)
#   ✓ link       todoapp
#   ✓ env        todoapp/main  (via only)
#   ✓ docker     /opt/homebrew/bin/docker
#   ✓ compose    Docker Compose version v2.29.0
#   ✓ creds      no credsStore configured (fine — docker will use plain config)
#   ✓ node       v26.7.0 (/opt/homebrew/bin/node)
#   ✓ bun        1.3.9 (/opt/homebrew/bin/bun)

The link line reports what the checkout is bound to — the project palbase/project.json names, a stack you host by its address, or the machine-local record — and says so plainly when there is none. The env line is where a verb would act right now and which rule chose it; when that refuses, the refusal's first line is printed there instead.

palbase open opens Studio in your browser. It does not read the link file, so there is nothing to deep-link to and it opens the Studio root — which is still the useful outcome.

Commands that no longer exist

The v2 cutover deleted these deliberately, with no shims and no aliases — an alias keeps a dead resource alive in muscle memory and in scripts. A retired name answers unknown command "apps" for "palbase". If you are reading an old script, this is the mapping:

GoneWhat to run instead
palbase project usepalbase link <project> — one mechanism for "which project is this directory"
palbase apps <anything>palbase link — it detects the platforms this checkout has and writes each one's slot
palbase github <anything>palbase push — one rail
palbase mode, palbase endpointspalbase doctor shows the configured addresses and checks the local environment
palbase upgradeUpdate @palbase/backend and run palbase push; cloud manages the runtime version automatically
palbase apikey create|revokepalbase apikey list | reveal | rotate — three verbs, no arguments
palbase members invite|role|invitations|acceptpalbase members list | add <email> | remove <user-id>
palbase secret pullpalbase secret set --stdin to write, palbase run -- <cmd> to use
palbase db diff|check|typespalbase db plan | apply | query; palbase build writes palbase-env.d.ts
palbase db resetpalbase start --reset
palbase debug historypalbase debug tail and palbase debug attach <code>
palbase project connect-repo|disconnect-reponothing — repository-driven deploy has no v2 route
palbase branch, groups, group, org, organizationnothing — a branch that needs its own database is its own project, and an Organization is resolved server-side
palbase install-hook, web gen, merge, serve, devnothing — deleted outright, and the pre-push hook they installed went with them

serve and dev became palbase start (a stack here) and palbase push (a deploy). web gen belongs to @palbase/web, whose own palbe-gen owns web client codegen.

install-hook no longer has a hole under it. The pre-push hook it installed was meant to wire itself up from push and clone; both call sites sat inside the retired repository_provider = github branch, so nothing installed it and palbase doctor reported it missing with an instruction that could not work. The whole subsystem — the hook, the doctor check, and the advice — has been removed. On this rail git push does not deploy; the build gate is palbase push's own.

There is no invitation in the CLI: members add takes the address an account already signed up with. There is no palbase deploy — the verb is push. palbase unlink removes a checkout's binding. Cloud manages each project's runtime version from the SDK it uses; self-hosted deployments manage their own runtime installation.

Five flags are banned on every command in the tree and fail to parse anywhere: --branch, --group, --organization, --org, --mode.

Files the CLI writes

In the checkout

Everything the CLI writes lives under the one visible root, palbase/, and every path in it is committed:

PathCommitted?Contents
palbase/project.jsonyesthe project this checkout belongs to, by id and name; for a stack you host, its address and insecure
palbase/palbase-env.d.tsyesyour secrets' types, regenerated by every palbase build
palbase/environments/<env>/openapi.jsonyesthe contract, one file per environment
palbase/environments/<env>/<platform>-config.jsonyesthe app's URL and publishable key, one JSON per platform per environment

Apple environments additionally carry the generated PalbaseGenerated.swift client and Palbase-Info.plist (the file the app actually bundles); web carries the generated palbe.gen.ts behind the stable palbase/client.ts barrel the app imports. See Codegen for the full per-platform layout, and what an Apple link writes for the snippet link prints instead of writing a build file.

Note: Nothing under palbase/ is gitignored — the whole directory is committed, palbase-env.d.ts included. palbase link takes back any .palbase ignore rule it finds left over from the retired layout rather than narrowing it — nothing in palbase/ is per-machine any more. What used to be the per-machine exception (local.json) and the build output (esm/, jobs/, hooks/) never enter the checkout at all today; see below.

On this machine

PathContents
~/.palbase/session.jsonthe cloud session: access token, refresh token, expiry, user (0600)
~/.palbase/credentials.jsonper-target project keys, keyed by URL — a different file, with a different job
~/.palbase/stacks/<group>/.envboot values for a stack palbase start runs here
~/.palbase/checkouts/<hash>/{local.json,plan.json,selection.json,origin}this checkout's machine-local state — the running-here record (or a stack linked by address), palbase plan's measurement, palbase env use's selection, and which checkout the directory belongs to

The two JSON files are not interchangeable and the split was forced: credentials.json was already taken by per-target project keys, and two writers on one path would sign somebody out of every project. There are no -<mode> suffixes on either path.

Environment variables

VariableEffect
PALBASE_ENVthe linked project's environment a command acts on, like --env
PALBASE_ACCESS_TOKENheadless credential; first in the account-credential chain
PALBASE_ACCOUNT_TOKENaccount-level machine identity, read only when the above is absent
PALBASE_DPOP_KEYthe private P-256 JWK paired with a machine token; required for pat_… credentials
PALBASE_STUDIO_URLoverride the Studio origin
PALBASE_AUTH_URLoverride the auth origin
PALBASE_PLATFORM_URLoverride the account API origin
PALBASE_STACK_DIRthe directory holding docker-compose.dev.yml for palbase start

palbase start derives Palbase image versions from the installed @palbase/backend; it does not accept per-image version overrides. PostgreSQL has its own upstream version. See Running It Locally.

Output and exit codes

  • Success → exit 0.
  • Any error → the message is printed once, to stderr, and the CLI exits 1. A runtime error does not dump the usage block; a genuine flag-parse mistake still does.
  • palbase db plan --detailed-exitcode exits 2 when the plan would change something — that is what makes it usable as a CI gate.
  • palbase run exits with the child's exit status, so palbase run -- npm test fails your pipeline exactly when the tests do.

Those last two ride a deliberate-exit interface that a subprocess failure cannot impersonate, which is why a failing child is never mistaken for "nothing to do".

--json is the machine surface, available on project, apikey, members list, deploys, status, logs, flags list, flags user list, storage list, test-user, debug and versions. palbase link has no --json flag. palbase auth always emits indented JSON, and a 204 prints nothing at all rather than an invented ok.

palbase project list --json | jq -r '.[].environments[].ref'
palbase apikey list --json  | jq -r '.publishable'
palbase status --json       | jq -r '.deployed.digest'
palbase deploys --json      | jq -r '.[].digest'
palbase versions --json --days 30 | jq '.current, .daily'

versions --json prints the current distribution. With --days 1..90, it prints one JSON object containing current and daily; 0 omits the daily curve. Unknown commands and unexpected arguments fail before the CLI runs an operation.

Destructive verbs do not accept y. Confirmation is typing something specific — the ref for project delete and apikey rotate — and --yes skips the prompt where one exists.

One status code is treated as "wait", and only one: a freshly created project answers 503 for a short while. push and link retry a 503 for up to 3 minutes at 6-second intervals; every other status is answered on the first try, because a 4xx is a decision and asking again would only make the same answer slower.

  • Linking a Checkoutpalbase link, what it writes, and which file wins
  • Projects & Linkingproject create, list, status, delete, clone, apikey, members
  • Deployingbuild, plan, push, deploys, rollback, and what travels
  • Running It Locallypalbase start, palbase stop, and the stack on this machine
  • Databasedb plan, db apply and db query against that stack
  • Stack Settings — storage, flags and notification senders, written straight to the stack
  • Codegen — platform detection in palbase link, and palbase spec
  • Quickstart — the whole path, end to end
  • Managing Projects — what a Project is, and what a ref addresses