Development beginner ⏱ 30 minutes

PocketBase: Backend in a Single File

Lightweight backend with admin UI. SQLite database, auth, and file storage.

By Homelab Team • 2025-01-15
#pocketbase#backend#sqlite#auth#docker

Introduction

PocketBase is a revolutionary open-source backend that packages everything you need into a single executable file: database, authentication, file storage, real-time subscriptions, and an admin dashboard. No complex setup, no dependency management, no microservices architecture - just one file that runs anywhere.

What makes PocketBase unique?

  • Single binary - Download one file and run it, no installation needed
  • SQLite database - Fast, reliable, embedded database with full SQL support
  • Built-in auth - Email/password, OAuth2, JWT tokens out of the box
  • File storage - Upload and serve files with automatic image resizing
  • Real-time subscriptions - WebSocket support for live data updates
  • Admin UI - Beautiful web-based dashboard for managing everything
  • REST & Realtime APIs - Auto-generated APIs from your schema
  • Go-powered - Written in Go for exceptional performance and low resource usage

Perfect for personal projects, internal tools, MVPs, and homelab applications where you want a powerful backend without the complexity of traditional setups.

Prerequisites

Before starting, you should have:

  • Basic command line knowledge - Ability to run commands and edit files
  • System resources - Minimal requirements: 256MB RAM, 100MB disk space
  • Optional: Docker - For containerized deployments
  • Optional: Domain name - For production with SSL

Nice to have:

  • Understanding of REST APIs
  • Basic SQL knowledge
  • JavaScript/TypeScript experience for client integration

Understanding PocketBase Architecture

PocketBase is beautifully simple compared to traditional backends:

Traditional Backend Stack

Frontend
    ↓
API Server (Node.js/Python/Go)
    ↓
Authentication Service
    ↓
Database Server (PostgreSQL/MySQL)
    ↓
File Storage Service (S3/MinIO)
    ↓
Cache Layer (Redis)

PocketBase Stack

Frontend
    ↓
PocketBase (Everything in one binary)
    - SQLite Database
    - Authentication
    - File Storage
    - Real-time Subscriptions
    - Admin Dashboard

Core Components:

Database (SQLite)

  • Embedded database, no separate server needed
  • Full SQL support with relations
  • ACID compliant, battle-tested reliability
  • Stored in a single pb_data/data.db file

Authentication System

  • Email/password with verification
  • OAuth2 (Google, GitHub, Facebook, etc.)
  • JWT tokens and refresh tokens
  • User and admin roles

File Storage

  • Stores files in pb_data/storage
  • Automatic image resizing and thumbnails
  • Direct file serving with optional auth
  • Configurable size limits

Real-time Engine

  • WebSocket-based subscriptions
  • Automatic updates when data changes
  • Per-record or collection-wide subscriptions
  • Built-in presence tracking

Admin Dashboard

  • Web UI at /admin
  • Visual schema builder
  • User management
  • API rules editor
  • Logs and analytics

Installation and Setup

Method 1: Binary Installation (Simplest)

Linux/macOS:

# Create project directory
mkdir pocketbase-homelab
cd pocketbase-homelab

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

# Extract
unzip pocketbase_0.22.0_linux_amd64.zip

# Make executable
chmod +x pocketbase

# Run PocketBase
./pocketbase serve

# Access admin UI at http://127.0.0.1:8090/_/

First run setup:

  1. Navigate to http://127.0.0.1:8090/_/
  2. Create your admin account (email + password)
  3. You’re ready to build!

Custom configuration:

# Run on different port
./pocketbase serve --http=0.0.0.0:8080

# Specify data directory
./pocketbase serve --dir=/path/to/pb_data

# Enable debug mode
./pocketbase serve --dev

# Show help
./pocketbase --help

Method 2: Docker Deployment

Create docker-compose.yml:

version: '3.8'

services:
  pocketbase:
    image: ghcr.io/muchobien/pocketbase:latest
    container_name: pocketbase
    restart: unless-stopped
    ports:
      - "8090:8090"
    volumes:
      - pocketbase_data:/pb/pb_data
      - pocketbase_public:/pb/pb_public
    environment:
      - TZ=America/New_York
    healthcheck:
      test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:8090/api/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 10s

