betavoidbase is in public beta. It runs, the API is PocketBase's and is not moving, and the version is still 0.x for everything around that.Help us get it to 1.0

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:

JSON
{
  "VOIDBASE_DEPLOY_CF_API_KEY": "...",
  "CLOUDFLARE_BUILDS_TOKEN": "..."
}

Connect it

voidbase sync

sync 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 toWhat happens
your production branchThe project is built and deployed. Migrations apply on the first request afterwards.
any other branchThe 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 push

That 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-run

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

YAML
.github/workflows/voidbase-update.yml
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 --check

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

Found something wrong on this page?

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.