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
- Lesson 1: Run Pocketbase on high ports (8090+) with reverse proxy - avoids mobile network blocking.
- Lesson 2: Always use the "expand" parameter for relations - it's not automatic.
- Lesson 3: Realtime subscriptions require proper API rules - check permissions when events aren't firing.
- Overall: Pocketbase is incredibly simple but has some gotchas. The single-binary deployment is amazing for side projects, and it handles way more traffic than you'd expect from a "lightweight" backend.
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
- Service restarting frequently - check disk space and memory usage
- Mobile apps timing out - verify reverse proxy and CORS configuration
- Realtime not working - check API rules and subscription filters
- Slow query performance - consider adding indexes in SQLite