Palbase
Sign inGet started

CLI

Linking a Checkout

palbase link binds this directory to a project — never to one of its environments — and, in a checkout with a client app, writes what that app needs for every environment of the project in one run. The binding is a committed file, palbase/project.json, so every later command in the checkout (push, plan, pull, status, deploys, rollback, spec, secret, run, storage, flags, egress, notifications, auth, test-user, apikey) acts on that project without being told again, and a colleague who clones the repository reaches the same project without being told which one it is. Which of the project's environments a command acts on is a separate choice that never touches the committed file: --env <name> for one command, palbase env use <name> to remember one on this machine. palbase unlink removes the binding.

Quick example

palbase project list
# PROJECT    ENVIRONMENT  REF        STATUS
# todoapp    main         k3xq81w4m  Running
#            staging      p8vn2c4rm  Running
# analytics  main         q7wd3n5vm  Running

palbase link todoapp --platform web
# wrote palbase/environments/main/web-config.json
# wrote palbase/environments/staging/web-config.json
# …
#
# linked to todoapp (proj_01)
#   contract read from main; each verb resolves its own environment
# commit palbase/

palbase env use staging
# ▸ todoapp/staging

The lines elided with are the web client's wiring, described in Web codegen. One link wrote both environments; palbase env use staging then chose which one this machine's commands act on, without changing a committed byte.

What you writeWhat it binds
palbase link <project>the project with that name — exact, case-insensitive, as the PROJECT column prints it — or its proj_… id
palbase link <ref>the project that environment belongs to: any row of the REF column binds the same project
palbase link https://<ref>.palbase.studiothe same project, by that environment's address, in any letter case
palbase link https://api.example.com --token-stdina stack you host yourself, by its address
palbase link http://localhost:54321a stack running on this machine, by its address
palbase linkwhat this checkout is already bound to, again

The positional argument and --url are the same thing, and passing either is enough. A name, a project id and an environment ref are counted together: the word has to answer to exactly one of your projects. When it answers to none, the command refuses and lists the projects you do have:

no project of yours is called "todoap", and none of your environments has that ref. Yours:

An address binds the project only when it is one of this cloud's environments and your listing knows it. Everything else stays an address: a stack somebody hosts, a loopback address, an address handed a key with --token-stdin, or any address when the listing cannot be read. An address link still needs a credential the stack accepts before it writes anything.

Note: A ref is 8 random characters from [a-z0-9] plus the letter m — nine characters, carrying no part of the name. palbase project create todoapp does not mint todoappm; it prints the ref it actually minted.

Flags

FlagDefaultWhat it does
--urlthe target, as a flag instead of an argument
--platform(detected)ios, macos, android or web — repeatable, comma-separated
--from-env(see below)the environment the link reads the contract and key from
--insecurefalseaccept the stack's self-signed certificate
--token-stdinfalseread this stack's key from stdin and remember it for this address
--entry, --out(detected)web only: the entry file that imports the client, and the generated file's name

--platform is not decoration: it says which toolchain is on the other end, and each platform gets exactly what its own generator reads.

You usually do not pass it. link reads the checkout — an Xcode project or workspace, an applicationId in build.gradle[.kts], a package.json beside an index.html/public//src/app — and prints what it found (▸ ios, web) before it does anything else. The flag is there to ask for LESS than what is present, not to repeat what already is. It used to default to ios, which meant a web-only checkout silently got an iOS slot and an Xcode build configuration it had no use for — a wrong default looks exactly like a right one.

--insecure exists for one case: a stack still serving the self-signed pair its first boot generated, linked from the machine that runs it. It is opt-in per invocation, never inferred from the host, and it is remembered with the link — in palbase/project.json as "insecure": true, or in this machine's record for a loopback address — rather than retyped on every command.

In a checkout with a client platform, one palbase link writes the config, the contract and the roles of every environment the project has, each in its own palbase/environments/<env>/. An app holds all of them, and the build — or PALBASE_ENV, for web — decides which one it ships. What happens to an environment depends on what it can answer:

  • One that is not running is not asked. It is named, and whatever it already has in the checkout is left exactly as it is: staging is Failed — not asked; its files are left as they are.
  • One that cannot be read, for any platform, is left exactly as it is and named with the reason (staging could not be read …). An environment is written for every platform or for none — half an environment is not written.
  • The default environment failing fails the link: it is what a build with no choice talks to.
  • One with nothing deployed still gets its config; its contract waits for a push: main has no contract to give (…) — `palbase push --env main` . When that is the environment a web checkout reads from, no client is generated and nothing is wired: the link says the web client is not generated yet: main has no contract — `palbase push --env main`, then `palbase link` and exits 0, and the client arrives with that palbase link after the push.
  • On an Apple platform, the directory of an environment the project no longer has is removed when every file in it is one Palbase generates (local never is), and every environment the project still lists is kept — including one this run could not read.

