getCurrentUser() only decoded the pb_auth cookie locally and trusted its embedded role/client/active fields directly — the PocketBase JS SDK holds no secret to check a JWT signature against, so a completely forged cookie (garbage signature, role: "owner", any email) was accepted as a real session. Verified exploitable end-to-end: a raw Cookie header against the live app, no browser and no password, rendered the owner's home page. This mattered most for actions writing through the superuser connection (log-work.ts, summarise.ts, ingest.ts, and critically users.ts for account creation) — those never route the caller's token through PocketBase's own rule engine at all, so the app-level role check was the ONLY gate, and it was reading an unverified claim. Actions using the caller's own token (cards.ts, projects.ts) still made the wrong authorization DECISION from the forged role, even though PocketBase's rule engine would have rejected the actual data write. Fixed by having getCurrentUser() call authRefresh(), which asks PocketBase to confirm the token is genuine; a forged or tampered token is refused there and treated as signed-out. Wrapped in React's cache() so the extra round-trip happens once per request, not once per call site (several pages call this more than once). Re-fetching the live record also means a deactivated account (`active: false`) stops working on its very next request instead of only being blocked at a new login. Regression test builds the exact forged cookie used to verify this, confirmed red against the vulnerable code and green against the fix. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|---|---|---|
| .superpowers/brainstorm/24862-1789651152 | ||
| docs/superpowers | ||
| pocketbase/pb_migrations | ||
| public | ||
| scripts | ||
| src | ||
| tests/integration | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| Dockerfile | ||
| next.config.ts | ||
| package-lock.json | ||
| package.json | ||
| postcss.config.mjs | ||
| README.md | ||
| tsconfig.json | ||
| vitest.config.ts | ||
Client Board
Project tracking for client work. A developer runs their own board across several client companies; each client's manager signs in, sees only their own projects, and sets the priorities and target dates that matter to them.
Next.js 16 + PocketBase.
There is no local PocketBase
This project uses one PocketBase instance, hosted on Coolify. There is no
local server to start and no local database to keep in sync. Point
NEXT_PUBLIC_PB_URL at the hosted instance and run the app against it.
That means every developer and every test run shares one database. See "Tests touch real data" below before running the integration suite.
Setup
npm install
cp .env.example .env.local
Fill in .env.local:
| Variable | Notes |
|---|---|
NEXT_PUBLIC_PB_URL |
The Coolify PocketBase URL. Public by design — the browser connects to it directly. |
PB_SUPERUSER_EMAIL |
Server-side only. Never exposed to the browser. |
PB_SUPERUSER_PASSWORD |
Server-side only. Never commit this. |
.env.local is gitignored. .env.example is not — it is tracked, so
anything written there is published to every clone and stays in history. Put
placeholders in it, never real credentials.
Running
npm run dev
Applying the schema
npm run setup-remote
scripts/setup-remote.ts is the single source of truth for the schema. It
creates every collection and applies the API rules against the hosted instance
through the admin API. It is idempotent and non-destructive: re-running reuses
existing collections and never deletes anything.
pocketbase/pb_migrations/ holds the same schema as PocketBase migration
files. They are reference only and never execute — migrations run on
server start, and this project does not start a PocketBase server. If you
change the schema, change scripts/setup-remote.ts. Editing a migration file
does nothing at all.
Seeding demo data
npm run seed
Creates one client, one project with its default columns, an owner account, a client-manager account, and a few cards.
Tests
npm test # unit — pure logic, no network
npm run test:integration # requires npm run dev to be running
npm run test:all
Tests touch real data. The integration suite signs in against the hosted PocketBase, creates and deletes a throwaway client and project, and changes priorities on seeded cards. That is fine while the database holds demo data only. Before a real client's board lives there, stand up a second PocketBase instance for tests — otherwise a test run edits their project.
Permissions, in one paragraph
PocketBase API rules are record-level and cannot express "this user may update
priority but not column". So client managers are denied all writes at
the database, and their edits go through /api/client/* route handlers that
authenticate as a superuser and copy across only whitelisted fields. The owner
writes through /api/owner/* using their own token, because the auth cookie is
httpOnly and the browser SDK therefore has no credentials. If you find yourself
relaxing a write rule to make a form work, that form should be posting to a
route handler instead.
Specs and plans
docs/superpowers/specs/— design documentsdocs/superpowers/plans/— implementation plans