income claims, fixed costs, data import batches). The Next.js app and the
MCP server both read/write it directly; there is no intermediate service.
2.**PocketBase itself does the file storage** for purchase-order PDFs and
delivery-note slip images. Routes stream those bytes in and out via the
PB admin client.
3.**The browser never talks to PocketBase directly in production.** Every PB
call from a server component or API route goes through `pbAdmin()` (see
§3). The browser-side `pb` client in `src/lib/pb.ts:36` exists for
completeness but no dashboard page uses it today.
---
## 2. The two PocketBase clients
`src/lib/pb.ts` is the single source of truth for all PocketBase access. It
exports two clients and you must use the right one for the job.
| Client | Where it runs | Auth | Used by |
| --- | --- | --- | --- |
| `pb` (`src/lib/pb.ts:36`) | Browser | Persisted JWT in `localStorage` (key `mega-pb-auth`) | Currently unused — kept for future client-side use |
| `pbAdmin()` (`src/lib/pb.ts:74`) | Server only | Superuser (email + password **or** preset token), cached in module scope | Every server component, every API route, the MCP server |
**Why this matters.** `pbAdmin()` authenticates as the PocketBase
**superuser**. That means the application's backend has read/write access to
every collection, irrespective of the per-collection API rules in
`scripts/pb-relax-rules.py`. The relaxed rules there are a defence-in-depth
measure that only constrains *direct* browser-to-PB access — which we do not
do. See [operations.md §3](./operations.md#3-pocketbase-api-rules) for the
exact rule set.
**Auto-cancellation off on `pbAdmin()`.** The SDK cancels in-flight requests
when a new request shares the same `(method, URL-path)` key. On the server,
the module-level client cache means every concurrent route handler shares
**one** PocketBase client, so the second concurrent request to the same
endpoint would abort the first and produce cascading `AbortError` 500s.
The code disables auto-cancel via a typed double-cast
(`src/lib/pb.ts:102`) — see the long comment at `src/lib/pb.ts:88-101` for
the full story.
The dashboard pages that fan out parallel reads also pass
`$autoCancel: false` explicitly on their own calls (e.g.
`src/app/dashboard/page.tsx:71` for the plant count, `src/app/dashboard/purchasing/page.tsx:22-25` for the needs-review DNS lookup). This is the
defensive belt-and-braces for the same problem.
---
## 3. Rendering model
Every dashboard page is a React server component (`export const dynamic = "force-dynamic"` is set on every page). Data fetching happens in
the page body before render; the only client work is the sidebar
(`src/components/layout/sidebar.tsx`, `"use client"`) and the few interactive
forms (chat, import buttons, inline edit).
| Surface | Rendering | Data fetching |
| --- | --- | --- |
| `/login` | Static + client hydration | None |
| `/dashboard` (Overview) | Server component | Parallel `getList` + `getFullList` via `Promise.all` in `loadOverview()` |
| `/dashboard/sites` | Server component | Seven parallel `getFullList` calls in `src/app/dashboard/sites/page.tsx:24-50` |