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

119 lines
6.3 KiB
Markdown

# 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: <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)
```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.