volumes:
  pocketbase_data:
  pocketbase_public:

Deploy:

# Start PocketBase
docker compose up -d

# View logs
docker compose logs -f pocketbase

# Access at http://localhost:8090/_/

Method 3: Systemd Service (Production)

Create service file /etc/systemd/system/pocketbase.service:

[Unit]
Description=PocketBase Backend Service
After=network.target

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

# Security hardening
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/opt/pocketbase/pb_data

[Install]
WantedBy=multi-user.target

Setup and start:

# Create user and directory
sudo useradd -r -s /bin/false pocketbase
sudo mkdir -p /opt/pocketbase
sudo chown pocketbase:pocketbase /opt/pocketbase

# Copy pocketbase binary
sudo cp pocketbase /opt/pocketbase/
sudo chown pocketbase:pocketbase /opt/pocketbase/pocketbase

# Enable and start service
sudo systemctl daemon-reload
sudo systemctl enable pocketbase
sudo systemctl start pocketbase

# Check status
sudo systemctl status pocketbase

Database Configuration

Creating Collections (Tables)

Collections in PocketBase are like tables in traditional databases.

Using Admin UI:

  1. Go to Admin Dashboard > Collections
  2. Click “New Collection”
  3. Choose collection type:
    • Base - Standard data collection
    • Auth - User authentication collection
    • View - SQL view (read-only)

Example: Creating a Blog Posts collection

Collection settings:

  • Name: posts
  • Type: Base
  • Schema:
FieldTypeRequiredOptions
titleTextYesMin: 3, Max: 200
contentEditorYes-
excerptTextNoMax: 500
featured_imageFileNoMax files: 1
publishedBoolYesDefault: false
authorRelationYesCollection: users
tagsJSONNo-

Using the API:

// Create collection via API (admin auth required)
const collection = await pb.collections.create({
  name: 'posts',
  type: 'base',
  schema: [
    {
      name: 'title',
      type: 'text',
      required: true,
      options: { min: 3, max: 200 }
    },
    {
      name: 'content',
      type: 'editor',
      required: true
    },
    {
      name: 'published',
      type: 'bool',
      required: true
    },
    {
      name: 'author',
      type: 'relation',
      required: true,
      options: { collectionId: 'users_id', maxSelect: 1 }
    }
  ]
})

Schema Field Types

Text Fields:

  • text - Short text (URL, plain editor)
  • editor - Rich text editor
  • email - Email validation
  • url - URL validation

Number Fields:

  • number - Integer or float
  • bool - True/false checkbox

Date/Time:

  • date - Date picker

Relations:

  • relation - Link to records in other collections
  • select - Dropdown with predefined values

Files:

  • file - File/image upload with size limits

Advanced:

  • json - Store JSON data
  • autodate - Automatically set creation/update time

API Rules and Security

API Rules control access to your collections. Each collection has rules for:

  • List/Search - Who can query records
  • View - Who can view a single record
  • Create - Who can create records
  • Update - Who can update records
  • Delete - Who can delete records

Rule syntax examples:

// Public read access
@request.auth.id != ""  // Any authenticated user

// Owner only
@request.auth.id = author.id  // Author field matches current user

// Public published posts
published = true  // Anyone can read if published

// Admin only
@request.auth.role = "admin"

// Combined rules
published = true || @request.auth.id = author.id  // Public if published OR owner

// Check relation
@request.auth.id = comments.author.id  // User owns related comment

Example: Blog post rules

List/Search rule:
published = true || @request.auth.id = author.id

View rule:
published = true || @request.auth.id = author.id

Create rule:
@request.auth.id != "" && @request.data.author = @request.auth.id

Update rule:
@request.auth.id = author.id

Delete rule:
@request.auth.id = author.id || @request.auth.role = "admin"

Database Migrations

Export schema for version control:

# Export all collections
./pocketbase export collections

# Creates JSON file with schema

Import schema:

# Import collections
./pocketbase import collections schema.json

Authentication Setup

Creating Auth Collection

By default, PocketBase includes a users auth collection. You can create additional auth collections for different user types.

Default users collection fields:

  • email - Unique email address
  • verified - Email verification status
  • emailVisibility - Show email in API responses
  • username - Optional unique username
  • name - Display name
  • avatar - Profile picture

