PocketBase Tutorial: Build a Real-Time Database and Backend with a Single Executable File

Database tutorial - IT technology blog
Database tutorial - IT technology blog

Quick Start: Get PocketBase Running in 5 Minutes

PocketBase ships as a single executable. Download it, run it, and you have a working backend: database, REST API, authentication, file storage, and an admin dashboard — all included. No Docker, no Node.js, no Postgres to install.

Here’s how to get it running on Linux:

# Download the latest release (check https://pocketbase.io/docs for current version)
wget https://github.com/pocketbase/pocketbase/releases/download/v0.22.0/pocketbase_0.22.0_linux_amd64.zip

# Unzip it
unzip pocketbase_0.22.0_linux_amd64.zip

# Make it executable and run
chmod +x pocketbase
./pocketbase serve

You’ll see output like this:

2024/01/15 10:00:00 Server started at http://127.0.0.1:8090
  - REST API: http://127.0.0.1:8090/api/
  - Admin UI: http://127.0.0.1:8090/_/

Open http://127.0.0.1:8090/_/ in your browser. Create an admin account when prompted, and you’re inside the dashboard.

macOS works the same way — just grab the darwin build. On Windows, download the Windows zip and run pocketbase.exe serve from Command Prompt.

Create Your First Collection

Think of a collection as a database table. In the admin dashboard, click New collection, name it posts, and add these fields:

  • title — Text, required
  • content — Editor (rich text)
  • published — Bool
  • author — Relation (linking to the users collection)

Save it. PocketBase automatically generates a REST API for your new collection. You now have a working /api/collections/posts/records endpoint — no code written.

Deep Dive: Working with the API

The REST API follows conventions you already know. Every collection gets the same set of endpoints, and you can filter, sort, and paginate using URL parameters.

Fetching Records

# Get all posts
curl http://127.0.0.1:8090/api/collections/posts/records

# Filter and sort
curl "http://127.0.0.1:8090/api/collections/posts/records?filter=(published=true)&sort=-created&perPage=10"

# Get a single record by ID
curl http://127.0.0.1:8090/api/collections/posts/records/RECORD_ID

Creating and Updating Records

# Create a new post (requires auth token if collection rules require it)
curl -X POST http://127.0.0.1:8090/api/collections/posts/records \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -d '{"title": "My First Post", "content": "Hello World", "published": true}'

# Update a record
curl -X PATCH http://127.0.0.1:8090/api/collections/posts/records/RECORD_ID \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -d '{"published": false}'

Authentication

User management is built in. The users collection exists from the start — you don’t create it. Registration and login look like this:

# Register a new user
curl -X POST http://127.0.0.1:8090/api/collections/users/records \
  -H "Content-Type: application/json" \
  -d '{"email": "[email protected]", "password": "password123", "passwordConfirm": "password123"}'

# Login and get a token
curl -X POST http://127.0.0.1:8090/api/collections/users/auth-with-password \
  -H "Content-Type: application/json" \
  -d '{"identity": "[email protected]", "password": "password123"}'

The login response includes a token field. Pass that as Authorization: Bearer TOKEN in subsequent requests.

Using the JavaScript SDK

For frontend work, the official JS SDK is cleaner than raw fetch calls:

npm install pocketbase
import PocketBase from 'pocketbase';

const pb = new PocketBase('http://127.0.0.1:8090');

// Login
const authData = await pb.collection('users').authWithPassword('[email protected]', 'password123');

// Fetch posts
const posts = await pb.collection('posts').getList(1, 20, {
  filter: 'published = true',
  sort: '-created',
});

// Create a post
const newPost = await pb.collection('posts').create({
  title: 'Hello from SDK',
  content: 'This is my post',
  published: true,
});

Advanced Usage: Real-Time Subscriptions and File Uploads

Real-Time Updates with Subscribe

Live updates are where PocketBase earns its keep. Subscribe to a collection and changes arrive at your client the moment they happen — no polling, no manual refresh logic:

// Subscribe to all changes in the 'posts' collection
await pb.collection('posts').subscribe('*', function(e) {
  console.log('Action:', e.action); // 'create', 'update', or 'delete'
  console.log('Record:', e.record);
});

