Install
PB_VER=0.23.5
curl -L -o /tmp/pocketbase.zip \
"https://github.com/pocketbase/pocketbase/releases/download/v${PB_VER}/pocketbase_${PB_VER}_linux_amd64.zip"
unzip /tmp/pocketbase.zip -d /tmp
sudo mv /tmp/pocketbase /usr/local/bin/
mkdir ~/pb-data
cd ~/pb-data
pocketbase serve --http=127.0.0.1:8090
First run creates the SQLite databases at ./pb_data/, prints the admin-setup URL (with a token), and starts listening. Open the URL, set an admin email/password, and the UI is ready.
What the admin UI gives you
Five tabs:
- Collections — tables. Per collection, define fields (text, number, email, file, JSON, relation, etc.), validation rules, indexes, and per-action access rules.
- Records — the data. Browse / edit / filter / export per collection.
- Logs — request log, query log, error log.
- Settings — SMTP, OAuth providers, backups, hooks, deployment.
- Auth — manage users + per-user records.
Define a collection
Click Collections → New collection → type "tasks". Add fields:
title— text, requireddone— booleanuser— relation tousers(single)created— auto-date
API rules (under the "API Rules" tab of the collection):
# List rule: only show records owned by the requesting user
@request.auth.id != "" && user = @request.auth.id
# View / Update / Delete rules: same
@request.auth.id != "" && user = @request.auth.id
# Create rule: the user being set in the record must match the requester
@request.auth.id != "" && @request.body.user = @request.auth.id
Save. The REST endpoints /api/collections/tasks/records are now live and per-user-scoped — without writing a line of backend code.
Use the API from a client
// JavaScript via the official SDK
import PocketBase from "pocketbase";
const pb = new PocketBase("https://pb.example.com");
// Auth
await pb.collection("users").authWithPassword("amir@example.com", "secret");
// CRUD
const list = await pb.collection("tasks").getFullList({ sort: "-created" });
await pb.collection("tasks").create({ title: "Buy milk", user: pb.authStore.model.id });
// Realtime — subscribe to all changes in the collection
pb.collection("tasks").subscribe("*", function (e) {
console.log(e.action, e.record);
});
// Realtime — only changes to one specific record
pb.collection("tasks").subscribe(recordId, callback);
The realtime channel uses Server-Sent Events; works through any HTTP-2 reverse proxy without WebSocket configuration.
OAuth providers
Settings → Auth providers → flip the toggles for Google, Apple, GitHub, GitLab, Discord, Microsoft, Twitter, etc., paste in client-id / client-secret from each provider. Frontends use:
await pb.collection("users").authWithOAuth2({ provider: "github" });
PocketBase handles the redirect, exchanges the code, creates / merges the user, returns a session token.
File storage
Any field of type "file" auto-handles uploads. By default files land in ./pb_data/storage/<collection-id>/<record-id>/<file>. For production, configure S3 / MinIO (Settings → Files):
Endpoint: https://s3.lab.example.com
Bucket: pocketbase
Region: us-east-1
Access key: ...
Secret key: ...
ForcePathStyle: true
Uploaded files now stream through PocketBase to your MinIO instance (see that tutorial) and back; clients access them via PocketBase URLs with thumbnails on demand for images.
Hooks: extending the server
PocketBase v0.18+ supports JavaScript / TypeScript hooks via the embedded Goja runtime. Drop a file under pb_hooks/:
// pb_hooks/main.pb.js
onRecordCreateRequest((e) => {
e.record.set("title", e.record.get("title").trim());
if (e.record.get("title").length < 3) {
throw new BadRequestError("title too short");
}
}, "tasks");
routerAdd("GET", "/hello/:name", (c) => {
return c.json(200, { hello: c.pathParam("name") });
});
Restart PocketBase. The hooks run server-side for the matching events; the custom route is live at /hello/world.
For heavier extensions or Go-native performance, use PocketBase as a Go library: import "github.com/pocketbase/pocketbase" and add your own handlers. The same binary still ships everything PocketBase provides; you add functions on top.
Backups
PocketBase has built-in backups: Settings → Backups → Create new. The output is a zip containing the SQLite databases + the storage directory. Restore by uploading and clicking restore. Automate with the API or a cron-triggered admin-API call. For offsite, drop the resulting file into a restic job (see that tutorial).
SQLite + Litestream (see that tutorial) is the other path: continuous replication of the database to S3, point-in-time recovery, sub-second RPO.
Production deployment
# systemd unit
[Unit]
Description=PocketBase
After=network-online.target
[Service]
User=pb
Group=pb
WorkingDirectory=/var/lib/pocketbase
ExecStart=/usr/local/bin/pocketbase serve --http=127.0.0.1:8090
Restart=always
[Install]
WantedBy=multi-user.target
# Caddy
api.example.com {
reverse_proxy 127.0.0.1:8090
}
Set --encryptionEnv=PB_ENCRYPT_KEY and define the env var to encrypt sensitive fields at rest.
When PocketBase is the wrong tool
- SQLite is single-writer. For workloads with high concurrent write throughput (>500 writes/sec sustained), look at Postgres-backed alternatives.
- For multi-region active-active, PocketBase isn't there — one node, one database. Pair with Litestream for offsite replication, not multi-master.
- For complex relational queries with many joins / window functions, the JSON-API filter language gets restrictive. Hooks let you drop to raw SQL when needed, but at some point Postgres + PostgREST (see that tutorial) is the more honest choice.
For the sweet spot — one-developer SaaS, mobile-app backend, internal admin tool, small-team production app — PocketBase is the smallest tool that still gives you the right abstractions.