Add custom fields:

// Via admin UI: Collections > users > Edit
// Add fields like:
- bio (Text)
- location (Text)
- website (URL)
- social_links (JSON)

Email/Password Authentication

JavaScript/TypeScript client:

npm install pocketbase
import PocketBase from 'pocketbase'

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

// Sign up new user
const user = await pb.collection('users').create({
  email: '[email protected]',
  password: 'secure-password',
  passwordConfirm: 'secure-password',
  name: 'John Doe',
  // Add custom fields
  bio: 'Software developer and homelab enthusiast'
})

// Send verification email
await pb.collection('users').requestVerification('[email protected]')

// Sign in
const authData = await pb.collection('users').authWithPassword(
  '[email protected]',
  'secure-password'
)

// Access token
console.log(pb.authStore.token)
console.log(pb.authStore.model) // User data

// Check if authenticated
console.log(pb.authStore.isValid)

// Sign out
pb.authStore.clear()

Auto-refresh authentication:

// Enable auto refresh (default: enabled)
pb.autoCancellation(false)

// Manual refresh
await pb.collection('users').authRefresh()

// Listen to auth state changes
pb.authStore.onChange((token, model) => {
  console.log('Auth state changed:', token, model)
})

OAuth2 Authentication

Enable OAuth providers in Admin UI:

  1. Settings > Auth providers
  2. Enable provider (Google, GitHub, Facebook, etc.)
  3. Enter Client ID and Client Secret
  4. Set redirect URL

Use in your app:

// List available OAuth providers
const authMethods = await pb.collection('users').listAuthMethods()

// Initiate OAuth flow
const authData = await pb.collection('users').authWithOAuth2({
  provider: 'google',
  createData: {
    name: 'John Doe',
    // Additional user data
  }
})

// GitHub OAuth example
const authData = await pb.collection('users').authWithOAuth2({
  provider: 'github',
  urlCallback: (url) => {
    // Open OAuth URL in popup or redirect
    window.open(url)
  }
})

Password Reset

// Request password reset email
await pb.collection('users').requestPasswordReset('[email protected]')

// Reset password with token (from email link)
await pb.collection('users').confirmPasswordReset(
  'TOKEN_FROM_EMAIL',
  'new-password',
  'new-password'
)

API Usage and Examples

CRUD Operations

Create record:

const record = await pb.collection('posts').create({
  title: 'My First Post',
  content: 'This is the content of my first post',
  published: true,
  author: pb.authStore.model.id,
  tags: ['tutorial', 'homelab']
})

List records:

// Get all posts
const records = await pb.collection('posts').getFullList({
  sort: '-created',
})

// Paginated list
const resultList = await pb.collection('posts').getList(1, 20, {
  filter: 'published = true',
  sort: '-created',
  expand: 'author', // Include related author data
})

console.log(resultList.page)
console.log(resultList.perPage)
console.log(resultList.totalItems)
console.log(resultList.items)

Get single record:

const record = await pb.collection('posts').getOne('RECORD_ID', {
  expand: 'author',
})

Update record:

const record = await pb.collection('posts').update('RECORD_ID', {
  title: 'Updated Title',
  published: true,
})

Delete record:

await pb.collection('posts').delete('RECORD_ID')

Advanced Filtering

// Text search
filter: 'title ~ "homelab"'  // Contains "homelab"

// Exact match
filter: 'published = true'

// Comparison
filter: 'created >= "2025-01-01"'

// Multiple conditions
filter: 'published = true && author = "USER_ID"'

// OR conditions
filter: 'published = true || author = "USER_ID"'

// Arrays (JSON fields)
filter: 'tags:each ~ "tutorial"'

// Relations
filter: 'author.name ~ "John"'

// Combining filters
const posts = await pb.collection('posts').getFullList({
  filter: 'published = true && tags:each ~ "homelab"',
  sort: '-created',
  expand: 'author',
})

Real-time Subscriptions

Subscribe to collection changes:

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

// Subscribe to specific record
pb.collection('posts').subscribe('RECORD_ID', (e) => {
  console.log('Post updated:', e.record)
})

// Subscribe with filter
pb.collection('posts').subscribe('*', (e) => {
  console.log('New published post:', e.record)
}, {
  filter: 'published = true'
})

