Create a project

A backend you extend and keep: your own endpoints, handlers that run when records change, a schema in version control. It runs as one process on your machine and as one Worker on Cloudflare, from the same directory.

Start one

bun i -g @voidbase-cloud/voidbase

mkdir blog-api && cd blog-api
voidbase init
voidbase superuser upsert you@example.com your-password
voidbase serve

That is a working instance on http://127.0.0.1:8090, with the admin panel at /_/, the API reference at /api/docs and a sample endpoint answering GET /api/hello. init says what it wrote:

pb_hooks/, pb_migrations/ and pb_secrets/ ready (wrote pb_hooks/main.pb.js, .gitignore, .env, package.json, pb_secrets/main.ts)
.gitignore covers pb_data/, pb_secrets/secrets.json and .cloud/

next: voidbase serve   (the API on 8090, the admin panel at /_/), or bun install && bun run dev

Inside blog-api/:

  • pb_hooks/: endpoints, event handlers, scheduled work; main.pb.js answers GET /api/hello
  • pb_migrations/: the schema, as code
  • pb_secrets/: configuration, and who may read each key; main.ts is a commented declaration to fill in
  • pb_public/: static files served at /, if you add any
  • pb_data/: the database and the uploaded files, git-ignored
  • package.json: the dependency, and dev, start, deploy scripts
  • .env, .gitignore

Only pb_hooks/ and pb_migrations/ matter on day one. Each directory has its own page:

Or start from somebody's working project rather than empty directories. The files of the repository are downloaded as a tarball and unpacked into the directory, which has to be empty or absent; nothing is cloned and no .git is left behind, and the next steps are read from what the template contains.

voidbase templates                                  # what the marketplace lists: name, title, summary, repository
voidbase init blog-api --template voidbase-demo     # one of those, by name
voidbase init blog-api --template owner/name        # or any public GitHub repository, at --ref if not the default branch

The loop

  1. Design the schema in the panel. Collections, fields, and the API rules that decide who may read and write what. It is the fastest way to get the shape right, and it is immediately live.

  2. Write it down as a file in pb_migrations, so the same schema reaches production and the next person who clones this.

  3. Add behaviour in pb_hooks: an endpoint the API does not have, a handler that fires when a record is written, a nightly job.

  4. Run it while you work. On Bun, or on Cloudflare's own runtime without a token or an account, which is the one to use before a first deploy.

    voidbase serve --dev              # restarts when a hook or a migration changes
    voidbase serve --dev --tunnel     # and on the internet meanwhile, at https://<words>.trycloudflare.com
    voidbase serve --workers          # the same project on Cloudflare's runtime, locally, with D1, R2, queue and hub in Miniflare
  5. Type the client from it. The instance describes its own API at /api/openapi.json, and types turns that into a file your app imports, so a renamed field is a compile error rather than an empty value. The SDK page shows the other end.

    voidbase types --url http://127.0.0.1:8090 --email you@example.com --password your-password
    # writes src/voidbase.ts: an interface per collection, a Collections map, a TypedPocketBase type

Put it on Cloudflare

One API token, which voidbase token prints the link for, declared as a local() key in pb_secrets and valued in its secrets.json. Then, from the project:

voidbase deploy

It creates the Worker, its database, its file storage, its queue and its realtime object on the first run, stores the declared secrets on it, applies any pending migrations on the first request, and prints the address. Deploy again whenever anything changes; the data is untouched, and a secret the Worker already holds is left alone.

voidbase deploy --domain api.example.com          # the domains plugin attaches it and turns workers.dev off
voidbase deploy --database durable                 # the data in a SQLite-backed Durable Object instead of D1
voidbase deploy --preview feature/login            # a second Worker for the branch, seeded from production
voidbase deploy --dry-run                          # the whole plan, nothing touched

Or let a push do it

sync is that deploy plus the wiring, so every later push to the repository deploys by itself and every change to the instance is a commit somebody can read and revert.

voidbase sync

It needs a second token and one dashboard step the first run points at. From then on the whole loop above is: edit, commit, push. Deploy on every push is the guide, and What git tracks is the list of what belongs in the repository and what must not.

