# 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: ```json { "tokens": ["fcm-token-1"], "title": "Hello", "body": "World", "data": {} } ``` - Requires `x-sidecar-secret: ` header - `data` is optional (`Record`) - 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) ```bash 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.