Pocketbase: Single File Backend That Actually Scales

Needed a backend for my Flutter app but Firebase was overkill. Found Pocketbase - literally one Go binary that handles auth, database, and realtime. Deployed it on a $4 VPS and it handles 10k users without breaking a sweat.

Problem

Set up Pocketbase on a VPS, worked fine from browser. Flutter app on Android couldn't connect - connection timeout errors. Tried the API URL in Postman, worked fine. Only mobile apps were affected.

Error: SocketException: Connection timed out

What I Tried

Attempt 1: Added http (non-https) to Android manifest - still timed out.
Attempt 2: Used IP address instead of domain - browser worked, mobile didn't.
Attempt 3: Checked firewall rules - ports 80 and 443 were open.

Actual Fix

The issue was that mobile carriers sometimes block direct connections to ports below 1024 on some networks. Running Pocketbase on port 8090 instead of 80 solved it. Also needed to configure proper CORS for mobile origins.

# Run Pocketbase on port 8090 instead of 80
./pocketbase serve --http 0.0.0.0:8090

# Set up reverse proxy (Nginx) to handle SSL and CORS
server {
    listen 443 ssl http2;
    server_name api.yourdomain.com;

    ssl_certificate /path/to/cert.pem;
    ssl_certificate_key /path/to/key.pem;

    location / {
        proxy_pass http://localhost: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;

        # CORS for mobile
        add_header 'Access-Control-Allow-Origin' '*' always;
        add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
        add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type' always;
    }
}

Problem

Created two collections: "posts" and "users", with a relation field in posts pointing to users. When fetching posts in Flutter, the user field came back null. Direct API call showed the relation ID, but the expanded data wasn't there.

What I Tried

Attempt 1: Verified relation was set up correctly in Pocketbase UI - it was.
Attempt 2: Tried fetching users separately and joining manually - tedious and didn't sync properly.
Attempt 3: Checked API documentation - seemed like I was doing it right.

Actual Fix

Pocketbase requires explicit expansion of relations via the "expand" query parameter. The Flutter SDK needs to be told which relations to fetch. Not obvious from the basic documentation.

// Flutter code to fetch posts with expanded user relation
final records = await pb.collection('posts').getList(
  expand: 'user',  // This is the key! Tell Pocketbase to expand relations
  filter: 'published = true',
);

// For multiple relations
final records = await pb.collection('posts').getList(
  expand: 'user,category,tags',  // Comma-separated list
);

// Access expanded data
for (var record in records.items) {
  final user = record.expand['user'];  // Access expanded relation
  print('Post by ${user['name']}');
}

// Alternative: Use expand on specific fields
final record = await pb.collection('posts').getOne(
  recordId,
  expand: 'user',  // Works for single record too
);

Problem

Set up a realtime subscription to listen for new posts. Initial connection worked fine, but when I created a new post (through the admin UI), the Flutter app didn't receive anything. The subscription was active but silent.

What I Tried

Attempt 1: Checked subscription status - showed "connected".
Attempt 2: Tried subscribing to "*" (all events) - still nothing.
Attempt 3: Added logs to confirm posts were being created - they were.

Actual Fix

Realtime subscriptions need proper rule configuration in Pocketbase. The admin user creating posts didn't trigger client subscriptions because the rules weren't set up to broadcast those events. Also needed to handle the subscribe callback properly in Flutter.

// Flutter realtime subscription setup
// Subscribe to posts collection updates
pb.collection('posts').subscribe('*', (e) {
  switch (e.action) {
    case 'create':
      print('New post created: ${e.record['title']}');
      break;
    case 'update':
      print('Post updated: ${e.record['title']}');
      break;
    case 'delete':
      print('Post deleted: ${e.record['title']}');
      break;
  }
}, filter: 'published = true');  // Optional: filter events

// Important: Handle connection state
pb.collection('posts').subscribe('*', (e) {
  print('Event received: $e');
}, onError: (error) {
  print('Subscription error: $error');
});

// In Pocketbase admin UI, ensure API rules allow subscription:
// Settings → API Rules → Posts → Set "View" rule to allow access
// For public subscriptions, set: "id = @request.auth.id || published = true"

// Keep subscription alive (Flutter lifecycle)
@override
void dispose() {
  pb.collection('posts').unsubscribe();
  super.dispose();
}

What I Learned

Production Setup

Complete production deployment with systemd and reverse proxy.

# 1. Download and install Pocketbase
wget https://github.com/pocketbase/pocketbase/releases/download/v0.22.0/pocketbase_0.22.0_linux_amd64.zip
unzip pocketbase_0.22.0_linux_amd64.zip -d /opt/pocketbase

# 2. Create systemd service
cat > /etc/systemd/system/pocketbase.service << EOF
[Unit]
Description=Pocketbase
After=network.target

[Service]
Type=simple
User=pocketbase
WorkingDirectory=/opt/pocketbase
ExecStart=/opt/pocketbase/pocketbase serve --http0.0.0.0:8090
Restart=always
RestartSec=5s

[Install]
WantedBy=multi-user.target
EOF

# 3. Create user and set permissions
useradd -r -s /bin/false pocketbase
chown -R pocketbase:pocketbase /opt/pocketbase

# 4. Start service
systemctl daemon-reload
systemctl enable pocketbase
systemctl start pocketbase

# 5. Set up Nginx reverse proxy
cat > /etc/nginx/sites-available/pocketbase << EOF
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;

    location / {
        proxy_pass http://localhost: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;

        # CORS for mobile apps
        add_header 'Access-Control-Allow-Origin' '*' always;
        add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
        add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type' always;

        if (\$request_method = 'OPTIONS') {
            return 204;
        }
    }
}
EOF

ln -s /etc/nginx/sites-available/pocketbase /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx
// Flutter initialization
import 'package:pocketbase/pocketbase.dart';

final pb = Pocketbase('https://api.yourdomain.com');

// Auth example
final authData = await pb.collection('users').create(
  email: 'user@example.com',
  password: 'securepassword',
  passwordConfirm: 'securepassword',
);

// Save token for later
await pb.collection('users').requestVerification('user@example.com');

// Store auth token
final prefs = await SharedPreferences.getInstance();
await prefs.setString('pb_token', pb.authStore.token);

// Restore auth later
pb.authStore.loadFromToken(prefs.getString('pb_token') ?? '');

Monitoring & Debugging

Essential commands for Pocketbase maintenance:

Essential Commands

# Check service status
systemctl status pocketbase

# View logs
journalctl -u pocketbase -f

# Restart service
systemctl restart pocketbase

# Backup data
tar -czf pocketbase_backup_$(date +%Y%m%d).tar.gz /opt/pocketbase/pb_data

# Admin UI access
# https://api.yourdomain.com/_/
# Default admin: create on first visit

# Test API endpoint
curl https://api.yourdomain.com/api/collections/posts/records \
  -H "Authorization: Bearer YOUR_TOKEN"

Red Flags to Watch For

Related Resources