Deploying
Code reaches an Environment one way: palbase link, then palbase push. There is no repository-driven deploy, no git push rail, no --mode and no GitHub integration on this cloud — a linked checkout builds its backend locally with Bun, packs the result into a gzip tarball, and posts it to the project's own management API, which applies the schema and activates the code in one request. There are no migration files either: what db/ declares is diffed live against the database the project is actually running, and the push carries the change. This page covers what travels, what refuses, and the four verbs that tell you what happened afterwards — status, deploys, rollback and logs.
Quick example
palbase plan
# code
# typed 3 controller file(s) from their return types
# built 3 controller file(s) → 3 controller(s) [bun 1.3.9]
# runtime
# 36.0.2 → 37.0.2 (migrations run against the live project before the new version takes over;
# the switch is a restart of seconds and requests wait)
# auth: expand 13→14, contract 0→1
# schema
# added column todos.priority text
# plan written: ~/.palbase/checkouts/<hash>/plan.json (3f9a1c2b0d4e)
palbase push
# ▸ todoapp/main
# applying plan 3f9a1c2b0d4e
# built 3 controller file(s) → 3 controller(s) [bun 1.3.9]
# bundled 2 job(s) → nightly-report, digest
# bundled hook(s) → before.user.create
# bundled 1 test suite(s)
# sending /Users/you/todoapp (412 KB)
# schema:
# added column todos.priority text
# live: 14 endpoint(s), 57788ca062dc
The first line is the banner every remote verb prints — the destination, on stderr, before any work happens, so palbase status --json | jq still works and a scripted run still says where it went. The schema: block appears only when db/public.ts differs from the database, and it reports what the apply did — added column …, DROPPED column … — not what a plan would have proposed. The last line is the whole result: how many endpoints are answering, and the short digest now serving them.
After a successful push the CLI refreshes the committed contract for you. The contract just changed, and this is the only moment a committed client can be brought level without anyone remembering to. If that step fails the push has still landed, and it says so:
the push landed, but the client could not be regenerated: ...
One rail: link, then push
palbase push takes no arguments. Its flags are --approve for destructive schema changes and --accept-breaking for overriding the running release's schema compatibility check. It acts on the environment this checkout resolves to — one environment of the project palbase/project.json names, or the stack palbase start runs here while its machine-local record exists:
palbase link todoapp
palbase push # the selected environment, or the only one
palbase push --env staging # staging, for this push only
--env <name> names another environment of the linked project for one command, and palbase env use <name> remembers one on this machine; acting on another project means linking to it. See Which environment a command acts on.
A push posts to POST <target>/v1/management/push with content-type: application/gzip. A freshly created project can answer 503 for a short while; push and link retry a 503 for up to 3 minutes at 6-second intervals, announcing the wait once, and every other status is answered on the first try:
the project is not serving yet — waiting for it (up to 3m0s)
A 4xx is a decision, and asking again would only make the same answer slower.
Note: There is one flag,
--approve, and it covers every dangerous thing a push can do: running a schema change that takes data away, and replacing a secret the target already holds. One flag on purpose — a person facing a refusal should not have to work out which of several flags this particular refusal wanted.
No push without a plan
palbase push reads the measurement palbase plan left for this checkout — ~/.palbase/checkouts/<hash>/plan.json, outside the tree, keyed by the checkout's own path. Without it, push exits before it
touches anything — not one request goes out. Run palbase plan and read what it
says; that file is what push then carries.
If the bundle, the running SDK or the schema plan changed since you planned, push names what moved and asks for a new plan rather than proceeding:
plan is stale: runtime moved 36.0.2 → 37.0.2; bundle changed; run `palbase plan`
The platform checks the same thing on its side, so an old plan cannot be replayed against a project that has since moved. The plan file is local state, and it lives outside the checkout entirely — there is nothing to gitignore and nothing a commit could ever carry.
Runtime upgrades never interrupt the project
When your checkout requires a newer @palbase/backend than the project is
running, the migrations that version needs run while your project is still
serving, and before the new version takes over. The switch that follows is a
restart measured in seconds, and requests wait rather than fail.
If a migration cannot run, the project is left exactly as it was — same image, same schema, still serving — and push prints the platform's report in full. That report is for us: a check that refuses an upgrade is a platform defect, and there is no inventory to take, no reset to run and nothing for you to fix.
A push at a local target is refused
While palbase start is running, the checkout is pointed at the stack on this machine, and that stack serves your source directly — it rebuilds as you save, with no artifact and no version history. Pushing there would activate a version nothing loads, so it refuses and names both ways out:
this checkout is pointed at the stack running on this machine, which already
serves this directory — a push here would activate a version nothing loads.
palbase stop point it back at the project, then push
palbase db apply if it was the schema you wanted applied here
What travels
The tarball is your current working directory, not your git HEAD. Uncommitted changes deploy exactly as they are on disk. Paths are relative, with no wrapper directory.
Always excluded, at every depth:
| Rule | What it covers |
|---|---|
| Directories | .git, .palbase, palbase, node_modules, .next, .palbase-build-controllers, .palbase-staged-controllers |
| Secret file globs | .env, .env.*, *.env, *.pem, *.key, *.p8, *.p12, *.pfx, id_rsa, id_dsa, id_ecdsa, id_ed25519 |
| Symlinks and special files | never followed, never packed — only regular files are included |
Every depth is load-bearing: a nested web/node_modules once shipped 357 MB and killed a deploy with an opaque RESOURCE_EXHAUSTED. The secret globs are excluded unconditionally and cannot be re-included by a .palignore. Palbase writes no dotenv file anywhere — there is no secret pull — but a hand-made one must never ride inside a deploy bundle. Runtime configuration belongs in Secrets.
Note:
palbase/— the CLI's own directory, committed and all — is excluded from the tarball too, right alongside the retired.palbase. Its contents (the link, the platform slots, the generated clients) are the CLI's own bookkeeping, not backend source. The layout migration briefly dropped this exclusion by accident — the list still named the old.palbase, and the newpalbasesailed straight through it — and a three-environment project was enough to blow past the platform's 4 MB request ceiling with an opaqueRESOURCE_EXHAUSTED. Both names are excluded on purpose now. One thing under it does travel: the backend's string table —palbase/strings/, or an older checkout'spalbase/strings.json— because the stack answers requests from it..palignorecannot drop it; a push refuses a table the target stack cannot read, and the stack refuses a table-less push from a CLI too old to carry the directory onto a release that has one (Localization).
Your own exclusions — .palignore at the project root, one glob per line, # comments and blank lines skipped. Each pattern is matched against the file's full relative path and its basename; * does not cross directory separators.
# .palignore
scratch-*
fixtures/*
docs/README.md
And then two paths are added back from a temporary build root — never from the checkout, because they are the build output the stack actually runs. palbase push builds this project first, into a fresh directory the OS temp folder holds for the life of the command (build output has not lived in the checkout since 0.61.1), and the tarball reaches into that directory for:
| Path (inside the temporary build root) | Why it travels |
|---|---|
.palbase/esm/ | the bundled controllers, plus one bundle per test suite |
.palbase/jobs/jobs.manifest.json | the only way the scheduler learns when a job is due |
Warning: the hook manifest is missing from that list, and that is the bug — measured 2026-09-11.
.palbase/hooks/hooks.manifest.jsonis the only way palsvc learns which events a project handles, and it is what the deploy reaches for; but the builder stopped writing it on 2026-09-01, when the move from directory discovery to the container left only the job half of the manifest writer standing. The tarball packs that path if it exists, so nothing errors — a deployed@Hookor@Onsimply never fires. Jobs and webhooks are unaffected. See Event Hooks.
That .palbase/ is the temporary build root's own internal shape, not your checkout's — nothing here is ever written to disk in your project, so there is no directory to find with ls, gitignore, or accidentally commit. Nothing from the checkout's own palbase/ travels either: not the link target, not the platform slots, not the fetched contract — that whole directory is excluded from the tarball, as above. .palbase/config.json used to be carried this way and no longer exists — settings reach a stack directly, through their own commands, and are described on Stack Settings.
Warning:
node_modules/is always excluded, so your installed dependencies are not uploaded. The managed runtime provides@palbase/backenditself.
Jobs, webhooks and hooks ship — because a module lists them
There are no jobs/, webhooks/ or hooks/ directories, and there is no root controllers/ either. There used to be four such globs, and every one of them was a second declaration the module system never saw: a class carrying @Job outside jobs/ never ran, and one inside it ran whether or not any module listed it.
The bundler has one glob now — *.module.ts, found wherever a domain keeps it — and a @Job, @Webhook or @Hook class ships because a module names it in providers, exactly as a controller ships because a module names it in controllers:
// modules/billing/billing.module.ts
import { Module } from "@palbase/backend";
import { NightlyReport } from "./nightly-report.job";
import { StripeWebhook } from "./stripe.webhook";
@Module({
controllers: [],
providers: [NightlyReport, StripeWebhook],
exports: [],
imports: [],
})
export class BillingModule {}
Both of those are ordinary classes in modules/billing/, exported by name — the
surface decorator is what they ARE, the module list is what makes them ship:
// modules/billing/nightly-report.job.ts
import { Job } from "@palbase/backend";
import type { JobMeta } from "@palbase/backend";
@Job({ name: "nightly-report", schedule: "0 3 * * *" })
export class NightlyReport {
run(_meta: JobMeta): Promise<void> {
return Promise.resolve();
}
}
// modules/billing/stripe.webhook.ts
import { On, Webhook } from "@palbase/backend";
import type { WebhookMeta } from "@palbase/backend";
@Webhook({ name: "stripe", provider: "stripe", secret: { env: "STRIPE_WEBHOOK_SECRET" } })
export class StripeWebhook {
@On("checkout.session.completed")
checkoutCompleted(_event: unknown, _meta: WebhookMeta): Promise<void> {
return Promise.resolve();
}
}
What the build then reports is read out of the built bundle's own container, not out of a directory listing, and every surface answers for itself by name:
bundled 2 job(s) → nightly-report, digest
bundled 1 webhook(s) → /webhooks/{stripe}
bundled hook(s) → before.user.create
That read exists because the stack's own activation gate asks only whether endpoints are served — jobs, webhooks and hooks are invisible to it, so a deploy that quietly carried none of them reported success. Four @Job classes once deployed to a live project with none of them in the bundle, no error anywhere, and zero rows in the job table. The gap cannot open the same way now: a job exists because a module lists it, and the same module is what the bundle imports.
Two rules survive the move, and they are not stylistic:
- The name is DECLARED, in the decorator —
@Job({ name: "nightly-report", schedule: "0 3 * * *" }). Two jobs claiming one name is refused by name:two jobs are named "report". The name is DECLARED (@Job({ name })), so rename one of them. - No surface, no manifest. The scheduler learns when a job is due from
jobs.manifest.json(inside the temporary build root's.palbase/jobs/, see What travels) and from nothing else, so when a build finds no jobs the manifest is removed rather than left stale — a leftover file would keep a deleted job on a schedule nothing declares.
If any of this refuses, the temporary build root's .palbase/esm, .palbase/jobs and .palbase/hooks are all removed — a refused build must leave nothing behind that could be deployed. None of this was ever in your checkout to begin with.
Warning: Be on a current CLI. A binary from before 2026-08-26 emits empty arrays instead of the real definitions, and it does so silently: the push succeeds, the endpoints come up, and every
@Job,@Webhookand@Hookin the project is quietly gone from the deployed artifact. One older still walks the four directory globs the bundler no longer has, so it finds nothing at all in amodules/tree. Check withpalbase --versionand upgrade — see Overview for the install and upgrade commands.
Tests travel too. Each tests/*.test.{ts,js,mts,mjs} becomes its own bundle under that temporary build root's .palbase/esm/tests, because the runner reports per file and a single blob would collapse every suite into one name. A helper beside them is pulled in by the suite that imports it. A project with no tests/ directory is perfectly legitimate.
The bundler is Bun
The bundle is built by bun build <entry> --target=bun --format=esm, not by esbuild and not by Node. The stack runs Bun, so the bundle is built by the engine that will run it, and a missing Bun is a hard refusal rather than a fallback:
bun is not installed, and it is what builds a backend for this runtime
(https://bun.sh). The stack runs Bun, so the bundle is built by the engine
that will run it
Two more refusals come from the same step. A backend is a schema plus at least one module — db/public.ts and a *.module.ts somewhere in the tree — and a directory that is neither is told what was looked for rather than what was missing in the abstract:
this is not a backend checkout: no db/public.ts and no *.module.ts here
(/Users/you/todoapp).
Run this where your modules live, or `palbase init` to start one
A directory named controllers answers for nothing here, on purpose: it used to, and palbase push refused a real project whose domains live under modules/<name>/ — after palbase build had already validated 85 routes in it. And a bundle that carries no controllers is refused outright — the bundle carries ZERO controllers — nothing would answer — because the alternative is a deploy that reports success and serves nothing.
The printed Bun version is not decoration:
built 3 controller file(s) → 3 controller(s) [bun 1.3.9]
The bundle is byte-identical per toolchain but not across machines, so two people pushing the same commit can activate two different digests, and the version is what lets you tell which.
Note: The bundler restores controller class names before emitting the entry. Minifiers rename duplicate top-level identifiers, and the contract's operation namespace comes from the class name — eleven files all declaring
PalaiControlleronce shipped a contract split intopalaipluspalaiController2throughpalaiController11, breaking every generated client. The restoration reads the names out of your source and fails closed.
Previewing a push — palbase plan
palbase plan takes no arguments and no flags, writes nothing to the target, and answers the two questions a push can fail on:
palbase plan
# ▸ todoapp/main
# code
# built 3 controller file(s) → 3 controller(s) [bun 1.3.9]
# 2 @Upload route(s), every bucket exists
# schema
# add column todos.priority text
The section headings are the bare words code and schema, with no colon. The code section runs the same function the push runs, not merely a build of the same sources. That matters: when plan used one bundler and push used another, a plan going green on code the push refused was a check whose passing meant nothing. The schema section posts what db/ declares to the stack and prints what the stack says it would do; with no schema file it says so — no db/public.ts — this project declares no tables.
There is deliberately no config section. The target publishes no route reporting what it currently holds, so such a section could only say what would be sent, never what would change.
@Upload buckets are checked against the stack's own bucket list rather than against a file. Storage will not create a bucket on demand, so an unknown name would compile, deploy, activate, and 404 the first upload.
Validating locally — palbase build
palbase build takes no arguments and no flags. It runs the same staging and metadata extraction the deploy runs, so the decorator mistakes that pass tsc and break the deploy are caught here:
palbase build
# ✓ palbase/palbase-env.d.ts
# build OK — 14 route(s) across the controllers would deploy cleanly, plus 2 job(s), 1 webhook(s), 1 hook(s)
The plus … suffix exists for one reason: a bare total hid four dropped @Job classes on a live project. Every surface answers for itself.
When something is wrong it prints the deploy's own error and exits non-zero:
palbase build
# ✗ DEPLOY WOULD FAIL: modules/todos/todos.controller.ts — route "GET /todos":
# @QueryParams(...) must be given a zod schema, but got the string "workout_id".
Exit contract: 1 only for a user-code validation error — a bad decorator, an invalid return type, a version skew. 0 when it passes, and also when this machine cannot run the check at all; a warning is printed and the server still gates the real deploy. Failing open on an environment problem and closed on your code is the split that makes it usable in a hook.
It is not offline. When @palbase/backend is absent it installs it, and it installs the runtime's own zod-to-json-schema with --no-save; both failures warn and continue. It also writes palbase/palbase-env.d.ts, the typed environment declaration — that is why palbase db types was removed, since palbase build produces everything derived.
It does not gate on your SDK version. The runtime vendors every @palbase/backend major inside a support window and builds each project against the one its lockfile resolved, so a ^12 project deploys fine on a runtime whose newest major is 13. The CLI reports the version; the authoritative check is server-side.
The pre-push git hook
The hook is version 3, marked on line 2 of its body as # palbase-hook: v3. It runs palbase build and nothing else — v3 dropped the old schema-drift check, because that question has no meaning once the schema is declarative.
✗ palbase build failed — this push would produce a FAILED deploy.
It exits 0 immediately when palbase is not on the PATH, so a colleague without the CLI is never blocked. Bypass it with git push --no-verify; the server always gates the actual deploy.
Removed. The pre-push hook is gone, and so is the subsystem behind it. Its installer was called from exactly two places, both inside the retired
repository_provider = githubbranch, so nothing ever wired it up whilepalbase doctorreported it missing and advised a command that could not install it. On this railgit pushdoes not deploy — the build gate ispalbase push's own — so the hook guarded a step that no longer carries a deploy.The report line that named it is gone with it:
palbase doctorno longer prints ahookrow, and no build of the CLI has told anyone to install one since. Runpalbase buildyourself before pushing, or wirecore.hooksPathby hand.
When a push refuses
Nothing is swapped by a refused push — the previous release keeps serving.
A schema change that takes data away comes back as a 409, itemised with row counts, and needs --approve:
this push would remove data:
drop column todos.notes (1284 rows, 903 non-null)
drop table drafts (17 rows)
repeat with --approve when that is what you mean
--approve appends ?accept-data-loss=true to the push. palbase db apply --approve uses the same consent flag against a local stack. --accept-breaking separately permits a schema change that conflicts with the currently serving release.
Credential and permission refusals name the fix rather than the status:
| Status | What it says |
|---|---|
401 | that stack no longer accepts this session — run `palbase login` |
403 | this account may not manage <url> — ask whoever runs it for `palsvc --grant-management` |
Refusals the stack decided print their own description verbatim — the stack's sentence and, for a test run, the run's output — then a closing line. The CLI adds no count and no summary of its own. The codes that read this way are tests_failed, tests_timed_out, schema_incompatible, candidate_failed and test_identities_unavailable:
the tests failed against the new release, so it was discarded and the previous one keeps serving.
<the run's own output>
push refused (tests_failed) — nothing was swapped, the previous release keeps serving
Anything else is reported as push refused (<status>): <body>, trimmed.
Secrets
Secrets are managed directly on the linked stack with palbase secret. A push does not copy secrets from another stack, and the retired .palbase/config.json is never read. See Secrets.
What a push does not carry
A push carries code and schema. Settings do not travel: storage buckets, flag definitions, notification senders and auth settings are written straight to the stack by their own commands and take effect immediately, so a copy riding along in a deploy could only overwrite what somebody set from the panel a minute earlier. The single exception in timing is the outbound allowlist — palbase egress add stores the host immediately, and the next deploy stamps it into the artifact, which is why the command answers effective on the next deploy.
The push output has no config: line, and that absence is the point. See Stack Settings.
What is live — palbase status
palbase status
# project: https://k3xq81w4m.palbase.studio
# address: https://k3xq81w4m.palbase.studio
# credential: the cloud (this project's key)
# deployed: 57788ca062dc, 37 endpoint(s), SDK 22.1.0
# activated 2026-08-26 14:02
status names the address, not a project-and-environment id pair, because the address is what makes "which runtime was looked at" unambiguous. The deployed line is read from GET /v1/management/deployments/current on the project itself. A project that has never deployed says so:
deployed: nothing yet — `palbase push`
…and a checkout pointed at the stack on this machine says something different, because that stack has no versions at all:
deployed: n/a — this stack serves this directory, and rebuilds when you save
--json is the machine surface: {project, address, credential{source,kind}, deployed{digest,endpoint_count,activated_at,sdk_version}|null, reason, app_key, last_attempt}. last_attempt is the newest row in your account's push ledger, which is a different fact from deployed — a push that never reached the project is not in the project's history, and that is exactly the failure worth seeing.
Deploy history — palbase deploys
palbase deploys
# VERSION ACTIVATED SDK
# 57788ca062dc 2026-08-26 14:02 22.1.0
# a1b2c3d4e5f6 2026-08-25 09:18 22.1.0
#
# ▸ 57788ca062dc is serving 37 endpoint(s)
#
# `palbase rollback a1b2c3d4e5f6` serves that version again
The closing hint names the last row of the listing, and the listing is newest first — so it points at the oldest version the project still holds, not at the one above it. Read it as "this is the shape of the command", not as a recommendation.
--json emits one JSON array containing the deployment history, including full digests, activation times, SDK versions and active markers. An empty history is []. Two empty states, and they mean different things: nothing deployed yet — palbase push for a project with no versions, and no deploy history — this stack serves this directory, and rebuilds when you save for a checkout pointed at the local stack.
Rollback — palbase rollback <version>
palbase rollback 57788ca
# ▸ 57788ca062dc is active
# serving 37 endpoint(s)
What you type is a prefix; what it echoes is the digest it resolved to, truncated to the same twelve characters palbase deploys prints. A rollback is a pointer move to a stored artifact, not a new version — nothing is rebuilt, nothing is rewritten, and rolling forward again is the same command with a different digest. It takes the short digest palbase deploys prints and resolves it by prefix. It has no flags.
| What you gave it | What it says |
|---|---|
| nothing | which version? `palbase deploys` lists them |
| a prefix matching none | no version starts with <x> — this project has <short digests…> |
| a prefix matching several | <x> matches 3 versions — give more of it |
| a project with no versions | this project has deployed nothing to go back to |
The stack waits for the runtime to confirm it is answering from that digest — about ten seconds — so nothing here polls. A 404 means the project has no such version; a 422 means the pointer moved and the runtime either never came or came and served nothing, and it is reported as a refusal rather than as a successful rollback.
Pulling the deployed code — palbase pull
palbase pull
pull replaces the backend in this directory with the one the project is serving, so it refuses a dirty tree first. In a linked checkout it GETs the deployed source from the project's own management API — /v1/management/deployments/latest/source — and extracts the tree over your working directory.
That is the whole operation. No git process is run and there is no merge, so no conflict markers are ever produced — which is exactly why the clean-tree check comes first: overwriting is the only thing it knows how to do. (The Homebrew formula does declare a git dependency, and its comment says the 3-way merge is the reason. The comment is stale; merge-file appears nowhere in the CLI.)
Two refusals rather than a silent replacement: a project that has never deployed, or whose live version predates source retention, answers 404 and is reported as <project> has no source to pull: …; an empty archive is refused outright, because extracting one succeeds at replacing a project with nothing.
Logs — palbase logs
palbase logs shows the server's view of a deployment, newest last. Which store it reads is decided by what the checkout is linked to, from the address's shape rather than from the network — an unreachable server does not make a cloud project stop being one.
| Linked to | Where the lines come from |
|---|---|
| a cloud project | your account — the same store the Studio's log screen reads |
| a stack on this machine | its four Docker containers directly, in order: runtime, palsvc, envoy, postgres |
| a stack that is neither | refused — a project's own management surface has no log operation at all |
That last row is the one worth reading twice. A local stack is the case that works; a self-hosted stack somewhere else is the case that refuses, because there is no route to ask:
https://api.example.com does not run on this machine, so its logs are not here either.
A self-hosted stack keeps its logs in its own containers; `palbase start` brings a stack
up here if you want to watch one.
palbase logs # the last hour, up to 100 lines
palbase logs --level error,warn # errors and warnings only
palbase logs --since 2d -q timeout # free-text filter over the last two days
palbase logs --source envoy --follow # tail one local container live
| Flag | Default | Meaning |
|---|---|---|
-f, --follow | false | keep polling for new lines (Ctrl-C to stop) |
--level <list> | — | comma-separated: debug,info,warn,error |
--since <window> | 1 hour | look-back window — Go durations plus d, e.g. 15m, 2h, 2d |
-q, --query <text> | — | free-text line filter |
--source <name> | — | one source only; on a local stack, runtime, palsvc, envoy or postgres |
--limit caps lines per fetch at 1–500, default 100, and --json emits raw JSON — one array, or one object per line with --follow. There is no --container flag.
--follow polls every two seconds and prints what it has not printed before. The store answers a window, newest first, with no cursor, so a follower that was not running misses the older lines rather than catching up on them. That is the honest cost of the surface, not a display choice.
An empty result prints (no log lines — is the backend deployed and receiving traffic?).
Note: For the client's view — the request that never left, the 401 nobody surfaced, the body that came back empty — use
palbase debug tailandpalbase debug attach. See Debug.
A complete deploy loop
# 1. catch a broken tree locally, the way the deploy would
palbase build
# 2. see what the push would change — code and schema, no writes
palbase plan
# 3. ship it: schema applied and code activated in one request
palbase push
# 4. confirm the digest that is answering
palbase status
# 5. refresh the committed contract (push already did this on success)
palbase spec
Three facts to carry out of that loop. Schema travels with the push — there are no migration files, and db/public.ts is diffed against the live database; see Schema changes. palbase db plan, db apply and db query act only on the stack running on this machine, never on a cloud project; see Database. And the only wait in this whole rail is the 503 a brand-new project answers for about half a minute.
Related
- Linking a Checkout —
palbase link, the file it writes, and which environment a command acts on - Overview — installing and upgrading the CLI, and the full command surface
- Database —
db plan,db apply --approveanddb queryagainst the local stack - Schema changes — how
db/public.tsis diffed and applied - Running It Locally —
palbase start, the four containers, and the file that wins while they run - Stack Settings — the settings a push deliberately does not carry
- Secrets — the Environment vault, and
palbase run - Codegen —
palbase specand the platformlink/usecommands - Debug — the client-side console,
debug tailanddebug attach - Scheduled Jobs · Webhooks · Event Hooks — the three by-name surfaces this page ships
- Deploying — what the platform does with the artifact