// Unsubscribe
pb.collection('posts').unsubscribe('*')
pb.collection('posts').unsubscribe('RECORD_ID')

React example with real-time:

import { useEffect, useState } from 'react'
import PocketBase from 'pocketbase'

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

function PostsList() {
  const [posts, setPosts] = useState([])

  useEffect(() => {
    // Initial fetch
    const fetchPosts = async () => {
      const records = await pb.collection('posts').getFullList({
        sort: '-created',
        expand: 'author',
      })
      setPosts(records)
    }
    fetchPosts()

    // Subscribe to real-time updates
    pb.collection('posts').subscribe('*', (e) => {
      if (e.action === 'create') {
        setPosts(prev => [e.record, ...prev])
      } else if (e.action === 'update') {
        setPosts(prev => prev.map(p => p.id === e.record.id ? e.record : p))
      } else if (e.action === 'delete') {
        setPosts(prev => prev.filter(p => p.id !== e.record.id))
      }
    })

    // Cleanup
    return () => {
      pb.collection('posts').unsubscribe('*')
    }
  }, [])

  return (
    <div>
      {posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.content}</p>
          <small>By {post.expand?.author?.name}</small>
        </article>
      ))}
    </div>
  )
}

File Uploads

Upload files:

// Create FormData
const formData = new FormData()
formData.append('title', 'Post with Image')
formData.append('content', 'Content here')
formData.append('featured_image', fileInput.files[0])
formData.append('author', pb.authStore.model.id)

// Create record with file
const record = await pb.collection('posts').create(formData)

// Update with new file
formData.append('featured_image', newFile)
await pb.collection('posts').update(record.id, formData)

Get file URL:

// Get file URL
const imageUrl = pb.files.getUrl(record, record.featured_image)

// With thumbnail (for images)
const thumbUrl = pb.files.getUrl(record, record.featured_image, {
  thumb: '100x100'
})

// Available thumb sizes: 100x100, 200x200, 300x300, etc.

Delete file:

// Remove file from record
await pb.collection('posts').update(record.id, {
  featured_image: null
})

Example Integrations

Next.js Integration

Setup PocketBase client (lib/pocketbase.ts):

import PocketBase from 'pocketbase'

export const pb = new PocketBase('http://localhost:8090')

// Enable auto-cancellation (optional)
pb.autoCancellation(false)

// Load auth from cookies (SSR)
export function initPocketBase(req?: any) {
  if (typeof window === 'undefined' && req) {
    // Server-side: load auth from cookies
    pb.authStore.loadFromCookie(req.headers.cookie || '')
  }
  return pb
}

API route (pages/api/posts.ts):

import type { NextApiRequest, NextApiResponse } from 'next'
import { initPocketBase } from '@/lib/pocketbase'

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  const pb = initPocketBase(req)

  if (req.method === 'GET') {
    const posts = await pb.collection('posts').getFullList({
      sort: '-created',
      filter: 'published = true',
      expand: 'author',
    })
    res.status(200).json(posts)
  } else if (req.method === 'POST') {
    const post = await pb.collection('posts').create(req.body)
    res.status(201).json(post)
  } else {
    res.status(405).json({ error: 'Method not allowed' })
  }
}

Python Integration

pip install pocketbase
from pocketbase import PocketBase

# Initialize client
pb = PocketBase('http://127.0.0.1:8090')

# Authenticate
admin_data = pb.admins.auth_with_password('[email protected]', 'password')
# Or user auth
user_data = pb.collection('users').auth_with_password('[email protected]', 'password')

# Create record
post = pb.collection('posts').create({
    'title': 'New Post from Python',
    'content': 'Content here',
    'published': True,
    'author': pb.auth_store.model.id
})

# Get records
posts = pb.collection('posts').get_full_list(
    query_params={
        'filter': 'published = true',
        'sort': '-created',
        'expand': 'author'
    }
)

# Update record
updated_post = pb.collection('posts').update(post.id, {
    'title': 'Updated Title'
})

# Delete record
pb.collection('posts').delete(post.id)

# File upload
with open('image.jpg', 'rb') as f:
    post_with_file = pb.collection('posts').create({
        'title': 'Post with Image',
        'featured_image': f
    })

Vue.js Integration

