Claude Code for PocketBase: Self-Hosted Backend with Collections and Auth — Claude Skills 360 Blog
Blog / Backend / Claude Code for PocketBase: Self-Hosted Backend with Collections and Auth
Backend

Claude Code for PocketBase: Self-Hosted Backend with Collections and Auth

Published: January 31, 2027
Read time: 8 min read
By: Claude Skills 360

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.

Keep Reading

Backend

Claude Code for Bun: Fast JavaScript Runtime and Toolkit

Build with Bun and Claude Code — Bun.serve for HTTP servers, Bun.file for fast file I/O, Bun.$ for shell commands, Bun.sql for SQLite and PostgreSQL, Bun.build for bundling, bun:test for testing, Bun.hash for hashing, bun.lock for deterministic installs, bun run for package.json scripts, hot reloading with --hot, bun init for project scaffolding, and compatibility with Node.js modules.

6 min read Jun 13, 2027
Backend

Claude Code for Express.js Advanced: Patterns for Production APIs

Advanced Express.js patterns with Claude Code — typed request handlers with RequestHandler generics, async error handling middleware, Zod validation middleware factory, rate limiting with express-rate-limit and Redis store, helmet security middleware, compression, dependency injection with tsyringe, file upload with multer and S3, pagination utilities, JWT middleware, and structured logging with pino.

6 min read Jun 8, 2027
Backend

Claude Code for KeystoneJS: Node.js CMS and App Framework

Build full-stack apps with KeystoneJS and Claude Code — config with lists, fields.text and fields.relationship for schema definition, access control with isAuthenticated and isAdmin functions, hooks with beforeOperation and afterOperation, GraphQL API auto-generation from schema, AdminUI for content management, session with statelessSessions, Prisma adapter for database, file storage with images and files fields, and custom REST endpoints.

6 min read Jun 7, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free