Tutorial: a backend on a $5 VPS
One small Linux server running Vaultbase over HTTPS, with sign-up, private per-user data, live updates and backups, called from curl and the TypeScript SDK.
By the end you have a backend for a notes app on a server you control: users sign up with email and password, each user sees only their own notes, an app gets new notes live, and the database is backed up off the server. It runs on the cheapest tier most hosts sell.
You need:
- A Linux VPS (Ubuntu 24.04 here) with a public IPv4 address. 1 vCPU and 1 GB of RAM is enough to start.
- A domain, here
api.example.com, with anArecord pointing at the server. - SSH access as a user that can run
sudo.
Install Vaultbase
Section titled “Install Vaultbase”sudo ufw allow OpenSSHsudo ufw allow 80,443/tcpsudo ufw enable
curl -fsSL https://get.vaultbase.dev | sudo sh -s -- --verify-sigThe installer puts the binary at /usr/local/bin/vaultbase, creates a
vaultbase system user, writes /etc/vaultbase/vaultbase.env with a fresh JWT
secret, and starts a systemd service. Data lives in /var/lib/vaultbase, on
SQLite. --verify-sig checks the release’s signature and needs cosign
installed; leave the flag off to check only when cosign is there.
To use Postgres instead, set VAULTBASE_DATABASE_URL in the env file now,
before the first admin exists: the database is chosen once per install. See
Databases.
Put HTTPS in front
Section titled “Put HTTPS in front”Keep Vaultbase on loopback and let Caddy serve the domain. Add to
/etc/vaultbase/vaultbase.env:
VAULTBASE_HOST=127.0.0.1VAULTBASE_TRUSTED_PROXIES=127.0.0.1/32,::1/128Install Caddy from its own repository,
then set /etc/caddy/Caddyfile to:
api.example.com { reverse_proxy 127.0.0.1:8091 { header_up X-Forwarded-For {client_ip} }}sudo systemctl restart vaultbasesudo systemctl reload caddycurl -s https://api.example.com/_/healthCaddy gets and renews the certificate, and proxies the realtime WebSocket without extra settings. Recipe: a plain VPS explains each line.
Create the admin
Section titled “Create the admin”Run it as the vaultbase user, like the service. Then sign in at
https://api.example.com/_/.
Turn on sign-up
Section titled “Turn on sign-up”A fresh install has no user accounts yet. In the admin, open Collections → New collection:
- Name
users- Type
- Auth
Create it, then open its API rules tab and set:
- Create
- Public: anyone may sign up
- View
- Custom:
@request.auth.id = id(each user sees their own account) - List / Update / Delete
- Admin only (leave as is)
New auth collections refuse sign-up until Create is Public. The View rule matters for the next step: a note can only point at a user the caller is allowed to see, so without it users couldn’t name themselves as a note’s author. Email, password and the password hash are built in.
Create the notes collection
Section titled “Create the notes collection”New collection again:
- Name
notes- Type
- Base
- Schema
+texttitle, Required ·+textbody ·+relationauthor →users
Vaultbase creates a real table, vb_notes, with an author column that holds
a user’s id.
Make each user’s notes private
Section titled “Make each user’s notes private”Open notes → API rules, choose Custom for each operation, and enter:
- List
@request.auth.id = author- View
@request.auth.id = author- Create
@request.auth.id != "" && @request.body.author = @request.auth.id- Update
@request.auth.id = author- Delete
@request.auth.id = author
A signed-in user can only create notes with themselves as the author, and only
ever sees or changes their own. The list rule becomes a WHERE author = ? in
SQL, so other users’ rows never leave the database. The update rule is checked
against the note as it would be after the change too, so nobody can hand a note
to someone else. More on rules →
Try it with curl
Section titled “Try it with curl”From your own machine:
API=https://api.example.com
# Sign up, then sign in for a tokencurl -s -X POST $API/api/v1/auth/users/register \ -H "Content-Type: application/json" \
LOGIN=$(curl -s -X POST $API/api/v1/auth/users/login \ -H "Content-Type: application/json" \TOKEN=$(echo "$LOGIN" | jq -r .data.token)ME=$(echo "$LOGIN" | jq -r .data.record.id)
# Create a note, then list Alice's notescurl -s -X POST $API/api/v1/notes \ -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d "{\"title\":\"First note\",\"body\":\"Hello from curl\",\"author\":\"$ME\"}"
curl -s "$API/api/v1/notes?sort=-created" -H "Authorization: Bearer $TOKEN"Sign up a second user and list with their token: the list is empty. Without a
token, GET /api/v1/notes returns no records at all.
Call it from an app
Section titled “Call it from an app”In the admin, open Settings → CORS and add your app’s origin (for example
http://localhost:5173), so browsers may call the API and connect to realtime.
Then, with the TypeScript SDK:
import { Vaultbase } from "@vaultbase/sdk";
const vb = new Vaultbase({ baseUrl: "https://api.example.com" });
const session = await vb.auth.users.login({ password: "correct-horse-battery",});if ("mfa_required" in session) throw new Error("this account has MFA on");const me = session.record.id;
// Live: every note Alice may see, as it's created, edited or deletedconst off = vb.subscribe("notes", "*", (ev) => console.log(ev.type, ev.record ?? ev.id));
await vb.collection("notes").create({ title: "From the SDK", body: "Hi", author: me });const { data } = await vb.collection("notes").list({ sort: "-created" });console.log(data.map((n) => n.title));
// off() closes the subscriptionRealtime events go through the same rules as the API: Alice’s subscription only ever receives her own notes. Flutter and Dart apps use the Dart SDK the same way.
Back it up
Section titled “Back it up”In the admin, Settings → Backup & restore schedules backups. Point them at
S3-compatible storage (S3, R2, B2), so losing the server doesn’t lose the
backups. Or from cron, with the bucket’s credentials in the standard
AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY variables:
0 3 * * * /usr/local/bin/vaultbase backup --to s3://my-bucket/vaultbase/$(date -u +\%F).db --quietAlso keep a copy of /etc/vaultbase/vaultbase.env and
/var/lib/vaultbase/.encryption-key somewhere safe: a backup restored without
the encryption key can’t read encrypted values. Backups →
How far one small box goes
Section titled “How far one small box goes”A shared 4 vCPU server sustained about 1,200 realistic reads and 1,800 writes
a second in our measurements, with p95 under 400 ms over the internet; a 1 vCPU
box does less. The owner rules above stay fast as notes grow, because every
relation field, author included, gets an index of its own.
Performance shows the method and the numbers. Vaultbase
runs on one host by design: it doesn’t do multi-region or sharding.
To update later: sudo vaultbase update --yes && sudo systemctl restart vaultbase,
after reading the release notes.