PocketBase v1.0: Single-File Backend That Actually Handles Production Traffic
I was paying Firebase $200/month for an app with 15K users. The bill kept creeping up because of Firestore reads — a single list view could trigger 500 reads. PocketBase v1.0 replaced Firebase entirely: auth, database, file storage, real-time subscriptions — all in one 20MB Go binary backed by SQLite. Monthly cost: $5 for a VPS. Here's how it went.
Why PocketBase Beat Firebase
| Feature | Firebase | PocketBase v1.0 |
|---|---|---|
| Database | Firestore (NoSQL) | SQLite (SQL) |
| Auth | Firebase Auth | Built-in (email, OAuth2) |
| File storage | Cloud Storage | Built-in local storage |
| Real-time | Firestore listeners | SSE subscriptions |
| Custom logic | Cloud Functions | Go hooks |
| Cost (15K users) | $200/month | $5/month VPS |
| Self-hosted | No | Yes |
v1.0 Breaking Changes
PocketBase v1.0 (stable release) brought these key changes from the beta:
- Stable Go API. The hook and middleware APIs are now finalized. No more breaking changes between versions.
- Improved migrations. Automated migration system for schema changes. No more manual SQL for simple changes.
- Better auth providers. OAuth2 providers can be configured via the admin UI. Added Apple, Discord, and GitLab.
- Soft delete. Records can be soft-deleted instead of hard-deleted. Useful for audit trails.
# Download PocketBase v1.0
wget https://github.com/pocketbase/pocketbase/releases/download/v1.0.0/pocketbase_1.0.0_linux_amd64.zip
unzip pocketbase_1.0.0_linux_amd64.zip
# Run (creates data directory automatically)
./pocketbase serve --http=0.0.0.0:8090
# Admin UI: http://localhost:8090/_/
# API: http://localhost:8090/api/
Extending with Go Hooks
This is where PocketBase shines over Firebase. Instead of Cloud Functions in Node.js (cold starts, billing per invocation), you write Go hooks that run in the same process. Zero latency, zero cost per invocation:
// main.go
package main
import (
"log"
"net/http"
"github.com/pocketbase/pocketbase"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/models"
)
func main() {
app := pocketbase.New()
// Hook: after a user signs up, create a profile record
app.OnRecordAfterCreateRequest("users", func(e *core.RecordCreateEvent) error {
profile := models.NewRecord(app.Dao().FindCollectionByNameOrId("profiles"))
profile.Set("user", e.Record.Id)
profile.Set("displayName", e.Record.GetString("email"))
return app.Dao().SaveRecord(profile)
})
// Hook: before creating an order, validate stock
app.OnRecordBeforeCreateRequest("orders", func(e *core.RecordCreateEvent) error {
productId := e.Record.GetString("product")
product, err := app.Dao().FindRecordById("products", productId)
if err != nil {
return err
}
stock := product.GetInt("stock")
qty := e.Record.GetInt("quantity")
if qty > stock {
return fmt.Errorf("only %d items in stock, requested %d", stock, qty)
}
return nil
})
// Hook: after order created, decrement stock and send notification
app.OnRecordAfterCreateRequest("orders", func(e *core.RecordCreateEvent) error {
productId := e.Record.GetString("product")
product, _ := app.Dao().FindRecordById("products", productId)
product.Set("stock", product.GetInt("stock") - e.Record.GetInt("quantity"))
app.Dao().SaveRecord(product)
// Send email notification (inline, no external service)
sendOrderConfirmation(e.Record)
return nil
})
// Custom route
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
e.Router.GET("/api/stats", func(c echo.Context) error {
totalUsers, _ := app.Dao().DB().Select("count(*)").From("users").Build().FetchOne()
totalOrders, _ := app.Dao().DB().Select("count(*)").From("orders").Build().FetchOne()
return c.JSON(http.StatusOK, map[string]any{
"users": totalUsers,
"orders": totalOrders,
})
})
return e.Next()
})
if err := app.Start(); err != nil {
log.Fatal(err)
}
}
# Build the custom binary
go build -o myapp main.go
# Deploy (just copy the binary + data directory)
scp myapp user@server:/opt/myapp/
# Set up systemd service for auto-restart
Problem
At around 50 concurrent users writing simultaneously, PocketBase started returning database is locked errors. SQLite only allows one writer at a time, and the default busy timeout was too short.
Actual Fix
// main.go — tune SQLite for higher concurrency
func main() {
app := pocketbase.New()
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
// Increase busy timeout to 5 seconds
app.Dao().DB().NewQuery("PRAGMA busy_timeout = 5000").Execute()
// Enable WAL mode (allows concurrent reads during writes)
app.Dao().DB().NewQuery("PRAGMA journal_mode = WAL").Execute()
// Set synchronous to NORMAL (faster writes, still safe with WAL)
app.Dao().DB().NewQuery("PRAGMA synchronous = NORMAL").Execute()
return e.Next()
})
app.Start()
}
Real-time Subscriptions for Chat
PocketBase uses Server-Sent Events (SSE) for real-time. Here's a chat app pattern:
// client-side chat with PocketBase JS SDK
import PocketBase from "pocketbase"
const pb = new PocketBase("https://myapp.example.com")
// Subscribe to new messages
pb.collection("messages").subscribe("*", (e) => {
if (e.action === "create") {
appendMessage(e.record)
}
})
// Send a message (also triggers the subscription above)
async function sendMessage(text, channelId) {
await pb.collection("messages").create({
text,
channel: channelId,
sender: pb.authStore.record.id,
})
}
// Unsubscribe on cleanup
function cleanup() {
pb.collection("messages").unsubscribe("*")
}
Scaling SQLite Past 100K Users
SQLite can handle more than most people think. Key optimizations:
- WAL mode is mandatory. Default rollback journal blocks readers during writes. WAL allows concurrent reads.
- Index frequently queried columns. PocketBase auto-indexes id and relation fields. Add indexes manually for your query patterns.
- Paginate everything. Never load all records. PocketBase supports
?page=1&perPage=50. - For read-heavy workloads, add replicas. PocketBase supports Litestream for streaming WAL to S3. Spin up read replicas from the S3 backup.
- The ceiling is around 1M daily active users on a single instance with good hardware. After that, you need to shard or move to PostgreSQL.
What I Learned
- PocketBase is perfect for indie projects and small teams. One binary, one file, full backend. Hard to beat.
- Go hooks are underrated. Zero cold starts, runs in-process, type-safe. Way better than Firebase Cloud Functions.
- SQLite with WAL mode handles surprising scale. My app does 200 writes/second without issues on a $5 VPS.
- The admin UI is good enough for most things. I only write Go code for complex business logic. Simple CRUD is admin UI only.
- Backup strategy matters. Litestream streams WAL to S3 continuously. Set it up from day one.
Wrapping Up
PocketBase v1.0 is the single-file backend I've been waiting for. It replaced Firebase and saved me $200/month. The Go extension model means I'm not limited to simple CRUD — I can write real business logic with zero cold start overhead. For any indie developer or small team building a web or mobile app, PocketBase should be your default backend choice.