AgentSkillsCN

pocketbase

PocketBase开发技能——项目搭建、架构迭代、JS迁移,以及开发服务器操作。

SKILL.md
--- frontmatter
name: pocketbase
description: PocketBase development skill — project setup, schema iteration, JS migrations, and dev server operations.

Skill: PocketBase

Complete reference for PocketBase development. Covers initial project setup, dev server operations, schema iteration workflow, and JS migration API.

Invocation: The Setup Steps should be run during project bootstrap. The Iteration Workflow and JS Migration Reference are referenced during schema and feature work.

Prerequisites

  • Go 1.23+ installed and on PATH
  • Working directory: Always run go commands from the workspace root using go -C pb. Never cd into pb/ directly.

Setup Steps

Follow these steps in order when setting up a new PocketBase project.

Step 1: Prompt for Configuration

Ask the user for the following values:

VariableDescriptionDefault
PB_PORTPort for PocketBase server8090
PB_ADMIN_EMAILSuperuser email(required)
PB_ADMIN_PASSWORDSuperuser password(required)
PB_MODULE_NAMEGo module nameInfer from project

Step 2: Initialize Project

Run PB Init (see Operations below) with all four configuration values. This creates the pb/ directory structure, writes pb/main.go, creates pb/.env, adds PocketBase entries to .gitignore, initializes the Go module, and installs dependencies:

bash
bash .github/skills/pocketbase/pb-init.sh <PB_MODULE_NAME> <PB_PORT> <PB_ADMIN_EMAIL> <PB_ADMIN_PASSWORD>

Step 3: Verify Setup

Run a PB Reset (see Operations below) to confirm everything works.

Expected outcome:

  • PocketBase compiles and starts
  • Superuser is created
  • Server is accessible at http://127.0.0.1:<PB_PORT>
  • Admin dashboard at http://127.0.0.1:<PB_PORT>/_/

Operations

All operations source pb/.env for PB_PORT, PB_ADMIN_EMAIL, and PB_ADMIN_PASSWORD.

All scripts live in this skill directory and resolve the workspace root automatically via SCRIPT_DIR.

OperationScriptDescription
PB Initbash .github/skills/pocketbase/pb-init.sh <MODULE> <PORT> <EMAIL> <PASS>Full project setup: directories, main.go, pb/.env, .gitignore, Go module, go mod tidy
PB Stopbash .github/skills/pocketbase/pb-stop.shKill existing PocketBase instance on the configured port
PB Devbash .github/skills/pocketbase/pb-dev.shStop existing instance, then start the dev server
PB Resetbash .github/skills/pocketbase/pb-reset.shStop instance, wipe data, create superuser, start fresh

Iteration Workflow

ActionProcedure
Initialize or update Go depsRun PB Init (pass module name on first run)
Start serverRun PB Dev
Wipe DB and restartRun PB Reset
Stop serverRun PB Stop (or Ctrl+C if running in foreground)

Schema Change Loop

The recommended workflow for iterating on schema:

  1. Write or edit migration files in pb/pb_migrations/
  2. Run PB Reset — wipes DB, re-runs all migrations from scratch, creates superuser
  3. Verify — check the admin dashboard at /_/ or hit the API

Migrations run automatically on server start. PB Reset gives a clean slate every time, so you can freely edit migration files and re-test.

Automigrate (Dashboard Mode)

When running via go run (which PB Dev uses), Automigrate is enabled. This means changes made in the admin dashboard (/_/) automatically generate JS migration files in pb_migrations/. These files should be committed to version control.

Hooks

Add JS hooks in pb/pb_hooks/ using the *.pb.js naming pattern. They hot-reload automatically during development.


JS Migration Reference

Migration files live in pb/pb_migrations/ and run in filename order on server start.

File Naming

code
pb_migrations/{unix_timestamp}_{description}.js

Example: pb_migrations/1687801097_create_posts.js

To generate a new migration file via CLI:

bash
go -C pb run . migrate create "description_here"

Migration Structure

javascript
migrate((app) => {
  // UP — apply changes
}, (app) => {
  // DOWN — revert changes (optional but recommended)
})

Both callbacks receive a transactional app instance.

Create a Collection

