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 stringsrouterAdd(method, path, handler, ...middlewares)— register custom API endpointsonMailerRecord*Send— intercept outgoing emailsonRecordAfterCreateSuccess— post-creation hooksonRecordRequestOTPRequest— OTP request hooke.next()— continue the middleware chain (required in hooks)require(path)— load other hook files (CommonJS-style)__hooks— absolute path to thepb_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}}— fromapp.settings().meta.appName{{.appUrl}}— fromapp.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):
- Reads
e.record.getString('language'), falls back to'en' - Loads
locales/{lang}.json, renders subject with$template.loadString() - Loads
templates/{lang}/{type}.html, renders HTML with$template.loadFiles() - Sets
e.message.subjectande.message.html, then callse.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 datais 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.