Start a stack app

One application: pages, typed endpoints, background jobs and a database, which builds into a voidbase instance and deploys as a single Worker serving the site, the API and the admin panel from one address.

It is a Void app with an adapter added. The project stays a plain Void app the whole way through: the adapter generates the voidbase instance from it rather than asking you to keep one.

From an empty directory

mkdir my-app && cd my-app
bun add void && bunx void init          # scaffolds a Void app: pages/, routes/, db/, vite.config.ts
bun add @voidbase-cloud/voidbase

Then add the adapter to the Vite config, which is the only wiring there is. A framework plugin such as voidReact() sits beside it when void init picked one:

// vite.config.ts
import { defineConfig } from "vite";
import { voidPlugin } from "void";
import { voidbaseAdapter } from "@voidbase-cloud/voidbase/adapter/plugin";

export default defineConfig({ plugins: [voidPlugin(), voidbaseAdapter()] });

And ignore the two things that should never be committed: the generated instance, and the local values of your configuration.

Git
.voidbase/
vb_secrets/secrets.json

What the project looks like

Your application, which is Void's:

  • pages/: the site, prerendered when void.json sets "output": "static"
  • routes/: typed API endpoints
  • middleware/: what runs on every request, PocketBase's endpoints and the panel included
  • crons/: scheduled work, each also a cron trigger of the Worker
  • queues/: background jobs, carried by the instance's own jobs queue
  • workflows/: durable, multi-step work, bound as WORKFLOW_<NAME>
  • db/: the app's own tables, in Drizzle
  • src/: library code the rest imports

What voidbase reads:

  • vb_hooks/: handlers that run around record writes
  • vb_migrations/: collections, for the schema the panel manages
  • vb_secrets/: configuration, for the server, the build and the browser

Written by the build:

  • .voidbase/: the instance this becomes, and git-ignored

Everything above the gap is Void's and means what Void means by it. The three directories below it are the ones the adapter adds, each named for the voidbase thing it is, each optional, and each with its own page:

Build and run it

bun run build                                  # or: bunx --bun vite build, or: bunx voidbase adapt
bun .voidbase/main.ts --http 127.0.0.1:8090    # site at /, API at /api, panel at /_/

The build writes a complete voidbase project into .voidbase/: your pages as its static files, your routes, middleware, hooks, crons and queues compiled into its hook bundle, your collections as its migrations, your declaration as its pb_secrets. It is git-ignored, because it is output, and it is an ordinary instance in every other respect. Build with Bun: voidbase ships TypeScript sources and Vite loads its config through the runtime.

A route that reads or writes collections imports pb, authOf and requireAuth from @voidbase-cloud/voidbase/adapter; a route on one of voidbase's own paths (/api/collections, /api/files, /api/realtime and the rest) never runs, and the build warns and names it.

Deploy it

cd .voidbase && voidbase deploy      # the generated instance, like any project
voidbase sync                        # or from the root: builds, deploys, connects the repository

Everything on the project page applies from here: the same token and the same deploy. sync knows this layout by its vb_ directories and deploys from what the build generated. To have a push do it instead, so the site and its backend ship together on every commit, Deploy on every push is two commands and one dashboard step.

An installable app

The pwa option writes what an installable app needs from what the app already declares in void.json: the manifest, the icons resized from one file, and a service worker that precaches the shell and the hashed assets under a version hashed from those files, never touching /api or the panel. @voidbase-cloud/sdk/pwa registers it and shows the update prompt.

voidbaseAdapter({ pwa: { icon: "icon.svg" } })   // manifest.webmanifest, the icon set and sw.js into pb_public

Updating

voidbase update

Run it in the project, the same as anywhere else: the @voidbase-cloud/voidbase dependency is bumped and installed, and the caret or pin you had is kept. Void and the rest of the toolchain are separate dependencies with their own versions, and this leaves them alone.

A stack app is one Worker, so the backend and the site go live together. Build it first and look at it locally before you deploy, because a voidbase upgrade and your own application ship in the same artifact.

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.

Two things that are different

Two databases, on purpose. Void's db/ is your application's own tables, in Drizzle, typed end to end, for the data your code owns. Collections are for the data the admin panel and the SDK own, with API rules and a schema someone can change without a deploy. Both live in the same D1, most apps want both, and vb_migrations is where the second kind is written down.

Routes and hooks are not the same thing. A route in routes/ is an endpoint you call. A hook in vb_hooks runs because a record changed, whoever changed it, including someone clicking in the admin panel. Reach for the first when you are writing an API, the second when a rule has to hold no matter who is writing.

This site is built this way: the page you are reading, its API and the admin panel are one Worker.

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 stack app 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-site and voidbase-marketplace are both stack apps serving real traffic, which makes them honest starting points rather than demos written to look good.

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:

bun add void && bunx void init && bun add @voidbase-cloud/voidbase

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.

pb_plugins/ and voidbase.lock sit at the project root beside vb_hooks, and the build carries both into the generated app, verifying every plugin against the lockfile as it does; a mismatch fails the build rather than the instance.

voidbase plugins add echo         # writes pb_plugins/echo and the lock entry, at the project root
git add pb_plugins voidbase.lock  # the build carries both into the generated app
bun run build && cd .voidbase && voidbase deploy   # 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.