Files
shiftcraft/pocketbase/CLAUDE.md
2026-04-17 23:26:01 +00:00

6.3 KiB

CLAUDE.md — pocketbase/

Runtime Environment

PocketBase hooks (pb_hooks/*.pb.js) execute in PocketBase's embedded JSVM — not Node.js, not Deno, not browser JS. Available APIs are PocketBase-specific:

  • $app — app instance (findFirstRecordByFilter, findFirstRecordByData, findRecordsByFilter, save, delete, etc.)
  • $os.getenv(key) — read environment variables
  • $http.send({url, method, headers, body, timeout}) — make HTTP requests
  • $template.loadFiles(path) / $template.loadString(str) — render Go templates
  • $security.randomString(n) — generate random strings
  • routerAdd(method, path, handler, ...middlewares) — register custom API endpoints
  • onMailerRecord*Send — intercept outgoing emails
  • onRecordAfterCreateSuccess — post-creation hooks
  • onRecordRequestOTPRequest — OTP request hook
  • e.next() — continue the middleware chain (required in hooks)
  • require(path) — load other hook files (CommonJS-style)
  • __hooks — absolute path to the pb_hooks/ directory

Reference: https://pocketbase.io/docs/js-overview/

No import, no npm packages. Use e.app.logger().error(...) for error logging.

Migrations

Files in pb_migrations/ run in order on PocketBase startup. Naming: {number}_{description}.js.

Each migration exports migrate(up, down). The down function reverts the change.

Current migrations:

File What it does
0_setup.js SMTP config (SMTP_HOST, SMTP_PORT), app meta (APP_NAME, POCKETBASE_URL), OAuth2 providers (Google, Apple), OTP (6 digits, 300s), creates superuser from SUPERUSER_EMAIL/SUPERUSER_PW
1_users_language.js Adds language select field ('de', 'en') to users collection
2_notifications.js Creates notifications collection
3_fcm_tokens.js Creates fcm_tokens collection
4_counters.js Creates counters collection

To add a new collection: Create the next numbered migration. Use $app.save(collection) pattern from existing migrations. After adding, run pnpm pocketbase:types from the project root.

Collection Rules

Collection listRule viewRule createRule updateRule deleteRule
notifications userId = @request.auth.id userId = @request.auth.id null userId = @request.auth.id null
fcm_tokens userId = @request.auth.id userId = @request.auth.id userId = @request.auth.id null null
counters userId = @request.auth.id userId = @request.auth.id userId = @request.auth.id userId = @request.auth.id userId = @request.auth.id

Hooks

File Trigger Purpose
createOtpUser.pb.js onRecordRequestOTPRequest on users Auto-creates user with random password if none exists; reads language header to set user language
mailTemplates.pb.js All 5 onMailerRecord*Send events Replaces default PocketBase emails with localized HTML templates
resetCounter.pb.js POST /counter/reset (requires auth) Resets counter to 0, creates notification with localized title/body
sendFcmPush.pb.js onRecordAfterCreateSuccess on notifications Looks up user's FCM tokens, POSTs to sidecar /notify

Email Templates

HTML templates in pb_hooks/templates/{lang}/ use Go template syntax with lowercase variables:

  • {{.password}} — OTP code or reset token
  • {{.appName}} — from app.settings().meta.appName
  • {{.appUrl}} — from app.settings().meta.appUrl
  • Plus any fields from e.meta

Mail subjects are in pb_hooks/locales/{lang}.json under mailSubject.*. The key for password reset is passwortReset (consistent across all locale files and template filenames).

Template types: authAlert, emailChange, otp, passwortReset, verification

The rendering pipeline (utils/renderMailTemplate.js):

  1. Reads e.record.getString('language'), falls back to 'en'
  2. Loads locales/{lang}.json, renders subject with $template.loadString()
  3. Loads templates/{lang}/{type}.html, renders HTML with $template.loadFiles()
  4. Sets e.message.subject and e.message.html, then calls e.next()

Sidecar (Deno)

The sidecar/ directory is a standalone Deno HTTP service (port 8091) that handles FCM push notification delivery. It authenticates with Google Cloud using a cached JWT from a service account (GOOGLE_CREDENTIALS_JSON env var). Access tokens are cached and refreshed automatically before expiry.

Endpoint: POST /notify

Request:

{ "tokens": ["fcm-token-1"], "title": "Hello", "body": "World", "data": {} }
  • Requires x-sidecar-secret: <SIDECAR_SECRET> header
  • data is optional (Record<string, string>)
  • Returns { successCount, failureCount }

PocketBase sends to $os.getenv('SIDECAR_URL') + '/notify' (defaults to http://localhost:8091). In Docker Compose this is http://sidecar:8091.

Docker Compose Services (local dev)

docker compose up
Service Config Details
pocketbase Build target dev (Alpine + PocketBase binary only) Port 8090; volumes mount pb_data/, pb_migrations/, pb_hooks/ to /pb/ for live reload
sidecar image: denoland/deno:latest + volume mount Port 8091; runs deno task dev (watch mode) against ./sidecar mounted to /app
mailpit image: axllent/mailpit Port 8025; catches all SMTP

PocketBase admin dashboard: http://localhost:8090/_/

Note: The dev stage does not include Deno. In dev, the sidecar runs as a separate container using the official Deno image with a live volume mount — not from a built image.

Production Dockerfile Stages

Stage Base Purpose
dev alpine:latest Downloads PocketBase binary only
build denoland/deno:latest Copies sidecar/, runs deno install to pre-cache dependencies
prod denoland/deno:latest Copies PocketBase binary from dev, pre-cached sidecar from build, plus migrations/hooks; runs start.sh

In prod, all files live under /app/ (working directory). start.sh starts PocketBase in the background (./pocketbase serve --http=0.0.0.0:8090) and then runs cd sidecar && deno task start as the foreground process.