Skip to content

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.

  • 9 steps
  • ~15 min
  • 1 vCPU · 1 GB RAM
  • Updated for v0.39.1

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 an A record pointing at the server.
  • SSH access as a user that can run sudo.
Server
sudo ufw allow OpenSSH
sudo ufw allow 80,443/tcp
sudo ufw enable
curl -fsSL https://get.vaultbase.dev | sudo sh -s -- --verify-sig

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

Keep Vaultbase on loopback and let Caddy serve the domain. Add to /etc/vaultbase/vaultbase.env:

/etc/vaultbase/vaultbase.env
VAULTBASE_HOST=127.0.0.1
VAULTBASE_TRUSTED_PROXIES=127.0.0.1/32,::1/128

Install Caddy from its own repository, then set /etc/caddy/Caddyfile to:

/etc/caddy/Caddyfile
api.example.com {
reverse_proxy 127.0.0.1:8091 {
header_up X-Forwarded-For {client_ip}
}
}
Server
sudo systemctl restart vaultbase
sudo systemctl reload caddy
curl -s https://api.example.com/_/health

Caddy gets and renews the certificate, and proxies the realtime WebSocket without extra settings. Recipe: a plain VPS explains each line.

Server
sudo -u vaultbase vaultbase setup-admin --email [email protected] --password '<a long password>'

Run it as the vaultbase user, like the service. Then sign in at https://api.example.com/_/.

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.

New collection again:

Name
notes
Type
Base
Schema
+text title, Required · +text body · +relation author → users

Vaultbase creates a real table, vb_notes, with an author column that holds a user’s id.

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 →

From your own machine:

Terminal
API=https://api.example.com
# Sign up, then sign in for a token
curl -s -X POST $API/api/v1/auth/users/register \
-H "Content-Type: application/json" \
-d '{"email":"[email protected]","password":"correct-horse-battery"}'
LOGIN=$(curl -s -X POST $API/api/v1/auth/users/login \
-H "Content-Type: application/json" \
-d '{"email":"[email protected]","password":"correct-horse-battery"}')
TOKEN=$(echo "$LOGIN" | jq -r .data.token)
ME=$(echo "$LOGIN" | jq -r .data.record.id)
# Create a note, then list Alice's notes
curl -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.

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:

notes.ts
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 deleted
const 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 subscription

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

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:

crontab -e (as root)
0 3 * * * /usr/local/bin/vaultbase backup --to s3://my-bucket/vaultbase/$(date -u +\%F).db --quiet

Also 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 →

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.