Social sign-in is read the same way for every environment. A checkout has one selection per platform — oauth.<platform> in palbase/project.json, or, when that is not set, the one the default environment identifies from the app's bundle identifier (on Android, the applicationId its Gradle file declares) — and every environment is read with it. An environment that configures the app under another application_key or variant is named, both pairs in the sentence, and left as it is, rather than written without its sign-in client.

A checkout with no client platform — a backend — reads only the environment it links from, and writes palbase/project.json:

▸ no client app here (looked for an Xcode project or workspace, an Android applicationId in build.gradle[.kts], and a package.json beside an index.html/public/src/app) — linking the backend only

Which environment the link reads from. The committed file records the project whichever one it is. --from-env <name> decides where this run's contract and key come from; without it, the link reads the only available environment, else this machine's selection, else main, else the first by name. The closing lines say which:

linked to todoapp (proj_01)
  contract read from main; each verb resolves its own environment

Which environment a command acts on

A target-relative command resolves one environment of the linked project every time it runs:

  1. A stack palbase start is running for this checkout, unless --env names an environment.
  2. --env <name> — a name or a ref — or the PALBASE_ENV variable. Neither is remembered.
  3. This machine's selection, written by palbase env use <name>.
  4. The project's only environment.

With several environments and none selected, the command refuses and says both ways out:

todoapp has 2 environments and none is selected:
  main      k3xq81w4m
  staging   p8vn2c4rm
  palbase env use <name>        remember one for this checkout
  palbase <verb> --env <name>   act on one without remembering it

Every verb that leaves this machine prints where it resolved 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. A stack palbase start runs prints as ▸ http://127.0.0.1:54321 (local), and a stack you host as its address. A stack you host is one installation with one environment, so --env there is refused by name rather than ignored. See Projects & Linking for palbase env list, use, create and delete.

  1. Resolves the target — a project from its name, id, environment ref or environment address, or an address that stays one.
  2. Asks the environment it reads from what it is. GET <base>/.well-known/palbase.json, with no credential, returning {"hosting":"project","sdk_version":"22.1.0"}. A 503 here is retried — see below.
  3. Stores a self-hosted stack's key, if --token-stdin was passed, after verifying it against the stack.
  4. Fetches that environment's publishable key from GET <base>/v1/management/keys, over an authenticated route. This used to ride on the public document, which handed a working client credential to anyone who knew the address; linking is now something you do as somebody.
  5. Writes palbase/project.json — the binding itself.
  6. Describes every other environment — its key, contract and roles — and writes each environment's files with its platform slot, printing wrote <path> for each slot.
  7. On an Apple platform, prints the one-time build-setting snippet (PALBASE_ENV plus the two-line include/exclude pattern) instead of writing a build file — there is nothing left for link to write into the app's build system.
  8. On web, generates the client for the environment it read from and wires it into the app.
  9. On an Apple platform, generates the Swift client, one per environment. If the generator is unavailable the command fails loudly and deletes the generated clients rather than leaving ones emitted from an older contract — those still compile, so the drift would stay invisible until a call 404s on a device.
  10. Reports contract drift when two environments serve different route sets, listing what is only in one and what is missing from the other.
  11. Prints linked to <project> (<id>) and the environment it read from, then commit palbase/.

A link that fails leaves the previous client artifacts where they were — everything is written into a stage and published together. The one file a failed link still publishes is palbase/project.json, once it knows which project this is: a refusal whose cure is palbase push must not also take away the binding that push reads. A stack this checkout was linked to by address on this machine is released whenever the committed record names the project the link was asked for — even when the link failed before reading anything — and the link says so before its error.

local can also appear among the environments link writes: it is added when this machine's register holds a stack for the same project — and it is written even when that stack is down, keyless, with the line naming what to run:

local: http://127.0.0.1:54321 did not answer — run `palbase start`, then `palbase spec` to fill it in

An app whose local configuration vanishes because a container was stopped is an app that stops compiling for a reason nobody connects to the container.

PathCommitted?Contents
palbase/project.jsonyesthe project, by id and name; for a stack you host, its address and insecure when set
palbase/environments/<env>/<platform>-config.jsonyesone flat config per environment — URL, publishable key, app id
palbase/environments/<env>/openapi.jsonyesthe OpenAPI contract, one file per environment, same path for every platform — and it carries that environment's role definitions inside itself, as x-palbase-roles