<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import PocketBase from 'pocketbase'

const pb = new PocketBase('http://127.0.0.1:8090')
const posts = ref([])
const isAuthenticated = ref(pb.authStore.isValid)

async function login(email, password) {
  await pb.collection('users').authWithPassword(email, password)
  isAuthenticated.value = true
}

async function createPost(title, content) {
  const post = await pb.collection('posts').create({
    title,
    content,
    published: true,
    author: pb.authStore.model.id
  })
  posts.value.unshift(post)
}

onMounted(async () => {
  // Fetch initial posts
  posts.value = await pb.collection('posts').getFullList({
    sort: '-created',
    filter: 'published = true',
    expand: 'author'
  })

  // Subscribe to real-time updates
  pb.collection('posts').subscribe('*', (e) => {
    if (e.action === 'create') {
      posts.value.unshift(e.record)
    } else if (e.action === 'update') {
      const index = posts.value.findIndex(p => p.id === e.record.id)
      if (index !== -1) posts.value[index] = e.record
    } else if (e.action === 'delete') {
      posts.value = posts.value.filter(p => p.id !== e.record.id)
    }
  })
})

onUnmounted(() => {
  pb.collection('posts').unsubscribe('*')
})
</script>

<template>
  <div>
    <div v-for="post in posts" :key="post.id">
      <h2>{{ post.title }}</h2>
      <p>{{ post.content }}</p>
    </div>
  </div>
</template>

Backup Procedures

Manual Backup

PocketBase stores all data in the pb_data directory:

# Stop PocketBase (if running as service)
sudo systemctl stop pocketbase

# Create backup
tar czf pocketbase-backup-$(date +%Y%m%d).tar.gz pb_data/

# Restart PocketBase
sudo systemctl start pocketbase

Backup contains:

  • data.db - SQLite database
  • storage/ - Uploaded files
  • logs.db - Application logs

Automated Backup Script

#!/bin/bash
# backup-pocketbase.sh

POCKETBASE_DIR="/opt/pocketbase"
BACKUP_DIR="/backups/pocketbase"
DATE=$(date +%Y%m%d_%H%M%S)
RETENTION_DAYS=30

# Create backup directory
mkdir -p $BACKUP_DIR

# Backup database (online backup using SQLite)
sqlite3 $POCKETBASE_DIR/pb_data/data.db ".backup '$BACKUP_DIR/data_$DATE.db'"

# Backup entire pb_data directory
tar czf "$BACKUP_DIR/pb_data_$DATE.tar.gz" -C $POCKETBASE_DIR pb_data/

# Remove old backups
find $BACKUP_DIR -name "data_*.db" -mtime +$RETENTION_DAYS -delete
find $BACKUP_DIR -name "pb_data_*.tar.gz" -mtime +$RETENTION_DAYS -delete

echo "Backup completed: $DATE"

Schedule with cron:

# Run daily at 3 AM
0 3 * * * /home/user/backup-pocketbase.sh >> /var/log/pocketbase-backup.log 2>&1

Restore from Backup

# Stop PocketBase
sudo systemctl stop pocketbase

# Restore from backup
cd /opt/pocketbase
rm -rf pb_data/  # WARNING: This deletes current data
tar xzf /backups/pocketbase/pb_data_20250115.tar.gz

# Start PocketBase
sudo systemctl start pocketbase

Production Deployment

Reverse Proxy with nginx

server {
    listen 443 ssl http2;
    server_name api.yourdomain.com;

    ssl_certificate /etc/letsencrypt/live/api.yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/api.yourdomain.com/privkey.pem;

    # Increase upload size limit
    client_max_body_size 50M;

    location / {
        proxy_pass http://127.0.0.1:8090;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # WebSocket support for real-time
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";

        # Timeouts
        proxy_connect_timeout 600;
        proxy_send_timeout 600;
        proxy_read_timeout 600;
        send_timeout 600;
    }
}

# Redirect HTTP to HTTPS
server {
    listen 80;
    server_name api.yourdomain.com;
    return 301 https://$server_name$request_uri;
}

Traefik Configuration

# docker-compose.yml with Traefik
version: '3.8'

