Client Apps

Syncing an App's Settings

palbase apps:sync writes into an app's repository what the app is built with in each environment it is set in: the environment's address, the app's publishable key, what the modules tell a client, and a typed client of each backend the environment runs. It writes under palbase/ and touches nothing outside it. The build reads those files, so nothing reaches the network when the app is built, and a URL or key is never typed into the app.

Running it

From the repository's root, name the apps it builds:

palbase apps:sync shop-web

The apps named join the checkout's, which palbase/project.json records. After that, palbase apps:sync with no name syncs them all. One checkout may build several apps: a web app and its admin site, or an iOS and an Android app side by side.

Run it again whenever the project changes: the app set in a new environment, its key replaced, a machine added, a backend's routes or gateways changed. Each file is made whole from what the project gives now and compared with the one on disk; only a file that differs is written. A file it made that the project no longer gives, such as an environment the app left or a machine removed, is deleted. Commit what it writes.

Each run prints what it added, changed and removed, and notes what needs you: an app in no environment yet, a machine with no contract to make a client from, the keychain entitlement an iOS app needs.

What it writes

palbase/
├── project.json
├── shop-web.ts                     web: the editor's environment
├── shop-web.main.ts                web: one per environment
├── shop-web.staging.ts
├── shop.palbase                    iOS: every environment, as Swift
├── palbe/
│   ├── backend.ts                  web: the editor's environment
│   ├── backend.main.ts
│   └── backend.staging.ts
└── environments/
    ├── main/
    │   ├── shop-web.json
    │   ├── shop-web.ts
    │   ├── shop.json
    │   ├── contracts/backend.openapi.json
    │   └── palbe/backend.ts
    └── staging/
        └── …
FileForWhat it holds
palbase/project.jsonevery checkoutThe project's id and name; the checkout's apps (id, slug, name, platform); the editor's environment (types_environment); the backends' names in the apps (clients)
palbase/environments/<env>/<app>.jsonevery appThe app's settings in that environment
palbase/environments/<env>/contracts/<machine>.openapi.jsonan environment with a backendThe contract the machine's serving build answers
palbase/environments/<env>/palbe/<machine>.tsa web appThe backend's typed client, in TypeScript
palbase/environments/<env>/palbe/<machine>.ktan Android appThe backend's typed client, in Kotlin
palbase/<app>.palbasean iOS appEach environment's configuration and its backends' clients, in Swift: one file, which the Xcode build rule reads for the build's environment
palbase/environments/<env>/<app>.tsa web appThe app's settings module: settings, connect() and pb, the client typed with the environment's backends
palbase/<app>.ts, palbase/palbe/<machine>.tsa web appThe editor's environment, re-exported
palbase/<app>.<env>.ts, palbase/palbe/<machine>.<env>.tsa web appEach environment's, which a build made for it is type-checked against

palbase/environments/ and palbase/palbe/ are the sync's own: a file there that it would not write is removed, except a .storekit file, which palbase purchases:storekit makes. At the top of palbase/ it touches only project.json, .palbase files and files that start with its header, // Code generated by palbase apps:sync. When palbase/, or a folder or file in it, is a symbolic link, it writes nothing.

The app's settings

palbase/environments/<env>/<app>.json is the document palbase apps:sdkconfig <app> -E <env> prints:

{
  "schema": 1,
  "project": { "id": "0b9c4e7a-…", "name": "Shop" },
  "environment": { "ref": "k3xq81w4m", "slug": "staging", "name": "Staging", "production": false },
  "app": { "id": "4f6c2d10-…", "name": "Shop Web", "slug": "shop-web", "platform": "web", "web": { "origins": ["https://staging.shop.example.com"] } },
  "url": "https://k3xq81w4m.palbase.studio",
  "publishable_key": "pb_k3xq81w4m_c…",
  "auth": { "rp_id": "k3xq81w4m.palbase.studio", "associated_domains": ["webcredentials:k3xq81w4m.palbase.studio"] },
  "realtime": { "url": "wss://k3xq81w4m.palbase.studio/realtime/v1" },
  "backends": [{ "machine": "backend", "url": "https://k3xq81w4m.app.palbase.studio", "contract_digest": "9e1f…" }],
  "digest": "sha256:5b2a…"
}
FieldWhat it is
environment.productiontrue for the project's first environment, main
appThe app, with what it is in this environment (ios, android or web)
url, publishable_keyWhat the app's client is made with: the environment's address and the app's own key
keychain_groupAn iOS app's: <App ID prefix>.<bundle>.palbase, the keychain access group the app and its extensions keep the sign-in in
authThe passkeys' relying party, and the iOS app's Associated Domains entry
backendsThe environment's machines: each one's address, and the digest of the contract its serving build answers (none while it is unread)
digestThe SHA-256 of the document without this field: a build made with another digest is not current