The per-environment files are written in a checkout with a client platform. Apple platforms additionally get, inside each palbase/environments/<env>/, the generated PalbaseGenerated.swift client and the Palbase-Info.plist the app actually bundles.

For a cloud project, palbase/project.json is {"project": "proj_01", "name": "todoapp"}, written as:

{
  "project": "proj_01",
  "name": "todoapp"
}

It names the project — no address, no environment, no key; the only other fields are the checkout's own, its sign-in selection under oauth and insecure, when set. The project growing a second environment changes nothing in it, and choosing another environment is never a diff on any branch. The publishable key comes from each environment over an authenticated route every time.

For a stack you host, the file carries its address instead — {"url": "https://api.example.com"}, plus "insecure": true when set. A stack on this machine linked by its address is recorded on this machine, not in the checkout, so a colleague's clone is never pointed at your loopback.

A checkout whose file still records one environment's address, {"url": "https://<ref>.palbase.studio"}, from before links bound projects, keeps working; palbase link with no target binds it to the project that address belongs to.

Note: Every platform's config is flat and holds one environment. There used to be a second shape — a native slot carrying a map of every environment in one file — because that file lived at a single path and pointing an app at another environment meant overwriting it. Now every environment has its own directory, so native and web write the same shape, and link prints one wrote line per environment on every platform.

The files that decide what a directory acts on

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 on the machine gets its own records without writing anything into the tree. palbase start writes local.json and palbase stop removes it, so "work locally, then push" is a two-word switch rather than a re-link, and every verb prints where it resolved so nobody has to remember which it was.

The committed file and the machine-local records are never written through each other. project.json is committed on purpose; a write that could reach it from palbase start is how a colleague's checkout ends up pointed at a machine that is not theirs — which is also why the local records live outside the checkout entirely rather than merely being gitignored inside it.

Linking a project releases a stack this checkout was linked to by address on this machine, and palbase unlink releases it too; left in place, that record would go on winning over the project you just linked. A local.json that palbase start wrote is left alone either way: linking that stack — by its address, or with no target in a checkout that has no project record — writes the clients and leaves the record exactly as start wrote it, so palbase unlink leaves it too and palbase stop still ends it. While a stack linked by address, or the stack palbase start runs in a checkout with no project record, is what this checkout acts on, palbase link --from-env with no target is refused: there is one installation and no other environment to read from.

palbase-env.d.ts is committed too (palbase/palbase-env.d.ts, regenerated by palbase build), but it decides nothing about which stack a directory acts on — it only types your own handlers — which is why it does not belong in this table.

Without a linked project or a running local stack, commands explain how to choose a target:

this checkout is not linked to a project.
  palbase link <project>        a project in the cloud
  palbase link <ref>            the project that environment belongs to
  palbase link <url>            a stack you host, or an environment's address
  palbase start                 bring one up here and link to it

An unreadable or invalid machine-local record is passed over: a command resolves from palbase/project.json as if the record were absent.

Nothing under palbase/ is gitignored

Every artefact under palbase/ is meant to be committed. That is what makes a fresh clone behave like the machine that linked it, with no login and no re-link — and there is no partial case any more. The one entry that used to be an exception (local.json, per-machine) and the build output that used to need ignoring (esm/, jobs/, hooks/) both left the checkout entirely — see The files that decide what a directory acts on above and Deploying — so there is nothing left inside palbase/ for a .gitignore to name.

palbase link actively repairs a checkout that still carries the old rule: if .gitignore has a directory-wide .palbase or .palbase/ line left over from the retired layout, it is dropped, not narrowed — there is no local.json left inside the directory to narrow it to. A repository that still ignores that name is a repository carrying a rule for a producer that no longer exists.

The file palbase init scaffolds ignores only the ecosystem's own paths — node_modules/ and *.log. Nothing it writes for palbase/ needs a rule.

Note: .palbase/config.json is retired too. It does not supply settings, secrets or test users, and nothing in the CLI reads or writes it any more — including palbase push, which some older material still credits with reading it.

A brand-new project answers 503 for about half a minute

A freshly created project answers 503 for a short while before it is ready to serve. That is not an error, and link waits it out rather than making you:

push and link retry a 503 for up to 3 minutes at 6-second intervals; every other status is answered on the first try. The wait is announced on stderr — by link, once for each environment it is waiting on:

the project is not serving yet — waiting for it (up to 3m0s)

If the budget runs out, link says so plainly rather than looping:

https://k3xq81w4m.palbase.studio did not start serving within 3m0s — it is a
Palbase address that is not up yet, so try again rather than changing anything

