Deploy on every push
Connect the repository once and a push is the deploy: Cloudflare builds the project, applies the migrations and replaces the Worker. Every change to the instance is then a commit somebody can read, revert and blame, which is the whole reason to do it this way.
This works the same for a project and a stack app. Where they differ is noted as it comes up.
Before you start
- The project is in a GitHub repository, and you have pushed it at least once.
- You have deployed it by hand once, with
voidbase deploy, so the instance exists.
Neither is strictly required, but doing the first deploy from your own machine means the first thing the pipeline ever does is an update rather than a creation, which is much easier to read when something is wrong.
Two tokens
One deploys the instance, one connects the repository. Both are yours, both are declared as local() keys, which is the tier that means read here and never deployed.
// pb_secrets/main.ts (vb_secrets/main.ts in a stack app)
import { defineSecrets, local, string } from "@voidbase-cloud/voidbase/secrets";
export default defineSecrets({
VOIDBASE_DEPLOY_CF_API_KEY: local(string(), "deploys the instance"),
CLOUDFLARE_BUILDS_TOKEN: local(string(), "connects the repository to Workers Builds"),
VOIDBASE_DEPLOY_NAME: local(string().default("blog-api"), "the Worker this repository deploys to"),
});voidbase token prints the link that creates the first. The second is a user API token from your Cloudflare profile with Workers Builds Configuration: Edit and Workers Scripts: Edit. An account token will not do: the Builds API refuses it.
Their values go in the git-ignored file beside the declaration, and stay on your machine:
{
"VOIDBASE_DEPLOY_CF_API_KEY": "...",
"CLOUDFLARE_BUILDS_TOKEN": "..."
}Connect it
voidbase syncsync deploys the instance and then wires the repository to it. There is one step an API cannot do for you, installing Cloudflare's GitHub App on the repository, so the first run stops and points at it:
ci: the GitHub App is not installed for you/blog-api. One dashboard step:
open https://dash.cloudflare.com/?to=/:account/workers/services/view/blog-api/production/builds
Under Builds, connect you/blog-api: that installs the "Cloudflare Workers and Pages"
GitHub App for it and creates the build token. Then run voidbase sync again.Do that, run the same command again, and the triggers are in place:
ci: you/blog-api -> Worker blog-api (account Example Ltd)
created trigger "blog-api (main)": a push to main runs `true`, then `bun run deploy`
created trigger "blog-api (branches)": every other branch runs `true`, then `bun run version`; nothing live is touched
build environment: BUN_VERSION, VOIDBASE_DEPLOY_CF_API_KEY (secret), MAX_UPLOAD_MB
the pipeline: push to main and watch it at https://dash.cloudflare.com/...What it built
Two triggers, because the two cases want different things:
| On a push to | What happens |
|---|---|
| your production branch | The project is built and deployed. Migrations apply on the first request afterwards. |
| any other branch | The project is built, and the deploy step reads back the configuration that branch would deploy with instead of deploying it. A branch can never replace what is live. |
The commands are the project's own verbs, bun run deploy and bun run version, which is why a scaffolded project has them in its package.json. Nothing about the pipeline lives in the dashboard: what those words do is decided by the repository, so it changes in a commit like everything else.
Then just push
git add -A
git commit -m "posts: add a featured flag"
git pushThat is the loop from here. Watch it at the link sync printed, or in the Cloudflare dashboard under the Worker's Builds tab.
A stack app is the same
Run voidbase sync at the project root. The build runs your Vite build, which generates the instance into .voidbase/, and the deploy publishes from there. The generated directory stays git-ignored: it is output, rebuilt on every build, and committing it would only create conflicts.
More than one instance in one repository
A build may deploy only the Worker its trigger belongs to, so each instance needs its own trigger. Run voidbase sync inside each project directory and each gets one, with commands that step into that directory. Declare VOIDBASE_DEPLOY_NAME in each, which makes a deploy refuse to run against a Worker the project does not name, and turns the worst mistake available here into an error message.
Checking without changing anything
voidbase sync --dry-runPrints the plan and stops: which repository, which Worker, which branch, and the exact commands it would write. --no-ci deploys without touching the pipeline, and --repo and --branch override what git says.
An existing trigger keeps the watch paths you set in the dashboard, so tuning which paths trigger a build is not undone the next time somebody runs sync.
Updating voidbase, tracked like everything else
A build installs what package.json says, so that file is what decides which voidbase a deploy carries. That is the useful property: an upgrade is a commit, it shows up in the diff, it deploys through the same pipeline as your own code, and reverting it is reverting a commit. Run voidbase update on your machine, look at the change to package.json and your lockfile, and push it.
A pinned version never changes underneath you. A caret range picks up a new version on the next install, which is convenient until a build behaves differently from the one before it for a reason that is not in any commit. Pin it if that matters to you, and let the update commit be the only thing that moves it.
To be told when there is something to update, add a step that asks. --check changes nothing and exits 1 when you are behind, so a scheduled job fails and notifies you exactly when a release is out.
name: voidbase update
on:
schedule: [{ cron: "0 9 * * 1" }] # Monday morning
workflow_dispatch:
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install --frozen-lockfile
- run: ./node_modules/.bin/voidbase update --checkNothing in the check needs a token or an account: it reads your lockfile and the public registry. It exits 2 if it could not reach the registry, so a network problem is distinguishable from being out of date and you can decide which of the two should fail a build.
Next
The one thing that must not be in the repository is the values of your secrets. What git tracks is the list, and how those values reach production without going through a commit.
Fix it yourself. The link below opens this file in GitHub's editor and forks the repository for you if you need one, and your change becomes a pull request without leaving the browser.