# Production & Deployment **Impact: LOW-MEDIUM** Backup strategies, configuration management, reverse proxy setup, and SQLite optimization. --- ## 1. Implement Proper Backup Strategies **Impact: LOW-MEDIUM (Prevents data loss, enables disaster recovery)** Regular backups are essential for production deployments. PocketBase provides built-in backup functionality and supports external S3 storage. **Incorrect (no backup strategy):** ```javascript // No backups at all - disaster waiting to happen // Just running: ./pocketbase serve // Manual file copy while server running - can corrupt data // cp pb_data/data.db backup/ // Only backing up database, missing files // sqlite3 pb_data/data.db ".backup backup.db" ``` **Correct (comprehensive backup strategy):** ```javascript // 1. Using PocketBase Admin API for backups const adminPb = new PocketBase('http://127.0.0.1:8090'); await adminPb.collection('_superusers').authWithPassword(admin, password); // Create backup (includes database and files) async function createBackup(name = '') { const backup = await adminPb.backups.create(name); console.log('Backup created:', backup.key); return backup; } // List available backups async function listBackups() { const backups = await adminPb.backups.getFullList(); backups.forEach(b => { console.log(`${b.key} - ${b.size} bytes - ${b.modified}`); }); return backups; } // Download backup async function downloadBackup(key) { const token = await adminPb.files.getToken(); const url = adminPb.backups.getDownloadURL(token, key); // url can be used to download the backup file return url; } // Restore from backup (CAUTION: overwrites current data!) async function restoreBackup(key) { await adminPb.backups.restore(key); console.log('Restore initiated - server will restart'); } // Delete old backups async function cleanupOldBackups(keepCount = 7) { const backups = await adminPb.backups.getFullList(); // Sort by date, keep newest const sorted = backups.sort((a, b) => new Date(b.modified) - new Date(a.modified) ); const toDelete = sorted.slice(keepCount); for (const backup of toDelete) { await adminPb.backups.delete(backup.key); console.log('Deleted old backup:', backup.key); } } ``` **Automated backup script (cron job):** ```bash #!/bin/bash # backup.sh - Run daily via cron POCKETBASE_URL="http://127.0.0.1:8090" ADMIN_EMAIL="admin@example.com" ADMIN_PASSWORD="your-secure-password" BACKUP_DIR="/path/to/backups" KEEP_DAYS=7 # Create timestamp TIMESTAMP=$(date +%Y%m%d_%H%M%S) # Create backup via API curl -X POST "${POCKETBASE_URL}/api/backups" \ -H "Authorization: $(curl -s -X POST "${POCKETBASE_URL}/api/collections/_superusers/auth-with-password" \ -d "identity=${ADMIN_EMAIL}&password=${ADMIN_PASSWORD}" | jq -r '.token')" \ -d "name=backup_${TIMESTAMP}" # Clean old local backups find "${BACKUP_DIR}" -name "*.zip" -mtime +${KEEP_DAYS} -delete echo "Backup completed: backup_${TIMESTAMP}" ``` **Configure S3 for backup storage:** ```javascript // In Admin UI: Settings > Backups > S3 // Or via API: await adminPb.settings.update({ backups: { s3: { enabled: true, bucket: 'my-pocketbase-backups', region: 'us-east-1', endpoint: 's3.amazonaws.com', accessKey: process.env.AWS_ACCESS_KEY, secret: process.env.AWS_SECRET_KEY } } }); ``` **Backup best practices:** | Aspect | Recommendation | |--------|---------------| | Frequency | Daily minimum, hourly for critical apps | | Retention | 7-30 days of daily backups | | Storage | Off-site (S3, separate server) | | Testing | Monthly restore tests | | Monitoring | Alert on backup failures | **Pre-backup checklist:** - [ ] S3 or external storage configured - [ ] Automated schedule set up - [ ] Retention policy defined - [ ] Restore procedure documented - [ ] Restore tested successfully Reference: [PocketBase Backups](https://pocketbase.io/docs/going-to-production/#backups) ## 2. Configure Production Settings Properly **Impact: LOW-MEDIUM (Secure and optimized production environment)** Production deployments require proper configuration of URLs, secrets, SMTP, and security settings. **Incorrect (development defaults in production):** ```bash # Running with defaults - insecure! ./pocketbase serve # Hardcoded secrets ./pocketbase serve --encryptionEnv="mySecretKey123" # Wrong origin for CORS # Leaving http://localhost:8090 as allowed origin ``` **Correct (production configuration):** ```bash # Production startup with essential flags ./pocketbase serve \ --http="0.0.0.0:8090" \ --origins="https://myapp.com,https://www.myapp.com" \ --encryptionEnv="PB_ENCRYPTION_KEY" # Using environment variables export PB_ENCRYPTION_KEY="your-32-char-encryption-key-here" export SMTP_HOST="smtp.sendgrid.net" export SMTP_PORT="587" export SMTP_USER="apikey" export SMTP_PASS="your-sendgrid-api-key" ./pocketbase serve --http="0.0.0.0:8090" ``` **Configure SMTP for emails:** ```javascript // Via Admin UI or API await adminPb.settings.update({ smtp: { enabled: true, host: process.env.SMTP_HOST, port: parseInt(process.env.SMTP_PORT), username: process.env.SMTP_USER, password: process.env.SMTP_PASS, tls: true }, meta: { appName: 'My App', appURL: 'https://myapp.com', senderName: 'My App', senderAddress: 'noreply@myapp.com' } }); // Test email configuration await adminPb.settings.testEmail('users', 'test@example.com', 'verification'); ``` **Configure S3 for file storage:** ```javascript // Move file storage to S3 for scalability await adminPb.settings.update({ s3: { enabled: true, bucket: 'my-app-files', region: 'us-east-1', endpoint: 's3.amazonaws.com', accessKey: process.env.AWS_ACCESS_KEY, secret: process.env.AWS_SECRET_KEY, forcePathStyle: false } }); // Test S3 connection await adminPb.settings.testS3('storage'); ``` **Systemd service file:** ```ini # /etc/systemd/system/pocketbase.service [Unit] Description=PocketBase After=network.target [Service] Type=simple User=pocketbase Group=pocketbase LimitNOFILE=4096 Restart=always RestartSec=5s WorkingDirectory=/opt/pocketbase ExecStart=/opt/pocketbase/pocketbase serve --http="127.0.0.1:8090" # Environment variables EnvironmentFile=/opt/pocketbase/.env # Security hardening NoNewPrivileges=yes PrivateTmp=yes ProtectSystem=strict ProtectHome=yes ReadWritePaths=/opt/pocketbase/pb_data [Install] WantedBy=multi-user.target ``` **Environment file (.env):** ```bash # /opt/pocketbase/.env # SECURITY: Set restrictive permissions: chmod 600 /opt/pocketbase/.env # SECURITY: Add to .gitignore - NEVER commit this file to version control # For production, consider a secrets manager (Vault, AWS Secrets Manager, etc.) PB_ENCRYPTION_KEY= # Generate with: openssl rand -hex 16 # SMTP SMTP_HOST=smtp.sendgrid.net SMTP_PORT=587 SMTP_USER=apikey SMTP_PASS= # Set your SMTP password here # S3 (optional) AWS_ACCESS_KEY= # Set your AWS access key AWS_SECRET_KEY= # Set your AWS secret key # OAuth (optional) GOOGLE_CLIENT_ID= # Set your Google client ID GOOGLE_CLIENT_SECRET= # Set your Google client secret ``` **Protect your environment file:** ```bash # Set restrictive permissions (owner read/write only) chmod 600 /opt/pocketbase/.env chown pocketbase:pocketbase /opt/pocketbase/.env # Ensure .env is in .gitignore echo ".env" >> .gitignore ``` **Production checklist:** - [ ] HTTPS enabled (via reverse proxy) - [ ] Strong encryption key set - [ ] CORS origins configured - [ ] SMTP configured and tested - [ ] Superuser password changed - [ ] S3 configured (for scalability) - [ ] Backup schedule configured - [ ] Rate limiting enabled (via reverse proxy) - [ ] Logging configured Reference: [PocketBase Going to Production](https://pocketbase.io/docs/going-to-production/) ## 3. Enable Rate Limiting for API Protection **Impact: MEDIUM (Prevents abuse, brute-force attacks, and DoS)** PocketBase v0.23+ includes built-in rate limiting. Enable it to protect against brute-force attacks, API abuse, and excessive resource consumption. > **v0.36.7 behavioral change:** the built-in limiter switched from a sliding-window to a **fixed-window** strategy. This is cheaper and more predictable, but it means a client can send `2 * maxRequests` in quick succession if they straddle the window boundary. Size your limits with that worst case in mind, and put Nginx/Caddy rate limiting in front of PocketBase for defense in depth (see examples below). **Incorrect (no rate limiting):** ```bash # Running without rate limiting ./pocketbase serve # Vulnerable to: # - Brute-force password attacks # - API abuse and scraping # - DoS from excessive requests # - Account enumeration attempts ``` **Correct (enable rate limiting):** ```bash # Enable via command line flag ./pocketbase serve --rateLimiter=true # Or configure specific limits (requests per second per IP) ./pocketbase serve --rateLimiter=true --rateLimiterRPS=10 ``` **Configure via Admin Dashboard:** Navigate to Settings > Rate Limiter: - **Enable rate limiter**: Toggle on - **Max requests/second**: Default 10, adjust based on needs - **Exempt endpoints**: Optionally whitelist certain paths **Configure programmatically (Go/JS hooks):** ```javascript // In pb_hooks/rate_limit.pb.js routerAdd("GET", "/api/public/*", (e) => { // Custom rate limit for specific endpoints }, $apis.rateLimit(100, "10s")); // 100 requests per 10 seconds // Stricter limit for auth endpoints routerAdd("POST", "/api/collections/users/auth-*", (e) => { // Auth endpoints need stricter limits }, $apis.rateLimit(5, "1m")); // 5 attempts per minute ``` **Rate limiting with reverse proxy (additional layer):** ```nginx # Nginx rate limiting (defense in depth) http { # Define rate limit zones limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s; limit_req_zone $binary_remote_addr zone=auth:10m rate=1r/s; server { # General API rate limit location /api/ { limit_req zone=api burst=20 nodelay; proxy_pass http://pocketbase; } # Strict limit for auth endpoints location /api/collections/users/auth { limit_req zone=auth burst=5 nodelay; proxy_pass http://pocketbase; } # Stricter limit for superuser auth location /api/collections/_superusers/auth { limit_req zone=auth burst=3 nodelay; proxy_pass http://pocketbase; } } } ``` ```caddyfile # Caddy with rate limiting plugin myapp.com { rate_limit { zone api { key {remote_host} events 100 window 10s } zone auth { key {remote_host} events 5 window 1m } } @auth path /api/collections/*/auth* handle @auth { rate_limit { zone auth } reverse_proxy 127.0.0.1:8090 } handle { rate_limit { zone api } reverse_proxy 127.0.0.1:8090 } } ``` **Handle rate limit errors in client:** ```javascript async function makeRequest(fn, retries = 0, maxRetries = 3) { try { return await fn(); } catch (error) { if (error.status === 429 && retries < maxRetries) { // Rate limited - wait and retry with limit const retryAfter = error.response?.retryAfter || 60; console.log(`Rate limited. Retry ${retries + 1}/${maxRetries} after ${retryAfter}s`); // Show user-friendly message showMessage('Too many requests. Please wait a moment.'); await sleep(retryAfter * 1000); return makeRequest(fn, retries + 1, maxRetries); } throw error; } } // Usage const result = await makeRequest(() => pb.collection('posts').getList(1, 20) ); ``` **Recommended limits by endpoint type:** | Endpoint Type | Suggested Limit | Reason | |--------------|-----------------|--------| | Auth endpoints | 5-10/min | Prevent brute-force | | Password reset | 3/hour | Prevent enumeration | | Record creation | 30/min | Prevent spam | | General API | 60-100/min | Normal usage | | Public read | 100-200/min | Higher for reads | | File uploads | 10/min | Resource-intensive | **Monitoring rate limit hits:** ```javascript // Check PocketBase logs for rate limit events // Or set up alerting in your monitoring system // Client-side tracking pb.afterSend = function(response, data) { if (response.status === 429) { trackEvent('rate_limit_hit', { endpoint: response.url, timestamp: new Date() }); } return data; }; ``` Reference: [PocketBase Going to Production](https://pocketbase.io/docs/going-to-production/) ## 4. Configure Reverse Proxy Correctly **Impact: LOW-MEDIUM (HTTPS, caching, rate limiting, and security headers)** Use a reverse proxy (Nginx, Caddy) for HTTPS termination, caching, rate limiting, and security headers. **Incorrect (exposing PocketBase directly):** ```bash # Direct exposure - no HTTPS, no rate limiting ./pocketbase serve --http="0.0.0.0:8090" # Port forwarding without proxy iptables -t nat -A PREROUTING -p tcp --dport 443 -j REDIRECT --to-port 8090 # Still no HTTPS! ``` **Correct (Caddy - simplest option):** ```caddyfile # /etc/caddy/Caddyfile myapp.com { # Automatic HTTPS via Let's Encrypt reverse_proxy 127.0.0.1:8090 { # Required for SSE/Realtime flush_interval -1 } # Security headers header { X-Content-Type-Options "nosniff" X-Frame-Options "DENY" Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'" Referrer-Policy "strict-origin-when-cross-origin" -Server } # Restrict admin UI to internal/VPN networks # @admin path /_/* # handle @admin { # @blocked not remote_ip 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 # respond @blocked 403 # reverse_proxy 127.0.0.1:8090 # } # Rate limiting (requires caddy-ratelimit plugin) # Install: xcaddy build --with github.com/mholt/caddy-ratelimit # Without this plugin, use PocketBase's built-in rate limiter (--rateLimiter=true) # rate_limit { # zone api { # key {remote_host} # events 100 # window 1m # } # } } ``` **Correct (Nginx configuration):** ```nginx # /etc/nginx/sites-available/pocketbase # Rate limit zones must be defined in http context (e.g., /etc/nginx/nginx.conf) # limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s; upstream pocketbase { server 127.0.0.1:8090; keepalive 64; } server { listen 80; server_name myapp.com; return 301 https://$server_name$request_uri; } server { listen 443 ssl http2; server_name myapp.com; # SSL configuration ssl_certificate /etc/letsencrypt/live/myapp.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/myapp.com/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256; ssl_prefer_server_ciphers off; # Security headers add_header X-Content-Type-Options "nosniff" always; add_header X-Frame-Options "DENY" always; add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; # Note: X-XSS-Protection is deprecated and can introduce vulnerabilities. # Use Content-Security-Policy instead. location / { proxy_pass http://pocketbase; proxy_http_version 1.1; # Headers 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; # SSE/Realtime support proxy_set_header Connection ''; proxy_buffering off; proxy_cache off; chunked_transfer_encoding off; # Timeouts proxy_read_timeout 3600s; proxy_send_timeout 3600s; } # Rate limit API endpoints location /api/ { limit_req zone=api burst=20 nodelay; proxy_pass http://pocketbase; proxy_http_version 1.1; 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; proxy_set_header Connection ''; proxy_buffering off; } # Static file caching location /api/files/ { proxy_pass http://pocketbase; proxy_cache_valid 200 1d; expires 1d; add_header Cache-Control "public, immutable"; } # Gzip compression gzip on; gzip_types text/plain application/json application/javascript text/css; gzip_min_length 1000; } ``` **Docker Compose with Caddy:** ```yaml # docker-compose.yml version: '3.8' services: pocketbase: # NOTE: This is a third-party community image, not officially maintained by PocketBase. # For production, consider building your own image from the official PocketBase binary. # See: https://pocketbase.io/docs/going-to-production/ image: ghcr.io/muchobien/pocketbase:latest restart: unless-stopped volumes: - ./pb_data:/pb_data environment: - PB_ENCRYPTION_KEY=${PB_ENCRYPTION_KEY} caddy: image: caddy:2-alpine restart: unless-stopped ports: - "80:80" - "443:443" volumes: - ./Caddyfile:/etc/caddy/Caddyfile - caddy_data:/data - caddy_config:/config depends_on: - pocketbase volumes: caddy_data: caddy_config: ``` **Key configuration points:** | Feature | Why It Matters | |---------|---------------| | HTTPS | Encrypts traffic, required for auth | | SSE support | `proxy_buffering off` for realtime | | Rate limiting | Prevents abuse | | Security headers | XSS/clickjacking protection | | Keepalive | Connection reuse, better performance | Reference: [PocketBase Going to Production](https://pocketbase.io/docs/going-to-production/) ## 5. Tune OS and Runtime for PocketBase Scale **Impact: MEDIUM (Prevents file descriptor exhaustion, OOM kills, and exposes secure config for production deployments)** Three low-effort OS/runtime knobs have outsized impact on production stability: open-file limits for realtime connections, Go memory limits for constrained hosts, and settings encryption for shared or externally-backed infrastructure. None of these are set automatically. **Incorrect (default OS limits, no memory governor, plain-text settings):** ```bash # Start without raising the file descriptor limit /root/pb/pocketbase serve yourdomain.com # → "Too many open files" once concurrent realtime connections exceed ~1024 # Start in a container that has a 512 MB RAM cap without GOMEMLIMIT docker run -m 512m pocketbase serve ... # → OOM kill during large file upload because Go GC doesn't respect cgroup limits # Store SMTP password and S3 secret as plain JSON in pb_data/data.db pocketbase serve # no --encryptionEnv # → Anyone who obtains the database backup can read all credentials ``` **Correct:** ```bash # 1. Raise the open-file limit before starting (Linux/macOS) # Check current limit first: ulimit -a | grep "open files" # Temporarily raise to 4096 for the current session: ulimit -n 4096 /root/pb/pocketbase serve yourdomain.com # Or persist it via systemd (recommended for production): # /lib/systemd/system/pocketbase.service # [Service] # LimitNOFILE = 4096 # ... # 2. Cap Go's soft memory target on memory-constrained hosts # (instructs the GC to be more aggressive before the kernel OOM-kills the process) GOMEMLIMIT=512MiB /root/pb/pocketbase serve yourdomain.com # 3. Encrypt application settings at rest # Generate a random 32-character key once: export PB_ENCRYPTION_KEY="z76NX9WWiB05UmQGxw367B6zM39T11fF" # Start with the env-var name (not the value) as the flag argument: pocketbase serve --encryptionEnv=PB_ENCRYPTION_KEY ``` **Docker deployment pattern (v0.36.8):** ```dockerfile FROM alpine:latest ARG PB_VERSION=0.36.8 RUN apk add --no-cache unzip ca-certificates ADD https://github.com/pocketbase/pocketbase/releases/download/v${PB_VERSION}/pocketbase_${PB_VERSION}_linux_amd64.zip /tmp/pb.zip RUN unzip /tmp/pb.zip -d /pb/ # Uncomment to bundle pre-written migrations or hooks: # COPY ./pb_migrations /pb/pb_migrations # COPY ./pb_hooks /pb/pb_hooks EXPOSE 8080 # Mount a volume at /pb/pb_data to persist data across container restarts CMD ["/pb/pocketbase", "serve", "--http=0.0.0.0:8080"] ``` ```yaml # docker-compose.yml services: pocketbase: build: . ports: - "8080:8080" volumes: - pb_data:/pb/pb_data environment: GOMEMLIMIT: "512MiB" PB_ENCRYPTION_KEY: "${PB_ENCRYPTION_KEY}" command: ["/pb/pocketbase", "serve", "--http=0.0.0.0:8080", "--encryptionEnv=PB_ENCRYPTION_KEY"] volumes: pb_data: ``` **Quick-reference checklist:** | Concern | Fix | |---------|-----| | `Too many open files` errors | `ulimit -n 4096` (or `LimitNOFILE=4096` in systemd) | | OOM kill on constrained host | `GOMEMLIMIT=512MiB` env var | | Credentials visible in DB backup | `--encryptionEnv=YOUR_VAR` with a 32-char random key | | Persistent data in Docker | Mount volume at `/pb/pb_data` | Reference: [Going to production](https://pocketbase.io/docs/going-to-production/) ## 6. Optimize SQLite for Production **Impact: LOW-MEDIUM (Better performance and reliability for SQLite database)** PocketBase uses SQLite with optimized defaults. Understanding its characteristics helps optimize performance and avoid common pitfalls. PocketBase uses two separate databases: `data.db` (application data) and `auxiliary.db` (logs and ephemeral data), which reduces write contention. **Incorrect (ignoring SQLite characteristics):** ```javascript // Heavy concurrent writes - SQLite bottleneck async function bulkInsert(items) { // Parallel writes cause lock contention await Promise.all(items.map(item => pb.collection('items').create(item) )); } // Not using transactions for batch operations async function updateMany(items) { for (const item of items) { await pb.collection('items').update(item.id, item); } // Each write is a separate transaction - slow! } // Large text fields without consideration const schema = [{ name: 'content', type: 'text' // Could be megabytes - affects all queries }]; ``` **Correct (SQLite-optimized patterns):** ```javascript // Use batch operations for multiple writes async function bulkInsert(items) { const batch = pb.createBatch(); items.forEach(item => { batch.collection('items').create(item); }); await batch.send(); // Single transaction, much faster } // Batch updates async function updateMany(items) { const batch = pb.createBatch(); items.forEach(item => { batch.collection('items').update(item.id, item); }); await batch.send(); } // For very large batches, chunk them async function bulkInsertLarge(items, chunkSize = 100) { for (let i = 0; i < items.length; i += chunkSize) { const chunk = items.slice(i, i + chunkSize); const batch = pb.createBatch(); chunk.forEach(item => batch.collection('items').create(item)); await batch.send(); } } ``` **Schema considerations:** ```javascript // Separate large content into dedicated collection const postsSchema = [ { name: 'title', type: 'text' }, { name: 'summary', type: 'text', options: { maxLength: 500 } }, { name: 'author', type: 'relation' } // Content in separate collection ]; const postContentsSchema = [ { name: 'post', type: 'relation', required: true }, { name: 'content', type: 'editor' } // Large HTML content ]; // Fetch content only when needed async function getPostList() { return pb.collection('posts').getList(1, 20); // Fast, no content } async function getPostWithContent(id) { const post = await pb.collection('posts').getOne(id); const content = await pb.collection('post_contents').getFirstListItem( pb.filter('post = {:id}', { id }) ); return { ...post, content: content.content }; } ``` **PocketBase default PRAGMA settings:** PocketBase already configures optimal SQLite settings. You do not need to set these manually unless using a custom SQLite driver: ```sql PRAGMA busy_timeout = 10000; -- Wait 10s for locks instead of failing immediately PRAGMA journal_mode = WAL; -- Write-Ahead Logging: concurrent reads during writes PRAGMA journal_size_limit = 200000000; -- Limit WAL file to ~200MB PRAGMA synchronous = NORMAL; -- Balanced durability/performance (safe with WAL) PRAGMA foreign_keys = ON; -- Enforce relation integrity PRAGMA temp_store = MEMORY; -- Temp tables in memory (faster sorts/joins) PRAGMA cache_size = -32000; -- 32MB page cache ``` WAL mode is the most impactful setting -- it allows multiple concurrent readers while a single writer is active, which is critical for PocketBase's concurrent API request handling. **Index optimization:** ```sql -- Create indexes for commonly filtered/sorted fields CREATE INDEX idx_posts_author ON posts(author); CREATE INDEX idx_posts_created ON posts(created DESC); CREATE INDEX idx_posts_status_created ON posts(status, created DESC); -- Verify indexes are being used EXPLAIN QUERY PLAN SELECT * FROM posts WHERE author = 'xxx' ORDER BY created DESC; -- Should show: "USING INDEX idx_posts_author" ``` **SQLite limitations and workarounds:** | Limitation | Workaround | |------------|------------| | Single writer | Use batch operations, queue writes | | No full-text by default | Use view collections with FTS5 | | File-based | SSD storage, avoid network mounts | | Memory for large queries | Pagination, limit result sizes | **Performance monitoring:** ```javascript // Monitor slow queries via hooks (requires custom PocketBase build) // Or use SQLite's built-in profiling // From sqlite3 CLI: // .timer on // SELECT * FROM posts WHERE author = 'xxx'; // Run Time: real 0.003 user 0.002 sys 0.001 // Check database size // ls -lh pb_data/data.db // Vacuum to reclaim space after deletes // sqlite3 pb_data/data.db "VACUUM;" ``` **When to consider alternatives:** Consider migrating from single PocketBase if: - Write throughput consistently > 1000/sec needed - Database size > 100GB - Complex transactions across tables - Multi-region deployment required **Custom SQLite driver (advanced):** PocketBase supports custom SQLite drivers via `DBConnect`. The CGO driver (`mattn/go-sqlite3`) can offer better performance for some workloads and enables extensions like ICU and FTS5. This requires a custom PocketBase build: ```go // main.go (custom PocketBase build with CGO driver) package main import ( "github.com/pocketbase/dbx" "github.com/pocketbase/pocketbase" _ "github.com/mattn/go-sqlite3" // CGO SQLite driver ) func main() { app := pocketbase.NewWithConfig(pocketbase.Config{ // Called twice: once for data.db, once for auxiliary.db DBConnect: func(dbPath string) (*dbx.DB, error) { return dbx.Open("sqlite3", dbPath) }, }) if err := app.Start(); err != nil { panic(err) } } // Build with: CGO_ENABLED=1 go build ``` Note: CGO requires C compiler toolchain and cannot be cross-compiled as easily as pure Go. **Scaling options:** 1. **Read replicas**: Litestream for SQLite replication 2. **Sharding**: Multiple PocketBase instances by tenant/feature 3. **Caching**: Redis/Memcached for read-heavy loads 4. **Alternative backend**: If requirements exceed SQLite, evaluate PostgreSQL-based frameworks Reference: [PocketBase Going to Production](https://pocketbase.io/docs/going-to-production/)