Skip to main content

Design-capstone accessibility pass + parental-write-gate generalization

Date: 2026-07-11 Status: Accepted — both shipped. Related: design-system decisions, actor-resolution invariant ADR, authored Authorization page §2.

Two load-bearing bodies of work worth a durable record.

1. Design-capstone pass (S1–S8) — accessibility & polish

A comprehensive design/usability/a11y audit across DS tokens + all top-level surfaces + key sheets, triaged must-fix / nice-to-have, then fixed in a wave. Shipped slices:

SliceCommitWhat
S1f5ab211Light-theme AA contrast pass (tokens + bucket-ink + light contrast test).
S29a2d4a2Keyboard focus + selected semantics for nav items, pickers, search-more.
S3fb7fd7b44dp minimum touch targets (emoji, timer start, bucket chips, color swatches, clear-icon).
S4f86c0adError liveRegion + loading/error/retry states + pull-to-refresh (Today/Earn/Rewards/sign-in).
S5fe042c8Success feedback + consent-capture guard + member-remove confirm + consent copy weight.
S649c40d0, 2c928e3Screen-reader labels + unique action labels + atom label safety (+ restore tap action on relabeled buttons).
S75df174bToken/size compliance + text-scale robustness + focus-ring + a11y-settings deep-link.
S862e4b22Typographic hierarchy — tabular numerics + lifted headers + readable-content type.

This is the natural home for the carried a11y/polish debts (switcher affordance cue, keyboard-focus parity on picker tiles, chip-gap extreme case). The DS a11y floor (WCAG AA contrast in both themes, Semantics coverage, reduced-motion, focus) is now audited, not assumed.

2. Parental-write-gate generalization (SDK ↔ RLS parity)

Decision: the SDK service must fail before the network round-trip wherever a Postgres RLS policy gates on parental_household_ids() — converting a raw PostgrestException(42501) into a typed AuthorizationFailure, and closing the class where an admin-role but non-parental member (e.g. an otherAdult granted admin) clears the app's admin-role guard but is denied at the wire.

  • The single shared helper EconomyService._requireParentalWriter([action]) resolves the acting member only from the authenticated account (memberByAuthUserId(auth.uid()), the SDK twin of CurrentMemberRepository) — never the viewingAs lens, never isParental.firstOrNull — and throws when the actor's kind is not isParental. It no-ops when the resolver is null (offline / free tier / unit slice) so RLS stays the sole backstop (online-enforced / offline-trusted parity with the child-PII gate).
  • Applied (each verified: parental-KIND RLS and intended parent-only): activities, rewards, houses (create/update/delete), places (create/update/delete), budget categories, spend gates (create/delete), goal lifecycle (create/update/archive/restore/complete/ approve).
  • Deliberately skipped (member-initiated — gating would break them): requestGoal, requestRewardRedemption/requestActivityRedemption, cancelSpendRequest, chore self-service verbs; and the ledger-internal token_batches/redemptions (no direct parental verb — downstream of already-parent-gated approval).
  • Deferred (dashed): ChoreService chore-catalog writes — that service has no currentMember resolver and shares the chores table with member self-service verbs, so it needs separate wiring + surgical per-verb gating.
SliceCommitWhat
Dda4adbeIntroduced the gate for catalog writes (activities/rewards) — the reproduced SDK↔RLS mismatch.
Fbc6f812Generalized _requireCatalogWriter_requireParentalWriter and applied it to houses/places/budget/goals/gates (25 call sites).

No RLS / schema / migration change; no gate weakened — the fix makes the SDK match the RLS truth and surface a clean typed error.