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, required
  • done — boolean
  • user — relation to users (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.