// Subscribe to a single record
await pb.collection('posts').subscribe('RECORD_ID', function(e) {
  console.log('This specific post changed:', e.record);
});

// Unsubscribe when done (e.g., when component unmounts)
pb.collection('posts').unsubscribe('*');

PocketBase uses Server-Sent Events (SSE) under the hood, not WebSockets. That means it works through most firewalls and proxies without any special configuration.

File Uploads

Add a File field to your collection in the admin dashboard, then upload files through the API:

const formData = new FormData();
formData.append('title', 'Post with attachment');
formData.append('attachment', fileInput.files[0]);

const record = await pb.collection('posts').create(formData);

// Get the file URL
const fileUrl = pb.files.getUrl(record, record.attachment);

Collection Rules and Access Control

Each collection has API Rules for read, create, update, and delete. They use PocketBase’s filter syntax — the same syntax you use when querying records:

  • Leave empty → everyone can access (good for public read)
  • @request.auth.id != "" → only authenticated users
  • author = @request.auth.id → only the record’s author
  • @request.auth.id = @collection.admins.id → only admins

A typical blog setup: List/View rules stay empty so posts are public. Create requires auth. Update/Delete require author = @request.auth.id.

Extending with Go Hooks

Need custom server-side logic? Embed PocketBase in a Go app and attach hooks:

go mod init myapp
go get github.com/pocketbase/pocketbase
package main

import (
    "log"
    "github.com/pocketbase/pocketbase"
    "github.com/pocketbase/pocketbase/core"
)

func main() {
    app := pocketbase.New()

    // Send email after a post is created
    app.OnRecordAfterCreateRequest("posts").Add(func(e *core.RecordCreateEvent) error {
        log.Println("New post created:", e.Record.GetString("title"))
        // send email, trigger webhook, etc.
        return nil
    })

    if err := app.Start(); err != nil {
        log.Fatal(err)
    }
}

Practical Tips for Real-World Use

Running PocketBase in Production

On a VPS, wrap PocketBase in a systemd service so it starts on boot and restarts automatically after a crash:

# /etc/systemd/system/pocketbase.service
[Unit]
Description=PocketBase service
After=network.target

[Service]
Type=simple
User=www-data
WorkingDirectory=/opt/pocketbase
ExecStart=/opt/pocketbase/pocketbase serve --http=0.0.0.0:8090
Restart=always

[Install]
WantedBy=multi-user.target
sudo systemctl enable pocketbase
sudo systemctl start pocketbase

Put Nginx in front to handle HTTPS. One non-obvious detail: disable proxy buffering, or SSE connections will stall silently:

location / {
    proxy_pass http://127.0.0.1:8090;
    proxy_set_header Host $host;
    # Important for SSE (real-time subscriptions)
    proxy_buffering off;
    proxy_read_timeout 3600;
}

Backup Strategy

All your data — records, users, settings — lives in a single SQLite file at pb_data/data.db. Backing up means copying one file:

# Simple daily backup
0 2 * * * cp /opt/pocketbase/pb_data/data.db /backups/pocketbase-$(date +%Y%m%d).db

You can also trigger backups via the admin API without stopping the server.

Data Import Workflow

When migrating existing data into PocketBase, I often need to convert CSV exports from old systems into JSON before importing. My go-to for this is toolcraft.app/en/tools/data/csv-to-json — it runs entirely in the browser so your data never leaves your machine, which matters when the CSV contains user information or sensitive records. Once I have the JSON, a quick script handles the bulk insert through PocketBase’s API.

When to Use PocketBase (and When Not To)

PocketBase works well when:

  • You’re building a side project or MVP and want a backend running in an hour
  • A single server is enough — internal tools, admin dashboards, small mobile apps
  • You need offline-sync in a mobile app without the complexity of managing it yourself
  • You’re prototyping and don’t want to commit to a full stack yet

Where it falls short:

  • Apps that need horizontal scaling across multiple servers — SQLite doesn’t support distributed writes
  • High-write workloads: SQLite handles concurrent reads well but starts struggling above a few hundred writes per second under contention
  • Teams that need complex SQL queries, stored procedures, or tight database-level constraints

For early-stage projects, a ~30MB binary that covers your database, API, auth, and file storage replaces four separate services you’d otherwise have to stand up and maintain. That’s a real trade worth making.

Share: