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 ofpalgroup/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 nopalbase backendcommand, andpalbase inittakes no arguments. The correct first line ispalbase init.
Some commands shell out to other tools. Nothing else in the CLI has a runtime dependency.
| Tool | Needed by | Why |
|---|---|---|
| Bun | palbase push, palbase plan | the stack runs Bun, so the bundle is built by the engine that will run it |
| Node.js + npm | palbase init, palbase build | init asks npm which @palbase/backend is newest; build runs the same staging the deploy runs |
git | — | the 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 Compose | palbase start, palbase stop, palbase db, local palbase logs | the 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.jsonmay 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:
- 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. - You sign in on the panel, over TLS, on a page whose address you can read.
- 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).
| Command | What it does |
|---|---|
palbase login | browser sign-in; writes ~/.palbase/session.json |
palbase logout | forgets the linked project's credential, then revokes the cloud session |
palbase whoami | prints 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:
PALBASE_ACCESS_TOKENPALBASE_ACCOUNT_TOKEN, read only when the first is absent- 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:
- a stack running on this machine answers for itself, read from its own state directory
- a hand-written entry in
~/.palbase/credentials.json, keyed by URL — whatpalbase link --token-stdinwrites 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- 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:
| Credential | Shape | Header |
|---|---|---|
| project key | pb_… | apikey: <value> |
| machine identity | pat_… | Authorization: DPoP <value> plus an RFC 9449 proof |
| person | a JWT | Authorization: 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:
| File | Committed? | Written by | Role |
|---|---|---|---|
palbase/project.json | yes | palbase link | the authority — the project this code belongs to |
~/.palbase/checkouts/<hash>/local.json | not in the checkout | palbase start, or palbase link to an address on this machine | wins while it exists — the stack this checkout acts on right now |
~/.palbase/checkouts/<hash>/selection.json | not in the checkout | palbase env use | which 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
| Command | What it does | Docs |
|---|---|---|
login | browser sign-in on this cloud | this page |
logout | forget this machine's credentials | this page |
whoami | show the signed-in user | this page |
doctor | show cloud addresses and diagnose the CLI environment | this page |
open | open Studio in a browser | this page |
Projects on the cloud
| Command | What it does | Docs |
|---|---|---|
project | create, list, status, delete — exactly four verbs | Projects & Linking |
clone | download a project's deployed source by name or ref | Projects & Linking |
apikey | list, reveal, rotate — the two keys a project holds | the two API keys |
members | list, add <email>, remove <user-id> | Projects & Linking |
Binding a directory
| Command | What it does | Docs |
|---|---|---|
init | scaffold a backend project here — no arguments, no flags | Projects & Linking |
link | bind this checkout to a project and write every environment's client inputs | Linking a Checkout |
env | list, use, create, delete — the linked project's environments | Projects & Linking |
unlink | remove the checkout's project binding | Linking a Checkout |
start | bring a stack up on this machine and point this checkout at it | Running It Locally |
stop | shut it down and point the checkout back at its project | Running It Locally |
Shipping code
| Command | What it does | Docs |
|---|---|---|
test | run the project's tests with disposable test identities | Test Users |
build | validate the backend locally the way a deploy would | Deploying |
plan | show what palbase push would change | Deploying |
push | ship code and schema to the linked stack | Deploying |
status | the active version and deploy state | Deploying |
deploys | deploy history, newest first | Deploying |
rollback | serve a stored version again | Deploying |
pull | update this directory to the deployed version | Deploying |
The schema
| Command | What it does | Docs |
|---|---|---|
db | plan, apply, query — against the stack running on this machine only | Database |
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.
| Command | What it does | Docs |
|---|---|---|
secret | set, list, remove — the value is never printed | Secrets |
run | run one command with this project's secrets in its environment | Secrets |
storage | buckets on the stack | Stack Settings |
flags | flag definitions, and per-user overrides | Stack Settings |
egress | the outbound-HTTP allowlist | Outbound Network |
notifications | this project's notification senders | Stack Settings |
auth | settings, providers, sessions, audit, templates, MFA | Auth Settings |
test-user | create, list, clone and delete disposable test users | Test Users |
roles | manage roles and user assignments | Auth Settings |
Clients and contracts
| Command | What it does | Docs |
|---|---|---|
spec | refresh the contract, and regenerate the committed Swift client | Codegen |
Watching
| Command | What it does | Docs |
|---|---|---|
versions | app-version adoption by installation, with an optional daily curve | this page |
logs | the server's view: a deployment's backend logs | Deploying |
debug | the client's view: tail a simulator's console, or attach <code> to a device | Debug |
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:
| Gone | What to run instead |
|---|---|
palbase project use | palbase 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 endpoints | palbase doctor shows the configured addresses and checks the local environment |
palbase upgrade | Update @palbase/backend and run palbase push; cloud manages the runtime version automatically |
palbase apikey create|revoke | palbase apikey list | reveal | rotate — three verbs, no arguments |
palbase members invite|role|invitations|accept | palbase members list | add <email> | remove <user-id> |
palbase secret pull | palbase secret set --stdin to write, palbase run -- <cmd> to use |
palbase db diff|check|types | palbase db plan | apply | query; palbase build writes palbase-env.d.ts |
palbase db reset | palbase start --reset |
palbase debug history | palbase debug tail and palbase debug attach <code> |
palbase project connect-repo|disconnect-repo | nothing — repository-driven deploy has no v2 route |
palbase branch, groups, group, org, organization | nothing — 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, dev | nothing — 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:
| Path | Committed? | Contents |
|---|---|---|
palbase/project.json | yes | the project this checkout belongs to, by id and name; for a stack you host, its address and insecure |
palbase/palbase-env.d.ts | yes | your secrets' types, regenerated by every palbase build |
palbase/environments/<env>/openapi.json | yes | the contract, one file per environment |
palbase/environments/<env>/<platform>-config.json | yes | the 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.tsincluded.palbase linktakes back any.palbaseignore rule it finds left over from the retired layout rather than narrowing it — nothing inpalbase/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
| Path | Contents |
|---|---|
~/.palbase/session.json | the cloud session: access token, refresh token, expiry, user (0600) |
~/.palbase/credentials.json | per-target project keys, keyed by URL — a different file, with a different job |
~/.palbase/stacks/<group>/.env | boot 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
| Variable | Effect |
|---|---|
PALBASE_ENV | the linked project's environment a command acts on, like --env |
PALBASE_ACCESS_TOKEN | headless credential; first in the account-credential chain |
PALBASE_ACCOUNT_TOKEN | account-level machine identity, read only when the above is absent |
PALBASE_DPOP_KEY | the private P-256 JWK paired with a machine token; required for pat_… credentials |
PALBASE_STUDIO_URL | override the Studio origin |
PALBASE_AUTH_URL | override the auth origin |
PALBASE_PLATFORM_URL | override the account API origin |
PALBASE_STACK_DIR | the 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-exitcodeexits2when the plan would change something — that is what makes it usable as a CI gate.palbase runexits with the child's exit status, sopalbase run -- npm testfails 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.
Related
- Linking a Checkout —
palbase link, what it writes, and which file wins - Projects & Linking —
project create,list,status,delete,clone,apikey,members - Deploying —
build,plan,push,deploys,rollback, and what travels - Running It Locally —
palbase start,palbase stop, and the stack on this machine - Database —
db plan,db applyanddb queryagainst that stack - Stack Settings — storage, flags and notification senders, written straight to the stack
- Codegen — platform detection in
palbase link, andpalbase spec - Quickstart — the whole path, end to end
- Managing Projects — what a Project is, and what a ref addresses