Only 503 is retried. Every other status is a decision, and asking again would only make the same answer slower. The two other refusals from that first request read differently on purpose, because they mean different things:

reach https://example.com: ...
(a self-signed certificate needs --insecure)

https://example.com does not look like a Palbase stack:
/.well-known/palbase.json answered 404

Linking a stack you host yourself

A stack on somebody else's cluster is in no ledger and on no machine of yours, so nothing can tell it who you are except you. --token-stdin reads that stack's key from standard input, checks it against the stack before writing anything, and remembers it for that address in ~/.palbase/credentials.json — after which push, spec, secret and the rest need no environment variable:

palbase link https://api.example.com --token-stdin --platform web < stack-key.txt
# remembered this stack's key for https://api.example.com
# wrote palbase/environments/main/web-config.json
#
# linked to https://api.example.com (project)
# commit palbase/

A stack you host is one installation with one environment, and the app knows it as main. The key goes in on stdin rather than as an argument so it stays out of the shell's history and out of the process list. A key the stack refuses is not stored:

https://api.example.com did not accept this token (401) — nothing was stored

Where the credential comes from

link needs a credential before it can ask for the publishable key. The resolver for a project's own management surface tries four sources, in this order:

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

With none of them available:

no credential for this project: https://k3xq81w4m.palbase.studio.
For a project on this machine, `palbase start` brings its stack up — the stack holds its own key and this reads it from there.
For a stack you host yourself, `palbase link <url> --token-stdin` takes its key and remembers it for that address.
For a cloud project, run `palbase login`, or set PALBASE_ACCESS_TOKEN to a Dashboard-issued token

And when a credential exists but the stack will not take it — usually a stale one for an address rebuilt since — the message names the project rather than the token:

https://k3xq81w4m.palbase.studio did not accept this credential (401).
For a project on this machine, `palbase start` writes a fresh one; for a cloud project, `palbase login`;
for a stack you host yourself, `palbase link <url> --token-stdin`

Re-linking and unlinking

Run palbase link with no target to bring a checkout level with its project: it links what the checkout is bound to again — the stack linked here by address first, as every verb resolves it, otherwise the project palbase/project.json names, otherwise the stack palbase start runs here, otherwise the self-hosted address that record names (carrying its insecure flag) — and rewrites every environment's config, contract and client in place. It is what palbase status tells you to run when an app's key went stale, and what an auth settings change runs for you. To bind a different project, link that project by name.

After a push you usually do not need a full re-link: palbase spec refreshes the contract alone, which is the part that changed. palbase push runs it for you on success.

Two things a re-link deliberately preserves rather than overwrites: a platform slot's app id, and the web config's OAuth block. A writer must not delete what it cannot produce — a run that overwrote that file wholesale would take the app's Apple and Google sign-in with it, and the app would go on building.

To detach entirely, palbase unlink removes palbase/project.json, and on this machine the record of a stack linked by address:

palbase unlink
# ✓ unlinked — removed palbase/project.json
#   generated clients and their imports are left in place
#   re-link with `palbase link <project>`

Generated clients and their imports stay where they are, because deleting somebody's source is not what an unlink is for. The re-link line names how the checkout was bound — palbase link <url> for a stack you host. Unlinking an already-unlinked checkout is not an error — it says this checkout was not linked, because you asked to end up somewhere and you are already there.

palbase logout forgets the linked project's credential first, printing forgot the credential for <target>, and then revokes the cloud session. The link file itself survives — the credential is a fact about you, the link is a fact about the repository.

Checking what a directory is bound to

palbase doctor
# palbase 0.67.0
#   ✓ 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/staging  (via selection)

doctor prints the CLI's version first, then one line per check. It is informative only and always exits 0. Its two binding lines answer two different questions. link is 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 when there is none it says so:

  ✓ link       this directory is not linked — run `palbase link <project>` here

env is where a verb would act right now, and which rule chose it: flag, local, selection or only for a project, url for a stack you host, legacy for a record that still names one environment's address. When that resolution refuses — two environments and none selected — the refusal's first line is printed there instead, because that refusal is the diagnosis.

  • Overview — installing the CLI, logging in, and the full command surface
  • Projects & Linkingproject create, list, status, delete, clone, and palbase env
  • Deployingpalbase push, the rail every linked checkout ships on
  • Running It Locallypalbase start and palbase stop, and the file that wins while they run
  • Codegen — platform detection in palbase link, and palbase spec
  • Introduction — projects, environments and the refs that address them
  • the two API keys — the two keys an Environment is minted with