Listing and deleting instances works the same from a project as without one: voidbase instances and voidbase destroy, and voidbase migrate moves the data between this instance and any other.

Updating

voidbase update

Run it in the project. The dependency in package.json is bumped to the newest published version and installed, keeping the caret or the exact pin you already had, so a project that deliberately pins stays pinned. The change is a diff in package.json and your lockfile: review it and commit it like any other dependency bump. Before changing anything it names the installed plugins whose declared range excludes the new version.

voidbase update --check changes nothing. It prints the version you are on and the newest one, and exits 1 when you are behind and 2 when it could not find out, which is what a pipeline reads. --to 1.0.0 goes to a particular version rather than the newest, and --dry-run prints the command it would run without running it.

You do not have to remember to check. Any voidbase command mentions a new release once a day, from an answer cached under ~/.voidbase. It never speaks in CI or when the output is not a terminal, and VOIDBASE_NO_UPDATE_CHECK=1 silences it everywhere. Every release is written up in the release notes.

When this stops being enough

A project is a backend. When the site is yours too and you would rather write pages, typed routes and a database schema in one application than keep a frontend and a backend in step, the voidbase stack is the next page.

Starting from a template

Everything above starts from nothing, which is the right way to learn what the pieces are and the slow way to get something running. A template is somebody's working a voidbase project that you copy into your own account and change.

From GitHub

Every template in the marketplace is a public repository with GitHub's Use this template switched on. Open it, press the button, and you have your own copy with its whole history left behind. That is the entire mechanism and it is GitHub's, not ours.

voidbase-demo is the one to copy for this shape: PocketBase's layout, a seeded schema, hooks, and nothing else.

From the CLI

Cloning through a browser is fine once and tedious after that, so init takes a template directly. Give it the name of anything the marketplace lists, which it looks up for you, or owner/name for any public repository, with --ref for a branch or a tag.

voidbase templates                                              # what the marketplace lists
voidbase init blog-api --template voidbase-site                 # by listed name
voidbase init blog-api --template voidbase-cloud/voidbase-demo  # any public repository, at its default branch
voidbase init blog-api --template acme/starter --ref v2         # at a branch or tag
voidbase init blog-api --template starter --marketplace https://templates.acme.internal   # another listing

It unpacks the repository at that ref and nothing else: no .git, no history, no remote pointing at somebody else's repository, and it prints the next steps it finds in what it unpacked. It refuses a directory that already has anything in it, because writing a whole project over your work is not something you can undo. --marketplace points it at another listing. The cloud page offers the same templates on its create screen once GitHub is connected.

Starting from nothing is still one command:

voidbase init blog-api && cd blog-api

Publishing your own is a GitHub issue, and what makes a template worth listing is its own page.

Plugins

Plugins exist and the marketplace lists them. voidbase plugins add <name> installs one: its files at the commit the marketplace approved land in pb_plugins/<name>, and voidbase.lock pins the marketplace, the version, that commit and a hash of the files. The instance loads it beside the eight plugins voidbase ships, and a superuser reads where each came from at /api/plugins. Removing a shipped plugin turns it off; installing one with its name takes its place. The plugins page has what a plugin is, the shipped ones and their knobs, and the installer.

Installed plugins land in pb_plugins/ beside pb_hooks and are committed, so what is running is what the repository says is running, and a deploy is what puts a new one live. On Workers the verification runs at build time, so a mismatch fails the build rather than the instance. A project deployed from a repository can also change its plugins from inside: with VOIDBASE_PROJECT_REPO and VOIDBASE_GH_TOKEN on the Worker, the instance's installer makes one commit, which the repository's own build deploys.

voidbase plugins add echo         # writes pb_plugins/echo and the lock entry
git add pb_plugins voidbase.lock  # the install is a commit, like any dependency
voidbase deploy                   # and the deploy is what puts it live (or push, if a pipeline deploys)

Any marketplace serving the registry protocol works, ours being the default: --marketplace <url> on an install, or VOIDBASE_PLUGIN_MARKETPLACES. To publish one, a repository with a manifest.json is enough: submit it and the marketplace audits it and records the 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.