cpatpa adds a mandatory onboarding wizard and per-user custom instructions

Three commits build the post-signup data-capture flow: a redirect-enforced onboarding wizard, a custom instructions panel with a 4096-char server-side cap, and a practice profile panel for updating the same fields after signup. The org-specific content (Piper Alderman's AU jurisdictions and practice areas) is seeded directly in `org_settings`.

personasintake

The wizard (afde5326) is gated by middleware: any authenticated user with onboarded === false is redirected to /onboarding for every non-public route except /onboarding itself and /api/session-token (needed to mint a bearer token for the backend submit). The wizard collects display name, office, jurisdictions, practice areas, sectors, an optional "other" area, and the AI-use disclaimer. Submit is atomic - all profile fields, onboarded_at, and ai_disclaimer_at land in a single transaction alongside an user.onboarding.complete audit event. On success the wizard calls useSession().update({ onboarded: true }) to flip the JWT flag without forcing a sign-out, then redirects to /assistant.

The backend exposes four /me routes: GET /me, GET /me/onboarding-options (active entries from org_settings sorted by order), POST /me/onboarding, and PATCH /me. Validation runs server-side against the canonical lists in org_settings - unknown ids are filtered out, and the submit fails closed if no valid ids remain for any required axis.

Custom instructions (PATCH /me + d2a8512b) accept up to 4096 characters and truncate server-side if the payload is longer. The account panel shows a live char counter that turns red past the cap, plus a Discard button when the field is dirty. The additive-only contract - org system prompt takes precedence - is explained in the UI copy and enforced server-side at prompt-assembly time (Phase 9 in this fork).

The practice profile panel (99046ce8) surfaces the same wizard fields on /account for post-signup edits. Changing office snaps the timezone to that office's default. Both panels share the /me/onboarding-options endpoint for their picklist data.

One detail worth pulling: the jwt callback in auth.ts handles trigger === "update" so the onboarding flag propagates to the session token without a sign-out cycle. It's a two-line pattern but easy to miss.

So what The firm-specific content - Piper Alderman's office list, jurisdictions, practice areas - is seeded directly in `org_settings`, so adopting this wholesale means swapping those seeds and deciding whether a mandatory post-signup wizard fits your product. The custom instructions plumbing (4096-cap, server-side truncation, additive-to-org pattern) is independently portable and worth looking at if you want a similar feature. Skip the wizard redirect middleware unless your use case genuinely needs to gate access behind a data-collection step.

View this fork on GitHub →

Spotted something wrong? Or know the PR text has fresher detail than the writeup above?

Commits in this thread

3 commits from cpatpa/PIP, oldest first. Source extracted verbatim from the harvested git log.

SHA Subject Author Date
afde5326 Phase 2: onboarding wizard + custom-instructions endpoint Claude 2026-05-15 ↗ GitHub
commit body
Adds the post-signup onboarding flow. The wizard collects display
name, office, jurisdictions, services (practice areas), sectors, an
optional "other" practice area, and the AI-use disclaimer
acknowledgement.

Middleware now redirects authenticated users with onboarded === false
to /onboarding for any non-public route, except /onboarding itself
and /api/session-token (which the wizard needs to mint backend
bearers). Once the wizard succeeds it calls
useSession().update({ onboarded: true }) so the session JWT picks up
the flag without a sign-out.

Backend new router /me with four endpoints:

  GET  /me                    Current user payload.
  GET  /me/onboarding-options offices, jurisdictions, services,
                              sectors from org_settings (active
                              entries only, sorted by order).
  POST /me/onboarding         Atomic submit: validates inputs,
                              filters unknown ids against the org
                              canonical lists, fails closed if
                              required axes are empty, writes all
                              profile fields plus onboarded_at and
                              ai_disclaimer_at, emits
                              user.onboarding.complete audit event.
  PATCH /me                   Partial post-onboarding updates,
                              including custom_instructions (capped
                              at 4096 chars).

Verified end-to-end against Postgres 16:
- GET /me reports onboarded=false before, true after.
- Disclaimer not acknowledged returns 400.
- Bad office id returns 400.
- All-unknown jurisdiction filters to empty and returns 400.
- Valid submit writes office, jurisdictions, services, sectors,
  practice_areas_other; sets onboarded_at and ai_disclaimer_at; and
  records audit event with metadata counts.
- PATCH /me applies custom_instructions and is visible on next GET.

The custom-instructions account-settings UI lands in a follow-up.
The org-level system prompt assembly (org + workspace + user) lands
in Phase 4 alongside the admin AI Policy page.
d2a8512b Phase 2: custom-instructions panel on the account page Claude 2026-05-15 ↗ GitHub
commit body
Adds a Custom Instructions section to /account that fetches and saves
via the new PIP /me endpoints. The section explains that user-level
instructions are additive on top of the organisation's instructions
and may not override them, and includes the AI-output disclaimer.

  - Loads the current custom_instructions from GET /me.
  - Saves with PATCH /me. Live character counter against the 4096 cap.
  - Shows a Discard button when dirty, a Saved indicator on success,
    and a red counter when the textarea exceeds the cap.

The existing display name, organisation, plan, sign-out, and delete-
account sections on the same page continue to use the upstream
Supabase-backed paths; they will be migrated when those backend routes
move off the Supabase JS client onto plain pg.

End-to-end round trip verified against Postgres 16:
  - GET /me returns the existing instructions.
  - PATCH /me persists a new value.
  - 5000-character payload is server-side truncated to 4096.
99046ce8 Surface PIP practice fields on the Account page Claude 2026-05-15 ↗ GitHub
commit body
Adds PracticeProfilePanel so users can update the PIP-specific fields
captured at onboarding (office, timezone, jurisdictions, practice
areas, sectors) from /account. Saves through PATCH /me; server-side
validation against org_settings canonical lists still applies, and
changing office snaps the timezone to that office's default.

Capture this thread into my fork

Download a single Markdown prompt that tells Claude how to port every commit above into your working tree — adapting paths and structure to match your repo. Run it via claude -p < capture-thread-362.md from inside the repo you want the changes in.

⬇ Download capture-thread-362.md