javascript
migrate((app) => {
  const collection = new Collection({
    type: "base",          // "base", "auth", or "view"
    name: "posts",
    listRule: "@request.auth.id != ''",
    viewRule: "@request.auth.id != ''",
    createRule: "@request.auth.id != ''",
    updateRule: "author = @request.auth.id",
    deleteRule: "author = @request.auth.id",
    fields: [
      new TextField({ name: "title", required: true, max: 200 }),
      new EditorField({ name: "body" }),
      new SelectField({ name: "status", values: ["draft", "published"], maxSelect: 1 }),
      new RelationField({
        name: "author",
        collectionId: "COLLECTION_ID_HERE",
        maxSelect: 1,
        cascadeDelete: false
      }),
      new AutodateField({ name: "created", onCreate: true, onUpdate: false }),
      new AutodateField({ name: "updated", onCreate: true, onUpdate: true }),
    ],
    indexes: [
      "CREATE INDEX idx_posts_status ON posts (status)",
    ],
  })
  app.save(collection)
}, (app) => {
  const collection = app.findCollectionByNameOrId("posts")
  app.delete(collection)
})

Create an Auth Collection

javascript
migrate((app) => {
  const collection = new Collection({
    type: "auth",
    name: "users",
    listRule: "id = @request.auth.id",
    viewRule: "id = @request.auth.id",
    createRule: "",
    updateRule: "id = @request.auth.id",
    deleteRule: null,
    fields: [
      new TextField({ name: "displayName", max: 100 }),
    ],
    passwordAuth: { enabled: true },
  })
  app.save(collection)
}, (app) => {
  const collection = app.findCollectionByNameOrId("users")
  app.delete(collection)
})

Modify an Existing Collection

javascript
migrate((app) => {
  const collection = app.findCollectionByNameOrId("posts")

  // Add a field
  collection.fields.add(new BoolField({ name: "featured" }))

  // Remove a field
  collection.fields.removeByName("old_field")

  // Modify a field
  const titleField = collection.fields.getByName("title")
  titleField.max = 500

  // Update API rules
  collection.listRule = ""

  app.save(collection)
}, (app) => {
  const collection = app.findCollectionByNameOrId("posts")
  collection.fields.removeByName("featured")
  collection.listRule = "@request.auth.id != ''"
  app.save(collection)
})

Relation Lookup by Name

When creating relations, you need the target collection's ID. Look it up by name:

javascript
migrate((app) => {
  const users = app.findCollectionByNameOrId("users")

  const collection = new Collection({
    type: "base",
    name: "posts",
    fields: [
      new TextField({ name: "title", required: true }),
      new RelationField({
        name: "author",
        collectionId: users.id,     // resolved at migration time
        maxSelect: 1,
        cascadeDelete: false,
      }),
    ],
  })
  app.save(collection)
})

Seed Data in Migrations

javascript
migrate((app) => {
  const collection = app.findCollectionByNameOrId("categories")
  for (const name of ["Work", "Personal", "Shopping"]) {
    const record = new Record(collection)
    record.set("name", name)
    app.save(record)
  }
})

Raw SQL

javascript
migrate((app) => {
  app.db().newQuery("UPDATE posts SET status = 'draft' WHERE status = ''").execute()
})

Field Types Quick Reference

ConstructorKey Options
TextFieldrequired, min, max, pattern
NumberFieldrequired, min, max, onlyInt
BoolFieldrequired
EmailFieldrequired, onlyDomains, exceptDomains
URLFieldrequired, onlyDomains, exceptDomains
DateFieldrequired
AutodateFieldonCreate, onUpdate
SelectFieldvalues (required), maxSelect
FileFieldmaxSelect, maxSize, mimeTypes, thumbs, protected
RelationFieldcollectionId (required), maxSelect, cascadeDelete
JSONFieldrequired (nullable unlike other fields)
EditorFieldrequired, maxSize, convertURLs
PasswordFieldrequired, min, max, cost
GeoPointFieldrequired

API Rules Quick Reference

ValueMeaning
nullSuperuser only (locked)
""Public access (no auth required)
"@request.auth.id != ''"Any authenticated user
"author = @request.auth.id"Owner only (field author matches current user)
"@request.auth.verified = true"Verified users only

Rules support: =, !=, >, >=, <, <=, ~ (contains), !~, &&, ||

For multi-value relation checks use ?=: "members ?= @request.auth.id"