PocketBase ships as a single Go binary with a SQLite database, REST API, real-time subscriptions, file storage, and authentication. Collections define the schema — fields, API rules, and indexes — through the admin UI or JSON definition. The JavaScript SDK provides type-safe CRUD operations and real-time subscribe() for live updates. API rules use PocketBase’s filter syntax to enforce row-level security without custom server code. Extend with Go using OnRecordBeforeCreateRequest and other hooks for business logic. A single ./pocketbase serve command runs the complete backend. Claude Code generates PocketBase collection schemas, JavaScript SDK integration, real-time subscription patterns, custom hook implementations, and the Docker configurations for self-hosted PocketBase deployments.
CLAUDE.md for PocketBase Projects
## PocketBase Stack
- Version: pocketbase >= 0.23, pocketbase JS SDK >= 0.21
- Schema: define collections in pb_schema.json or via Admin UI
- SDK: import PocketBase from 'pocketbase' — pb.collection("orders").getList/create/update/delete
- Auth: pb.collection("users").authWithPassword() — returns token + user record
- Realtime: pb.collection("orders").subscribe("*", callback) for live updates
- Rules: filter expresses in collection API rules — @request.auth.id, @collection.users, etc.
- Extend: main.go with app.OnRecordBeforeCreateRequest().Add(handler) hooks
- Deploy: single binary + pb_data directory — or Docker with volume mount
JavaScript SDK Setup
// lib/pocketbase.ts — PocketBase client
import PocketBase from "pocketbase"
export const pb = new PocketBase(
process.env.NEXT_PUBLIC_POCKETBASE_URL || "http://localhost:8090"
)
// Type-safe collection records
export interface Order {
id: string
collectionId: string
collectionName: string
created: string
updated: string
customer_id: string
status: "pending" | "processing" | "shipped" | "delivered" | "cancelled"
total_cents: number
items: OrderItem[]
tracking_number: string
// Expanded relations (when using expand param)
expand?: {
customer_id?: User
}
}
export interface User {
id: string
email: string
name: string
avatar: string
}
export type OrderCreate = Omit<Order,
"id" | "collectionId" | "collectionName" | "created" | "updated" | "expand"
>
// Restore auth from localStorage on page load
if (typeof window !== "undefined") {
pb.authStore.loadFromCookie(document.cookie)
}
CRUD Operations
// lib/orders.ts — type-safe PocketBase CRUD
import { pb, Order, OrderCreate } from "./pocketbase"
import type { ListResult } from "pocketbase"
export async function listOrders(options: {
page?: number
perPage?: number
status?: string
customerId?: string
} = {}): Promise<ListResult<Order>> {
const { page = 1, perPage = 20, status, customerId } = options
const filters: string[] = []
if (status) filters.push(`status = "${status}"`)
if (customerId) filters.push(`customer_id = "${customerId}"`)
return await pb.collection("orders").getList<Order>(page, perPage, {
filter: filters.join(" && "),
sort: "-created",
expand: "customer_id",
})
}
export async function getOrder(orderId: string): Promise<Order> {
return await pb.collection("orders").getOne<Order>(orderId, {
expand: "customer_id",
})
}
export async function createOrder(data: OrderCreate): Promise<Order> {
return await pb.collection("orders").create<Order>(data)
}
export async function updateOrderStatus(
orderId: string,
status: Order["status"]
): Promise<Order> {
return await pb.collection("orders").update<Order>(orderId, { status })
}
export async function deleteOrder(orderId: string): Promise<void> {
await pb.collection("orders").delete(orderId)
}
Authentication
// lib/auth.ts — PocketBase authentication
import { pb } from "./pocketbase"
export async function signIn(email: string, password: string) {
const authData = await pb.collection("users").authWithPassword(email, password)
// Save token as cookie for SSR
if (typeof document !== "undefined") {
document.cookie = pb.authStore.exportToCookie({ httpOnly: false })
}
return authData
}
export async function signUp(email: string, password: string, name: string) {
const user = await pb.collection("users").create({
email,
password,
passwordConfirm: password,
name,
})
// Auto-login after registration
await signIn(email, password)
return user
}
export function signOut() {
pb.authStore.clear()
if (typeof document !== "undefined") {
document.cookie = pb.authStore.exportToCookie({ httpOnly: false })
}
}
export async function uploadAvatar(file: File) {
const formData = new FormData()
formData.append("avatar", file)
return await pb.collection("users").update(pb.authStore.model!.id, formData)
}
export function getCurrentUser() {
return pb.authStore.isValid ? pb.authStore.model : null
}
Real-Time Subscriptions
// hooks/useOrdersRealtime.ts — live order updates
import { useEffect, useState, useRef } from "react"
import { pb, Order } from "@/lib/pocketbase"
export function useOrdersRealtime(customerId: string) {
const [orders, setOrders] = useState<Order[]>([])
const [loading, setLoading] = useState(true)
const unsubscribeRef = useRef<(() => void) | null>(null)
useEffect(() => {
// Initial fetch
pb.collection("orders")
.getList<Order>(1, 50, {
filter: `customer_id = "${customerId}"`,
sort: "-created",
})
.then(result => {
setOrders(result.items)
setLoading(false)
})
// Subscribe to real-time changes for this customer's orders
pb.collection("orders")
.subscribe<Order>(
"*",
(event) => {
if (event.record.customer_id !== customerId) return
if (event.action === "create") {
setOrders(prev => [event.record, ...prev])
} else if (event.action === "update") {
setOrders(prev =>
prev.map(o => o.id === event.record.id ? event.record : o)
)
} else if (event.action === "delete") {
setOrders(prev => prev.filter(o => o.id !== event.record.id))
}
},
{ filter: `customer_id = "${customerId}"` }
)
.then(unsubscribe => {
unsubscribeRef.current = unsubscribe
})
return () => {
unsubscribeRef.current?.()
}
}, [customerId])
return { orders, loading }
}
Collection Schema JSON
[
{
"name": "orders",
"type": "base",
"schema": [
{ "name": "customer_id", "type": "relation",
"options": { "collectionId": "_pb_users_auth_", "maxSelect": 1 }
},
{ "name": "status", "type": "select",
"options": { "values": ["pending","processing","shipped","delivered","cancelled"] }
},
{ "name": "total_cents", "type": "number", "required": true },
{ "name": "items", "type": "json", "required": true },
{ "name": "tracking_number", "type": "text" }
],
"listRule": "@request.auth.id != '' && @request.auth.id = customer_id",
"viewRule": "@request.auth.id != '' && @request.auth.id = customer_id",
"createRule": "@request.auth.id != ''",
"updateRule": "@request.auth.id != '' && @request.auth.id = customer_id",
"deleteRule": null
}
]
Extend with Go Hooks
// main.go — PocketBase with custom hooks
package main
import (
"log"
"net/http"
"github.com/pocketbase/pocketbase"
"github.com/pocketbase/pocketbase/core"
)
func main() {
app := pocketbase.New()
// Hook: validate and enrich order before creation
app.OnRecordBeforeCreateRequest("orders").Add(func(e *core.RecordCreateEvent) error {
record := e.Record
// Ensure total matches items
items := record.GetStringSlice("items")
if len(items) == 0 {
return apis.NewBadRequestError("Order must have at least one item", nil)
}
// Set initial status
record.Set("status", "pending")
return nil
})
// Hook: send notification after order status change
app.OnRecordAfterUpdateRequest("orders").Add(func(e *core.RecordUpdateEvent) error {
oldStatus := e.Record.OriginalCopy().GetString("status")
newStatus := e.Record.GetString("status")
if oldStatus != newStatus && newStatus == "shipped" {
customerId := e.Record.GetString("customer_id")
go sendShippedNotification(app, customerId, e.Record.Id)
}
return nil
})
// Custom API endpoint
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
e.Router.GET("/api/custom/order-stats", func(c echo.Context) error {
// Admin-only custom endpoint
admin, _ := c.Get(apis.ContextAdminKey).(*models.Admin)
if admin == nil {
return apis.NewForbiddenError("", nil)
}
var stats struct {
TotalOrders int64 `db:"total_orders" json:"total_orders"`
TotalRevenue int64 `db:"total_revenue" json:"total_revenue"`
}
app.Dao().DB().NewQuery(`
SELECT COUNT(*) as total_orders, SUM(total_cents) as total_revenue
FROM orders WHERE status != 'cancelled'
`).One(&stats)
return c.JSON(http.StatusOK, stats)
})
return nil
})
if err := app.Start(); err != nil {
log.Fatal(err)
}
}
For the Supabase alternative that adds PostgreSQL semantics, vector search, and Row Level Security to a similar BaaS model with a larger managed feature set, see the authentication guide for Supabase Auth patterns. For Convex’s reactive document database alternative with TypeScript-first querying and no separate schema JSON, see the Convex guide for query/mutation function patterns. The Claude Skills 360 bundle includes PocketBase skill sets covering collection schemas, SDK integration, and Go hook extensions. Start with the free tier to try PocketBase backend generation.