Stack Settings
Storage buckets, flag definitions, notification senders, the outbound-host allowlist and the auth module's settings live on the stack, not in your repository. Each command writes the linked Environment's own Management API at https://<ref>.palbase.studio/v1/management/* directly, so a change lands the moment the command returns — there is no file to commit, nothing to review, and no deploy to wait for. This inverts what the retired "Config as Code" pages taught: config/storage.ts, config/flags.ts, config/notifications.ts, config/egress.ts, config/secrets.ts and config/test-users.ts are evaluated by no build, travel in no artifact, and are ignored if a leftover copy is still on disk. Two consequences follow and both matter: a remove verb is a live operation rather than a file edit, and reverting a commit undoes none of it.
Quick example
palbase storage add avatars --public --max-size 5MB
# ▸ todoapp/main
# ✓ bucket "avatars" ready
palbase flags add new_checkout --type boolean --default false
# ▸ todoapp/main
# ✓ new_checkout declared (boolean = false)
palbase storage list
# ▸ todoapp/main
# buckets on this stack:
# avatars public 0 file(s), 0 bytes
Both changes are already live on the Environment. Nothing was written to your working tree, and palbase push plays no part in either.
One door, and it is not the repository
Settings used to be declared in config/*.ts and reconciled by every deploy. That model had two writers on one setting, and it failed in both directions: the panel could not change a flag without the next deploy putting it back, and a remove that only edited a file left the live resource — and, for a bucket, its files — in place, so "removed" and "still there" were both true at once.
Today there is one writer per setting, and it is whoever changes it:
| Setting | Command | Route on the stack |
|---|---|---|
| Storage buckets | palbase storage | /v1/management/storage/buckets |
| Flag definitions | palbase flags | /v1/management/flags |
| Per-user flag overrides | palbase flags user | /v1/user-flags/users/<id>, /v1/user-flags/system |
| Notification senders | palbase notifications | /v1/management/notifications/providers |
| Outbound allowlist | palbase egress | /v1/management/egress |
palbase auth, palbase secret and palbase test-user are the other three members of the same family, on /v1/management/auth, /v1/management/secrets and /v1/management/test-users. All eight behave identically, and the surface is the same on a stack you host yourself — every module setting is reachable by command, against whatever the checkout is linked to, and there is no command that exists only for the cloud.
Each of them needs two things: a checkout that is linked, and a credential for that stack. Neither needs palbase push. Every one of them prints its destination to stderr first, so palbase storage list --json | jq still works:
▸ todoapp/main
If the checkout is not linked, the command refuses and tells you how to bind it.
Note: Settings belong to one environment. Like every target-relative command, these act on the environment the checkout resolves to —
palbase env use <name>selects one on this machine, and--env <name>names one for a single command — and a setting added tostagingis not added tomain. The banner names the project and the environment; check it before a live change. See Which environment a command acts on.
When the stack answers 400 or worse, the CLI carries the module's own refusal out rather than replacing it — <error>: <error_description>, or the stack answered <N>: <body> when the body is not an error envelope — and exits non-zero.
What the config/ DSLs do now: nothing
defineStorage, defineFlags, defineNotifications, defineEgress, defineSecrets and defineTestUsers are still exported from @palbase/backend 22.1.0, and their builders still validate their input the moment you author it. They are read by nothing. Neither bundler evaluates config/: the CLI-side bundler stages controllers, jobs and hooks and says outright that it no longer evaluates the directory, and the cloud-side bundler ends by deleting .palbase/config.json outright. palbase build produces no evaluated config, prints no config: line, and a config/ directory containing invalid TypeScript does not fail the build — because nothing compiles it.
Two stories the old pages told have to be deleted, not softened:
config/secrets.tsdoes not exist as a gate.palbase pushnever reads a declaration, so it cannot refuse a deploy because a required secret is unset. A secret exists because it is in the Environment's vault and for no other reason. The runtime's boot linesecrets=Ncounts the names the vault holds, not names an artifact declared.config/test-users.tsships no fixture accounts — nothing reads it. No deploy materialises anything from that file. Test users and their seed templates live on the Environment and are created through its management surface — see Test Users.
Warning: A
.palbase/config.jsonleft on disk by a CLI older than 2026-08-23 is a dead leftover. Nothing in the CLI reads or writes it any more —palbase pushincluded — so it is not even a frozen snapshot consulted by anything; it is inert bytes describing settings you may have changed since, from the panel or with the commands on this page. Delete it.
Delete the config/ directory too when you find one. Keeping it is not harmless: it reads like an authority, and it is not one.
Removing is a live operation
This is the safety rule, and the retired pages had it exactly backwards. They said removal never deletes live resources and that a bad commit could never destroy data. There is no commit involved, and removal removes:
Warning:
palbase flags remove <key>issues aDELETEand the flag definition is gone from the stack — there is no file left to fall back to.palbase notifications remove <provider>deletes the sender and delivery stops.palbase storage remove <name>is documented in-code as destructive — a bucket holds files, and removing here is meant to remove them. None of the three prompts for confirmation, none waits for a deploy, and reverting a commit brings none of them back.
palbase egress remove <host> is the mild one: it narrows what your backend may reach, which fails calls rather than destroying anything, and it takes effect on the next push rather than immediately.
Bucket deletion does not work today
Measured against the storage module's real router and the management mux: DELETE /bucket/{bucket} is not registered. The module mounts only PUT (create or update) and GET (list objects) on that path, so a delegated delete is answered 405 with Allow: PUT before it reaches a handler, and the management layer — which publishes only 200/204/404/409/501 for this operation — turns the unexpected 405 into an HTTP 500. What you see is:
the stack answered 500: DeleteBucket: the module answered 405, which this operation does not publish
Nothing is deleted and nothing is changed. Studio's bucket-delete control calls the same missing route, so it is not a way around this either. To stop using a bucket, delete its objects and leave the bucket in place.
That measurement was taken in-process against the real routers rather than by firing a DELETE at a live stack, because the only way to observe the behaviour on a live stack is to attempt the mutation. Two things follow, and neither should be read as more than it says: the command does not currently succeed, and it is not a safe no-op you should get into the habit of running. When bucket removal ships it will be destructive and immediate — no confirmation prompt, no undo, and the bucket's files go with it.
Storage buckets — palbase storage
Buckets live on the stack. add is create-or-update and idempotent, so re-running it with different limits edits the bucket in place.
| Command | Arguments | Flags |
|---|---|---|
palbase storage list | none | --json |
palbase storage add <name> | exactly one | --public, --max-size, --mime |
palbase storage remove <name> | exactly one | — |
palbase storage add uploads --max-size 5MB --mime image/png,image/jpeg
# ✓ bucket "uploads" ready
palbase storage list
# buckets on this stack:
# avatars public 128 file(s), 4194304 bytes
# uploads private 0 file(s), 0 bytes
--public means anyone with the URL may read it. --max-size is a per-file ceiling written the way you would say it (5MB). --mime is a comma-separated allowlist of content types. A stack with no buckets says so and offers the next command:
this stack has no buckets
add one: palbase storage add avatars --public
Two limits worth knowing before you name things. The CLI does not validate bucket names locally — the stack owns that rule and answers with its own refusal. And palbase storage add has no --variant flag: image renditions are rendered at upload and stored as objects, but the only ways to declare them today are the raw PUT /v1/management/storage/buckets/<name> body and the panel. If you need variants, that is the gap; see Storage for what the bucket accepts.
Note:
palbase pushcross-checks buckets. If a route decorated with@Uploadnames a bucket this stack does not hold, the push is refused before anything ships, naming the buckets and telling you to runpalbase storage add <name>— storage never creates a bucket on demand. See Direct Uploads.
Flag definitions — palbase flags
A flag definition is a typed, project-wide default: a type, a default value, and for string flags an optional list of variants. It lands on the stack immediately.
| Command | Arguments | Flags |
|---|---|---|
palbase flags list | none | --json |
palbase flags add <key> | exactly one | --type, --default, --variants, --description |
palbase flags remove <key> | exactly one | — |
--type is one of boolean, number, string or json. --default is JSON. --variants is a comma-separated list and applies to string flags.
palbase flags add theme --type string --default '"light"' --variants light,dark
# ✓ theme declared (string = "light")
palbase flags remove theme
# ✓ removed flag "theme"
A stack with no flags prints its own next step:
this stack declares no flags
add one: palbase flags add <key> --type boolean --default false
remove is the whole operation, not half of one. Under the old model it edited config/flags.ts and left the live definition standing; there is no file now, so a removed flag is removed.
Dotted, palbase.-namespaced keys (palbase.debug_console) cannot be declared with flags add. They exist as per-user overrides only.
Per-user flag overrides — palbase flags user
An override is runtime state for one user on the linked Environment. It is live the moment it is written and it is never in git.
| Command | Arguments | Flags |
|---|---|---|
palbase flags user set <user-id> <key> | exactly two | --type (required), --value (required) |
palbase flags user unset <user-id> <key> | exactly two | — |
palbase flags user list <user-id> | exactly one | --json |
palbase flags user clear <user-id> | exactly one | — |
--type says how to serialise what you typed. It is not sent to the server: the wire body is {"value": <raw JSON>} with no type field at all. The declared type lives on the flag definition, and the stack rejects a mismatch with 400 type_mismatch. Overriding a key that was never declared is 404 key_not_found.
--type | Accepts | Sends |
|---|---|---|
boolean | exactly true or false | JSON true / false |
number | a numeric literal | a JSON number |
string | the text verbatim | a JSON string ("dark") |
json | any JSON literal, parsed | that literal; objects nest at most three deep |
--type is required and never inferred, because guessing would turn a string flag whose value is literally "true" into a JSON boolean and the write would succeed with the wrong type.
flags user list reads the merged per-user view and the stack's flag list, and calls a key overridden when the two differ. That is the honest limit of the API rather than a display choice: there is no endpoint that reports a per-key source for another user, so an override deliberately set to the same value as the definition's default is indistinguishable from no override and reads as coming from the Environment.
flags user clear removes every override for that user in one call, with no confirmation prompt — overrides are cheap to set again. Flag definitions are untouched.
Notification senders — palbase notifications
Senders live on the stack and take effect immediately. They used to be declared in config/notifications.ts, created on every deploy and never deleted when dropped from the file, so "removed" and "still sending" were both true.
| Command | Arguments | Output |
|---|---|---|
palbase notifications providers | none | the catalog, marking what is configured here |
palbase notifications add <provider> | exactly one | ✓ uploaded secret <PB_…> (encrypted) then ✓ provider "<name>" configured — delivering now |
palbase notifications remove <provider> | exactly one | ✓ provider "<name>" removed |
palbase notifications providers
# notification senders (● configured on this stack):
# ● apns push
# fcm push
# ● sendgrid email
# twilio sms
The listing says which senders are configured, never with what. A provider's secret goes to the Environment's vault under a reserved name derived as PB_NOTIFICATIONS_<PROVIDER>_<FIELD>, encrypted, and is never read back.
That name is reserved by convention only. palbase secret set validates no names: it splits NAME=value (or reads --stdin) and PUTs the value straight to /v1/management/secrets/<name>, and neither the CLI nor the stack checks for a PB_ prefix anywhere. So palbase secret set PB_NOTIFICATIONS_SENDGRID_API_KEY=… succeeds and overwrites what notifications add wrote. Nothing stops you; leave the namespace to palbase notifications. (An SDK doc-comment claims the CLI refuses it. It never has.)
| Provider | Channel | Settings | Secret |
|---|---|---|---|
apns | push | --team-id, --key-id, --bundle-id (all required); --production (default true) | --p8-file |
fcm | push | — | --service-account-file |
sendgrid | --from-domain (required) | --api-key-file | |
ses | --region, --access-key-id, --from-domain (all required) | --secret-access-key-file | |
smtp | --host, --port, --from-email (required); --username, --starttls | --password-file | |
acs | --from-email (required); --from-name | --connection-string-file | |
twilio | sms | --account-sid (required); one of --from-number or --messaging-sid | --auth-token-file |
Pass --production=false on apns for the APNs sandbox; it defaults to the production gateway. sendgrid, ses, smtp, acs and twilio prompt for their secret when you omit the file flag; apns and fcm require the file.
palbase notifications add apns --team-id T --key-id K --bundle-id com.acme.app --p8-file AuthKey.p8
palbase notifications add fcm --service-account-file service-account.json
palbase notifications add sendgrid --from-domain mail.acme.com # prompts for the API key
palbase notifications add twilio --account-sid AC.. --messaging-sid MG.. # prompts for the auth token
Because cobra declares every provider's flags on one command, palbase notifications add --help lists the union of all of them. Only the ones in this table apply to the provider you named.
The help text no longer mentions a file: it used to say the non-secret fields were written to config/notifications.ts, and that sentence went when the file did. Nothing add does touches your working tree — the fields go to the Environment and the secret goes to the vault.
A stack that has not been told how to send mail says so rather than inheriting somebody else's account: there is no default managed email or SMS sender on the current runtime. palbase notifications providers is how you check what this Environment actually holds.
Sends are also capped as an operator spend guard, not as a plan feature: by default 100 emails and 50 SMS per project per calendar month, counted at request time and incremented only when a send is allowed. Over the cap the send is answered 429 with Retry-After and the counter is not moved. Push and inbox messages are never capped. An operator can raise, lower or disable either cap.
The outbound allowlist — the one setting that waits for a push
palbase egress is the exception to everything above. The list is stored on the stack the moment you write it, exactly like the others, but the deploy is what stamps it into the artifact — so a change takes effect on your next palbase push, and both add and remove say so:
palbase egress add api.stripe.com
# api.stripe.com allowed — effective on the next deploy
palbase egress remove api.stripe.com
# api.stripe.com removed — effective on the next deploy
palbase egress list
# hosts this stack allows:
# api.stripe.com
# .example.com (and subdomains)
add on a host that is already allowed prints <host> is already allowed. remove on a host that was never allowed prints <host> was not allowed and writes nothing — it is a no-op, not an error. palbase egress has no --json flag, unlike flags list and storage list.
A stack that has never had an allowlist set is unrestricted, and the runtime says so at boot rather than leaving you to guess:
[runtime] egress: UNRESTRICTED — no allowlist is set for this stack (`palbase egress add <host>`)
Warning: Removing the last host does not mean "call nothing" — it means unrestricted again. The deploy only stamps an egress declaration into the artifact when the list is non-empty (or a timeout is set), so an emptied list produces an artifact with no declaration at all; the runtime then treats undeclared as allow, installs no fence, and logs the
UNRESTRICTEDline above. The two states are indistinguishable end to end. "Call nothing" is written in the API's field comment and implemented nowhere. To deny everything, there is nothing to type today.
Hosts are bare hostnames: no scheme, port, path or wildcard, and no IP literals. A leading dot (.example.com) also covers subdomains. The CLI validates the host locally with the same rules the stack applies, so you learn the rule from the command rather than from a failed deploy. The full rejection table and the runtime behaviour of a denied call are on Outbound Network.
Note: A denied call at runtime throws
egress denied: <host> is not in this backend's declared allowlist. Add it to config/egress.ts and deploy.That message names a file that no longer exists. The fix ispalbase egress add <host>followed bypalbase push.
Auth settings — palbase auth
The auth module is the fifth member of this family: palbase auth settings, providers, sessions, audit, templates and mfa all write /v1/management/auth on the linked stack and act immediately, with nothing to push. palbase auth always emits indented JSON so a shell pipeline can read it, and a 204 prints nothing at all rather than an invented "ok".
One detail is load-bearing enough to repeat here: palbase auth settings set reads the current document first when you use named flags, because the module's PUT replaces the whole document and a partial body erases what it does not mention. Only the flags you actually changed are sent. The full surface is on Auth Settings.
What a push does carry
A push carries code and schema — your controllers, everything under db/, and the job, hook and webhook manifests that travel with them. It carries no settings, because settings reach the stack directly from whoever changes them, and a copy riding along in a deploy could only overwrite what somebody set from the panel a minute earlier. The one thing this page describes that a deploy does touch is the egress allowlist, and even there the deploy is handed what the stack currently holds rather than re-deriving it from a file.
The practical consequence for a team: a settings change is not reviewable in a pull request, and it is not reverted by reverting one. If you need a record of who changed what, palbase auth audit covers the auth module, and Studio shows the current state of the rest.
Related
- Linking a Checkout — which project and environment these commands act on
- Auth Settings — the auth module's own settings surface
- Secrets — the Environment vault, written through the same door
- Test Users — disposable accounts and the templates that live on the stack
- Outbound Network — the host rules, and why the allowlist waits for a deploy
- Storage — what a bucket holds, and how your code reaches it
- Flags — reading flags and overrides from a controller
- Notifications — sending push, email, SMS and inbox messages
- Deploying — what
palbase pushactually ships