An iOS app needs the Keychain Sharing capability with the group $(AppIdentifierPrefix)$(PRODUCT_BUNDLE_IDENTIFIER).palbase in every target that uses Palbase: without it the SDK keeps no session, and sign-in fails with keychain_unavailable. The sync reminds you each run.

--format env prints the address and key alone, for a build that does not read palbase/ (another framework, a game engine):

palbase apps:sdkconfig shop-web --format env -E staging
PALBASE_URL=https://k3xq81w4m.palbase.studio
PALBASE_PUBLISHABLE_KEY=pb_k3xq81w4m_c…

The publishable key is public: it ships in the app. A secret key never does, and no file the sync writes holds one.

A build and its environment

The settings of every environment are in the checkout; the build picks one.

  • Web. An app imports pb from palbase/<app>.ts. For the editor, that is one environment's: --types <env> names it, palbase/project.json remembers it, and without one it is the first environment the app is in that is not main. A build imports its own environment's instead: the Vite plugin from palbase/web/vite, palbase({ app: "shop-web", environments: { development: "staging", production: "main" } }), maps each vite --mode to an environment and type-checks the app against that environment's backends first. See Calling Your Backend.
  • iOS. The Xcode build rule extracts the Swift of the environment its configuration's PALBASE_ENVIRONMENT names from palbase/<app>.palbase; a configuration that names none, or one the app is not set in, stops the build. See iOS SDK.
  • Android. The app's build.gradle.kts maps each build variant to an environment and compiles that environment's <app>.json and Kotlin clients into the variant's sources; a variant that names no environment, or one the app is not set in, stops the build. See Android SDK.

Backends

Each machine of the environment becomes pb.<name> in its apps, typed from the contract its serving build answers: its routes, their models and typed errors, and its gateways. The name is the machine's camelCased, pb.shopApi for the machine shop-api, unless --as shop-api=shop names it otherwise. A name is the checkout's, the same in every environment, kept in palbase/project.json, and never one of the client's own (auth, flags, storage, …).

  • A machine whose contract has not been read yet gets no client, and the run says so. A contract is read once the build's version alone serves: a palbase/backend app writes its own; for another program, palbase machines:update <machine> --openapi-path <path> names where it serves one.
  • When a push's version did not roll out (its contract was refused, its tests failed), the client is made from the build that serves, and the run says so.
  • Two backends of one environment that declare the same type, both with a HealthController say, cannot share one Swift or Kotlin target: the run stops (backends_collide) and names it.

--backend [<machine>=]<dir> makes a backend's client from its checkout on this computer instead, to call a route before any machine serves it. The CLI runs bun --no-env-file run src/main.ts there with PALBASE_CONTRACT_OUT set: the palbase/backend app writes its contract and exits, connecting to nothing and reading no .env file. Sync again without --backend once the backend is deployed.

In CI

palbase apps:sync --check

--check writes nothing. It exits 1 when the committed files are not what the project gives now, and lists each file as missing, differing or no longer given.

An environment token sees its own environment alone, so a sync made with one writes that environment's files and counts every other environment's as no longer given. For a checkout of several environments, run the check with an organization's access token of the member role.

When it refuses

CodeWhy
another_projectThe checkout's palbase/ is another project's. Run it for that project (-P), or in this project's own checkout.
app_deletedThe app is deleted; palbase apps:restore brings it back.
no_such_appThe project has no app by that slug or id; the answer lists its apps.
environments_collideTwo environments' slugs differ only in letter case: their folders would be one on macOS and Windows.
linked_pathpalbase/ or a folder in it is a symbolic link.
unusable_contractA backend's contract makes no client in the app's language: fix the backend, deploy it, and sync again.
invalid_backend_nameAn --as name is a word the client or the language keeps.