diff --git a/docs/superpowers/specs/2026-09-21-phase-9-per-client-intake-links-and-project-picker-design.md b/docs/superpowers/specs/2026-09-21-phase-9-per-client-intake-links-and-project-picker-design.md
new file mode 100644
index 0000000..20e86fe
--- /dev/null
+++ b/docs/superpowers/specs/2026-09-21-phase-9-per-client-intake-links-and-project-picker-design.md
@@ -0,0 +1,499 @@
+# Phase 9 — Per-Client Intake Links + Project Picker
+
+**Date:** 2026-09-21
+**Status:** Draft for review
+**Phase:** 9
+**Depends on:** Phase 8 (public anonymous intake, PWA shell, per-project markdown context)
+
+---
+
+## 1. Purpose
+
+Phase 8 made the public route work end-to-end. The next thing the
+developer asked for after playing with it for an hour is the link shape
+itself: **one URL per client to share, with a project picker in front of
+the wizard**, instead of one URL per project. The reasoning is in the
+thread above this spec; the short version is that human clients do not
+think in projects, they think in companies, and "send us a request —
+`rjb.example.com/intake`" beats "send us a request for the SE-Sprint-2
+migration — `/intake/hkXHJPUS…`" by a mile.
+
+Three things change, and nothing else moves:
+
+1. **The link is bound to a client, not a project.** Each client has a
+ small set of links (typically one) that open a picker page listing the
+ client's active projects.
+2. **The picker page is the new entry.** It is a real, anonymous,
+ authenticated-by-token page that lists projects the client has and
+ asks the user to pick one before the wizard runs.
+3. **The wizard itself does not change.** It still takes a `projectId`
+ and still asks questions and still writes a card. It just gets that
+ `projectId` from the URL of the picker-selected project rather than
+ from the link row.
+
+The security property Phase 8 set up carries forward unchanged: **the
+worst a hostile body achieves is a strangely-worded card in the Inbox**.
+The picker is a UI change to how the link and the wizard connect — it
+does not add a way for a hostile body to escape the parseBrief whitelist
+or the parse-and-server-decide-fields rule.
+
+Out of scope for this spec:
+
+- **Re-introducing per-project links.** The new shape is additive; the
+ old per-project shape stays live. Existing Phase 8 tokens keep
+ working as-is and continue to point at their original project.
+- **Multi-client links.** One token, one client, easy to rotate. A
+ token cannot span clients, today or later.
+- **Reordering intake by client-defined workflow.** Each project still
+ gets its own card; the picker just routes which project's wizard runs.
+- **Renaming projects in the picker to be friendlier than the internal
+ `slug`.** A later phase can add a `client_display_name`; for now the
+ picker shows the project's actual name.
+- **Branch-by-tenant lookup of intake links, analytics, etc.** One
+ PocketBase superuser; one rule set. Per-tenant isolation was deliberately
+ out from Phase 1 onwards.
+
+## 2. What is already settled
+
+Carried forward unchanged from the Phase 8 spec and its ancestors:
+
+| Decision | Where from |
+|---|---|
+| The link is a capability, not a session. Rotation is the threat model. | Phase 8 §3 |
+| The brief is re-parsed through `parseBrief`, which is a whitelist | Phase 4b |
+| Column, priority, `accepted`, `created_by_role` are set by the server, never the body | Phase 1, Phase 4b |
+| Submitted text is data, never instructions | Phase 1 + 3 preamble |
+| The wizard is stateless — the transcript travels in the prompt | Phase 4 |
+| Today's signed-in wizard does not change | Phase 8 / 8 / 8c |
+| Phase 8's "minimal intake layout with no app shell" is the picker page's layout too | Phase 8 Task 9 |
+| The PWA manifest, service worker, and iOS meta already cover any `/intake/*` URL | Phase 8 Tasks 3–5 |
+
+## 3. Threat model for the picker
+
+The Phase 8 threat model is the floor. Three Phase-9-specific concerns
+sit on top of it; I'll name each and what we do about it.
+
+**Concern 1 — Listing too many projects.**
+A picker that dumps every active project a client has leaks work the
+client didn't know about. A picker with too few projects is useless.
+Resolution: each project carries a `show_in_intake: bool` (default
+`false`, owner opts in). The picker queries `project.active AND
+project.show_in_intake` — never the wider set. A confidential project
+("Acme Rebrand — pre-announce") simply isn't in the list until the
+owner says so.
+
+**Concern 2 — Cross-client leakage through the picker.**
+The token resolves to one client. The picker lists *that client's*
+projects. Cross-client is structurally impossible because the picker
+reads projects through the link's `client`, not by enumerating all
+projects.
+
+**Concern 3 — Enumeration via the picker.**
+There is no "is this token valid?" probe because the picker is the
+response to an unknown token: same 404 wording as the wizard in
+Phase 8. The picker cannot be used to test whether a project name
+exists on the platform.
+
+The Phase 8 controls carry forward unchanged:
+- rate limit per `(token, ip)`
+- daily cap (now per `(link, project)` — see §5)
+- token shape check before any DB read
+- 404 not 403 on bad tokens (same wording for unknown/revoked/expired)
+- server-decides-fields (the body never sets `column`, `priority`,
+ `created_by_role`, etc.)
+- `parseBrief` whitelist on the model's output
+
+The token is unchanged: 24 bytes CSPRNG → base64url → 32 chars.
+
+## 4. Data model
+
+Two schema changes, both additive. The Phase 8 `intake_links.project`
+shape stays live alongside the new shape; the two are distinguished by
+which field is populated on the row.
+
+### 4.1 `intake_links.client` (new required field on intake_links)
+
+Replace `intake_links.project` (Phase 8) with `intake_links.client` for
+new rows. The schema definition in `scripts/setup-remote.ts` flips:
+
+```ts
+// scripts/setup-remote.ts — intake_links collection declaration
+fields: [
+ {
+ name: "client",
+ type: "relation",
+ required: true,
+ collectionId: (await pb.collections.getOne("clients")).id,
+ cascadeDelete: true,
+ minSelect: 0,
+ maxSelect: 1,
+ },
+ // project field is gone for new rows. Migration §10 keeps
+ // existing rows intact via a partial-index trick.
+ ...
+],
+indexes: [
+ "CREATE UNIQUE INDEX idx_intake_links_token ON intake_links (token)",
+ "CREATE INDEX idx_intake_links_client ON intake_links (client, active)",
+],
+```
+
+`project` becomes an *optional* field, kept on the collection for the
+migration window (§10). New rows leave it empty. The TypeScript
+`IntakeLinkRecord` becomes:
+
+```ts
+export interface IntakeLinkRecord extends BaseRecord {
+ client: string;
+ /** Migration window only: projects this link binds to directly (Phase 8 shape).
+ * Empty on Phase 9 rows. The wizard reads projects via the client. */
+ project: string;
+ /** The owner's name for this link, e.g. "Acme — till phone". */
+ label: string;
+ token: string;
+ active: boolean;
+ expires_at: string;
+ max_submissions_per_day: number;
+ submission_count: number;
+ // submission_count_today and submission_count_today_date become
+ // per-project for Phase 9 — see §5.
+ ...
+}
+```
+
+### 4.2 `projects.show_in_intake` (new optional bool)
+
+```ts
+{ name: "show_in_intake", type: "bool", required: false }
+```
+
+Default `false`. Owner opts a project in by toggling it on the existing
+project admin page (one new field on `BackgroundMdEditor`'s sibling
+panel — same shape).
+
+### 4.3 API rules (intake_links)
+
+Same as Phase 8 — owner-only at the rule level. The wizard reads
+through the superuser connection; the rule exists so a signed-in client
+manager cannot enumerate links, same as before.
+
+### 4.4 Daily cap, per `(link, project)`
+
+Phase 8 tracked today's submissions on the link row itself:
+`submission_count_today`, `submission_count_today_date`. With the new
+per-client shape, that counter now means "across all projects under
+this link" — which is wrong. One busy project could lock the others
+out at the cap.
+
+Phase 9 splits it: the daily counter moves onto a *new* collection,
+`intake_link_submissions`, one row per (link, project, date):
+
+```ts
+{
+ link: string; // intake_links.id
+ project: string; // project id
+ date: string; // YYYY-MM-DD
+ count: number; // submitted today
+}
+unique index: (link, project, date)
+```
+
+The wizard's daily-cap check (`isLinkUsable`) becomes:
+
+```
+cap = max_submissions_per_day || 100
+for the chosen project: count = today's row, or 0
+return count < cap
+```
+
+Existing per-project Phase 8 links keep using `submission_count_today`
+on the link row for that one project — the field stays, the new
+collection is additive for the new shape only.
+
+### 4.5 What the owner must opt
+
+Owner-side:
+
+1. On each project: flip `show_in_intake` to `true` for the projects
+ they want in the picker. Default off, so a freshly-created project
+ never appears in the picker until explicitly opted in.
+2. On each client: mint at least one intake link from
+ `/admin/clients/[clientId]` (a new page) or via a "New link for this
+ client" button on an existing client-detail page.
+
+The first link minted under the new shape is effectively the default
+"send a request" link. Owners can mint more if they want segmented
+URLs (e.g. one per support channel), but most clients need one.
+
+## 5. Public route
+
+Two URLs, two steps, both unauthenticated and the same minimal
+layout Phase 8 Task 9 wrote.
+
+### 5.1 `/intake/[token]` — the picker
+
+Server component. Resolves the link server-side (same `resolveIntakeLink`
+as Phase 8 — it does not care whether the row carries `project` or
+`client`). On unknown/revoked/expired, **404 with the same wording as
+the wizard** so a hostile body cannot probe.
+
+On success, renders a project picker:
+
+- Reads every project on the link's client with
+ `active = true AND show_in_intake = true`.
+- If zero or one project qualifies, the page skips the picker and
+ behaves like a per-project link:
+ - One project → server-side redirect to `/intake/[token]/p/[projectId]`.
+ - Zero projects → static "no projects enabled" page with the
+ developer's email or phone (also owner-controlled).
+- Two or more → renders a small list (`
` of project names).
+ Each item is a `` to `/intake/[token]/p/[projectId]`.
+
+The picker is the only place a project is exposed to the public. No
+list of clients, no list of all projects, no alphabetical sort key
+that would let a hostile body infer project names by timing.
+
+### 5.2 `/intake/[token]/p/[projectId]` — the wizard
+
+Server component. Same as Phase 8's `/intake/[token]` page, except:
+
+- The token resolves to a link (same as Phase 8).
+- The URL's `projectId` is verified to belong to the link's client
+ AND have `show_in_intake = true`. Mismatch → 404.
+- The wizard runs with that `projectId`.
+
+`IntakeWizard` and `IntakePublic` already accept `projectId` as a
+prop (Phase 4 / Phase 8). Nothing in the wizard itself changes.
+
+### 5.3 Caching and PWA
+
+The Phase 8 service worker already intercepts `/intake/*` HTML
+network-first with an offline fallback. The picker and wizard URLs
+both fall under that pattern. No SW changes needed; the cache key
+naturally covers both paths.
+
+The manifest's `start_url` is unchanged (`/intake/`). A returning PWA
+user lands on the picker first; selecting a project re-uses the
+cached wizard HTML for that project if available.
+
+## 6. Server Action & submission route
+
+Both change shape to carry `projectId`. The change is *additive* to
+the action's inputs — older fields stay for backwards compat with the
+existing Phase 8 wizard code path which never sets `projectId` (the
+link's `project` carries it).
+
+### 6.1 `intakeStepPublicAction`
+
+```ts
+export async function intakeStepPublicAction(input: {
+ token: string;
+ projectId: string; // NEW: required on the new shape. Empty on the
+ // old shape, where the link's project fills in.
+ turns: Turn[];
+ finishNow: boolean;
+}): Promise;
+```
+
+Internally:
+
+1. Resolve `link` from token.
+2. If `projectId` is provided, verify it belongs to `link.client` and
+ has `show_in_intake = true AND active = true`.
+3. If `projectId` is empty, fall back to `link.project` (Phase 8 path).
+4. Rate limit + daily cap (now per `(link, project)`) + token shape —
+ same shape check, same reasons.
+5. `nextStep({ projectId, turns, finishNow })`.
+
+The `clientIpFromHeaders(next/headers())` call from the recent fix
+(`b02ebb7`) carries over unchanged.
+
+### 6.2 `POST /api/public/intake`
+
+```ts
+export async function POST(request: Request): Promise {
+ // Same body whitelisting + token-shape check + 404 handling as Phase 8.
+ // NEW: read `projectId` from the body; verify it against the link.
+ // The card-create call uses the verified projectId.
+}
+```
+
+The integration tests from Phase 8 (`intake-submit-public.test.ts`)
+exercise the Phase 8 path with `projectId` left empty. New Phase 9
+tests (file `intake-submit-picker.test.ts`) exercise the picker path
+with both tokens and project ids on the wire.
+
+## 7. Owner UI
+
+### 7.1 Client admin page (new)
+
+`/admin/clients/[clientId]` currently exists? (No — Phase 8 only added
+links at the project level.) Phase 9 adds:
+
+- A new route `/admin/clients/[clientId]` (server component,
+ owner-only via the existing admin layout).
+- Reads the client plus the active projects on it (the owner's view of
+ what's deployable for intake purposes).
+- Renders three panels stacked:
+ 1. **Public intake links** — the same `` from
+ Phase 8, but now `link.project` is always empty on new rows.
+ Mint / rotate / revoke / per-link daily cap.
+ 2. **Projects** — a list of the client's projects with a small
+ `` next to each (one toggle calls one new
+ Server Action per project). Disabled + active styled. Toggling
+ `show_in_intake` is two clicks, not three.
+ 3. *(Future)* a "client URL" badge — a copyable URL of the form
+ `https:///intake/` — landed on the page when at
+ least one active link and at least one opted-in project exist.
+
+The admin layout already enforces owner-only access; no extra guard
+needed here.
+
+### 7.2 Project admin page (modify)
+
+`/admin/projects/[projectId]` already exists. One small addition:
+
+- A new `` rendered
+ in the existing admin page, just above the
+ ``. It calls a new Server Action
+ `setProjectShowInIntakeAction({ projectId, show })`. Owner-only.
+
+The Public-intake-links section on the project page stays (Phase 8
+shape: a per-project link can still be minted). It just becomes the
+"legacy / experimental" path; the new client-level link is the
+default.
+
+### 7.3 No new Server Actions beyond what is listed above
+
+- `setProjectShowInIntakeAction` (owner-only)
+- The picker page itself needs no Server Action — it's a server
+ component that reads the database and renders. Submission still
+ goes through `intakeStepPublicAction` and `POST /api/public/intake`.
+
+## 8. Migration of existing Phase 8 tokens
+
+Phase 8 left any number of per-project tokens live in PocketBase.
+Most owners will want to consolidate to a single per-client link,
+but doing that automatically could change the URL a client already has.
+**Phase 9 deliberately does not auto-rotate Phase 8 tokens.** They keep
+working with the Phase 8 behaviour: the old URL still renders the
+wizard, the wizard still runs against the link's `project`, the
+existing rate limit + daily cap continue to apply on the link row.
+
+Owners can:
+
+- Leave them as-is indefinitely (they're still secure).
+- Manually rotate them from the project admin page (which now reads
+ as "this is a Phase 8 shape; consider minting a Phase 9 link
+ from `/admin/clients/[clientId]` instead" but does not force it).
+- The owner UI surfaces a per-link `created_via` field (added to
+ `intake_links`):
+ `"phase-8-project-link" | "phase-9-client-link"` set at insert
+ time.
+
+The choice not to auto-rotate is on purpose — a leaked Phase 8 link
+must remain revokable through the same mechanism it was minted
+through.
+
+## 9. Daily cap, per `(link, project)`
+
+Section 4.4 named the shape. Two operational consequences:
+
+1. **Existing `submission_count_today` on the link row stays.** It's
+ the canonical counter for *Phase 8* links. Phase 9 links use the
+ new `intake_link_submissions` collection for *their* cap.
+2. **The UI shows per-project counts.** Where Phase 8 says `1 total`,
+ Phase 9 says `quotes=12, billing=3` — at most one per project the
+ client has, summed across the lifetime of the link. The picker
+ itself does NOT show today's counter to the public; the counter is
+ owner-side only.
+
+## 10. Plan split
+
+Three plans in the same format as Phase 4b / 8a / 8b / 8c.
+
+| Plan | Scope | Day-estimate |
+|---|---|---|
+| **9a — schema + per-project opt-in + owner toggle** | Add `intake_links.client` (optional, additive), add `projects.show_in_intake`, add `intake_link_submissions`, add migration seed; new ``; `setProjectShowInIntakeAction`. Daily-cap unit tests for the new per-(link, project) shape. | 1 day |
+| **9b — picker page + route split + new owner page** | `/admin/clients/[clientId]` route + panel; `/intake/[token]` picker page; `/intake/[token]/p/[projectId]` wizard page; `intakeStepPublicAction` accepts `projectId`; `POST /api/public/intake` verifies it; `` takes a `projectId` from URL; client URL badge in owner UI. | 1 day |
+| **9c — integration tests + verification** | `intake-submit-picker.test.ts` mirroring Phase 8 Task 17 shape; happy path, hostile project-id, revoked project, mid-cap; full migration smoke; updated old-test signatures if needed. | 1 day |
+
+Total: 3 working days. The Phase 8 architecture carries the load; the
+new work is narrow.
+
+## 11. Acceptance criteria
+
+1. **Single-URL UX.** A client with one Phase 9 link and three projects
+ gets a picker page listing those three; selecting any one runs the
+ wizard against it.
+2. **Opt-in is the default.** A newly-added project is NOT in the
+ picker until the owner flips `show_in_intate` to `true`.
+3. **One URL per client.** `/admin/clients/[clientId]` shows every
+ link minted for that client, with mint / rotate / revoke / per-link
+ daily cap, same `IntakeLinksPanel` UI Phase 8 shipped.
+4. **Worst a hostile body achieves** on the picker path matches the
+ Phase 8 invariant: a strangely-worded card in the Inbox of the
+ chosen project. `parseBrief` whitelists the output; server decides
+ `column`, `priority`, `accepted`, `created_by_role="anonymous"`,
+ `created_by=""`.
+5. **Token shape check fires first.** A 32-char base64url for `token`,
+ a real project id for `projectId`. Anything else → 404, same wording.
+6. **Daily cap is per `(link, project)`.** Two projects under the same
+ link each get their own 100/day budget; one doesn't lock the other.
+7. **Phase 8 tokens keep working.** A pre-existing per-project link
+ (with `link.project` set) still resolves the Phase 8 way; the test
+ suite doesn't regress.
+8. **The picker page is itself a static, public, signed-out route.**
+ No AppShell, no theme provider, the minimal layout Phase 8 wrote.
+9. **The PWA service worker continues to cache `/intake/*`.** No
+ service-worker change; the picker is part of the same cache
+ allowlist.
+10. **The signed-in wizard / signed-in route / signed-in owner UI
+ is unchanged.** Existing tests still pass; signed-in behaviour
+ does not regress.
+
+## 12. What this spec deliberately doesn't do
+
+- **Cross-client links.** A token still binds to exactly one client.
+ Multi-tenant intake is a different problem.
+- **Recategorising links automatically.** No system process that
+ detects a Phase 8 link and "upgrades" it. The owner does the work
+ explicitly if they want; otherwise the old shape lives forever.
+- **Inline client-name overrides.** The picker shows the project's
+ actual `name`. A future Phase 10 can add `client_display_name` to
+ `projects` for client-branded naming if it ever matters.
+- **Renaming the old URL.** `/intake/[token]` with `project` on the
+ link still resolves to the project; the picker is only added to
+ client-shaped rows. There are no redirects to introduce.
+- **Search / tag filtering in the picker.** Picker is alphabetical
+ by project name. If a client has 30 projects the picker is a long
+ list — that's their problem to solve (their team should be a smaller
+ list), not ours.
+- **Branch-by-tenant analytics on intake.** One PocketBase, one
+ collection, one schema. Reporting on intake still happens through
+ the same dashboard phase 2 shipped.
+
+---
+
+## 13. Definition of done
+
+1. `npm run test:all` passes: ~459 unit, ~75 integration (existing
+ Phase 8 set + the new picker path).
+2. `npm run build` succeeds; colour check empty (modulo the spec-known
+ PWA themeColor metadata exception).
+3. The full picker flow worked end-to-end in the developer's browser
+ before merge.
+4. Phase 8 manual walkthrough steps 19.3–19.8 still pass.
+5. Owner can opt a project in/out from `/admin/projects/[id]`.
+6. A wiped-and-rebuilt dev PocketBase from this spec produces a
+ working picker with one seeded link and three opted-in projects.
+
+---
+
+## 14. Deliberately not in scope for this spec
+
+The owner-side UX work — drag-and-drop reorder of links, link
+notes/description editor, copy-URL-to-clipboard button on the client
+admin page — is a Phase 9.5 task list. This spec closes on the
+minimum surface that makes the picker flow work; everything else is
+polish on top.