services:
  pocketbase:
    image: ghcr.io/muchobien/pocketbase:latest
    container_name: pocketbase
    restart: unless-stopped
    volumes:
      - pocketbase_data:/pb/pb_data
    networks:
      - traefik
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.pocketbase.rule=Host(`api.yourdomain.com`)"
      - "traefik.http.routers.pocketbase.entrypoints=websecure"
      - "traefik.http.routers.pocketbase.tls.certresolver=letsencrypt"
      - "traefik.http.services.pocketbase.loadbalancer.server.port=8090"

volumes:
  pocketbase_data:

networks:
  traefik:
    external: true

Environment Variables

Configure PocketBase via environment variables:

# .env file
POCKETBASE_PORT=8090
POCKETBASE_DATA_DIR=/pb/pb_data
POCKETBASE_ENCRYPTION_KEY=your-32-char-encryption-key

Troubleshooting

Database Locked Error

Symptoms: database is locked error when multiple requests occur

Solutions:

# Check for zombie processes
ps aux | grep pocketbase

# Increase SQLite timeout in custom build
# Or use WAL mode (enabled by default in recent versions)

Enable WAL mode manually:

sqlite3 pb_data/data.db "PRAGMA journal_mode=WAL;"

File Upload Fails

Check file size limits:

Admin UI > Settings > File upload > Max file size

Verify storage permissions:

# Check permissions
ls -la pb_data/storage/

# Fix permissions
chmod -R 755 pb_data/storage/

Authentication Issues

JWT token expired:

// Force refresh token
await pb.collection('users').authRefresh()

// Check expiry
console.log(pb.authStore.token)
console.log(pb.authStore.isValid)

Clear auth state:

pb.authStore.clear()

Real-time Not Working

Check WebSocket connection:

  1. Ensure reverse proxy supports WebSocket upgrades
  2. Verify firewall allows WebSocket connections
  3. Check browser console for connection errors

Test connection:

pb.realtime.subscribe('test', (data) => {
  console.log('Real-time connected:', data)
})

Performance Optimization

SQLite optimization:

-- Analyze and optimize database
sqlite3 pb_data/data.db "PRAGMA optimize;"
sqlite3 pb_data/data.db "VACUUM;"

-- Create indexes for common queries
sqlite3 pb_data/data.db "CREATE INDEX idx_posts_created ON posts(created DESC);"

Monitor database size:

# Check database size
du -h pb_data/data.db

# Check storage size
du -h pb_data/storage/

Advanced Features

Custom Routes (Hooks)

Extend PocketBase with custom Go code:

// main.go
package main

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

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

    // Custom API endpoint
    app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
        e.Router.GET("/api/custom", func(c echo.Context) error {
            return c.JSON(200, map[string]string{
                "message": "Custom endpoint",
            })
        })
        return nil
    })

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

Database Views

Create read-only SQL views:

-- Via admin UI: Collections > New Collection > View

-- Example: Recent posts view
SELECT
    p.id,
    p.title,
    p.content,
    p.created,
    u.name as author_name,
    u.avatar as author_avatar
FROM posts p
LEFT JOIN users u ON p.author = u.id
WHERE p.published = true
ORDER BY p.created DESC
LIMIT 10

SMTP Configuration

Admin UI: Settings > Mail settings

SMTP Host: smtp.gmail.com
SMTP Port: 587
SMTP Username: [email protected]
SMTP Password: your-app-password
Sender Name: PocketBase Homelab
Sender Email: [email protected]

Test email:

// Request verification email
await pb.collection('users').requestVerification('[email protected]')

Conclusion

PocketBase proves that powerful doesn’t have to mean complex. With a single binary, you get a complete backend that’s perfect for homelab projects, internal tools, MVPs, and production applications. Its simplicity makes it incredibly easy to deploy and maintain, while SQLite ensures reliability and performance.

Key advantages:

  • Zero complexity - One file, no dependencies
  • Batteries included - Auth, DB, storage, real-time, admin UI
  • Lightning fast - Go-powered performance
  • Self-contained - SQLite means no external database
  • Developer friendly - Excellent documentation and SDKs

Perfect for:

  • Personal projects and MVPs
  • Internal homelab tools
  • Mobile app backends
  • Rapid prototyping
  • Educational projects

Next steps:

Start simple, scale as needed, and enjoy the freedom of a backend that just works!