fix(backend): survive un-migrated databases and cap prompt-bound fields

↗ view on GitHub · Amal · 2026-08-21 · 3f9b9b9d

Two backend hardenings from the PR #365 review, both in user.ts.

── 1. Profile reads on databases without the 20260821 migrations ──

WHY THIS MATTERS
Replicated live by dropping the six new user_profiles columns (the
state of any deployment that updates the backend before running
migrations): GET /user/profile returned 200 but the user's saved
legal_research_us=false and quick_actions_visible=false came back as
true, model preferences fell back to defaults, and PATCH /user/profile
returned a raw 500 leaking PostgREST internals. Settings appeared to
reset themselves.

WHAT IS THE FALLBACK-TIER CASCADE
selectProfile tolerates older databases by retrying with smaller
column lists. Each tier is entered by matching the NAME of its newly
added column inside the Postgres 42703 (undefined column) error.
Postgres reports only the FIRST unknown column - after this PR that is
"jurisdiction" - so none of the existing tiers matched and the code
fell through to a select that predates the model/research/quick-action
columns, then papered over the gap with defaults.

HOW THE FIX WORKS
A new first tier keys on any of the six 20260821 columns and retries
with PROFILE_SELECT_NO_ONBOARDING - the exact pre-PR column list - so
every previously saved preference survives. serializeProfile already
treats the absent onboarding fields as "legacy exempt" (version 0),
which matches what the migration's backfill would write, so existing
users are not funneled into onboarding by a missing migration either.
getUserModelSettings gets the same treatment: on 42703 it retries with
the pre-migration three-column select instead of silently switching
every chat to default models and re-enabling US legal research.

The test harness's per-table stub now accepts a queue of results so a
test can express "first select fails with 42703, the retry succeeds",
and a regression test pins the preserved preferences.

── 2. 200-character cap on displayName / organisation ──

WHY THIS MATTERS
This PR starts injecting both fields into the system prompt of every
chat, project chat, and Word chat turn. Every other personalisation
field is bounded (jurisdiction <= 100, practice areas <= 20 x 100,
titles allowlisted), but these two only had a cap inside the signup
trigger - PATCH /user/profile accepted arbitrary sizes, letting one
oversized value inflate token cost on every message thereafter.

HOW THE FIX WORKS
validateProfilePayload rejects values over 200 characters (the same
limit handle_new_user applies via left(..., 200)), with tests for both
fields.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RuPCULDYgVsiCanRgbEy6W
Repository open-legal-products/mike
Author Amal <mamalanand3@gmail.com>
Authored
Parents 64f2e625
Stats 3 files changed , +153 , -6
Part of Add Google sign-in and personalised onboarding

Capture this commit into my fork

Download a Markdown prompt that tells Claude how to port this exact commit into your working tree. Run it via claude -p < capture-commit-3f9b9b9d.md from inside the repo you want the change in.

⬇ Download capture-commit-3f9b9b9d.md