user_profiles (id/email/role/district_id/vendor_id) has existed since the very first migration (0001) and has sat completely empty and unwired the whole time — it's exactly the mapping table this item needs; build on it rather than adding a new one. Also confirmed coordinator@demo.theclearline.io has no real Supabase Auth user behind it and only "worked" because RLS was off — needs a real account before the authenticated-only policies are safe to run.auth.users in the Supabase dashboard. Applies to district admins and vendors alike (vendors set their own password through /accept-invite, so they will forget it). Scope: a working forgot-password request (Supabase resetPasswordForEmail) that never reveals whether an email has an account; a /reset-password page that accepts the recovery link and sets the new password; a change-password option for signed-in users; graceful handling of Supabase's recovery-email rate limits. Per-environment setup outside the code: the Site URL and redirect allow-list in each Supabase project (dev and prod), and custom SMTP confirmed on prod — Supabase's built-in mailer only delivers to team members and is heavily rate-limited, so real customers would never get the email. The hardcoded demo accounts (admin@, coordinator@, etc.) are unaffected. Related: B-053 (staff-forced reset from the internal portal), B-004b (sole-contact recovery). Code built 2026-09-21 on the demo branch: the forgot-password form sends the recovery email (generic confirmation either way, rate-limit message, no email for the shared demo logins), /reset-password handles the emailed link (expired-link state, signs out every session afterwards), and a "Change password" item in the account menu asks for the current password first. Still open: the Supabase Site URL / redirect allow-list and custom SMTP in each project, and promotion to main.aaaaaaaa-0000-0000-0000-000000000001) is hardcoded as DEMO_DISTRICT_ID in 11 files — the admin home, vendors list, vendor detail, Vendor Requirements and Activity pages, the invite-vendor and backstage routes, onboarding, the vendor staff page, the daily digest, and Ask Clearline — each with a "replace with district from auth session" TODO. Consequences: every admin page can only ever show the demo district, so on clearline-prod (whose district has a different ID, 00000000-0000-0000-0000-000000000001) the admin pages show nothing, and inviting a vendor there would write rows against a district that doesn't exist. Which logins count as admins is also hardcoded, to a short email list in adminRole.ts, so a new district's admin would be routed to the vendor portal. Fix: resolve the logged-in user's district once, from user_profiles (the mapping table from B-005), pass it through the app, and remove the constants; do the same for vendors (resolve vendor_id from the profile instead of matching primary_email) and replace the admin-email list with the profile's role. Shares its foundation with B-005 — build the profile mapping once for both. Blocks the first real customer on prod, and B-053 depends on it./backstage: that panel has no login of its own and was blocked on production in September 2026 for exactly that reason. Depends on: the district-to-login mapping in B-005 (user_profiles is designed for it but unused today), so the first admin can be assigned to a district; a working password-recovery flow (B-054); and replacing the hardcoded demo district ID and admin-email allowlist (B-055), so a newly created district's admin can actually use the app. Related: B-046 (read-only cross-district visibility) is likely part of this same portal — scope them together. Until this exists, onboarding a customer is a manual runbook in the Supabase dashboard.replaces_requirement_id chaining (confirmed 2026-08-20, no new logic needed) — this is a visualization layer on top, worth building once people are actually stacking multiple changes deep, not before.useMemo/useCallback exists anywhere in admin/ or portal/ — e.g. the vendor search box re-filters the full vendor list on every keystroke, and vendor status gets recomputed on every list render. Trivial today (dozens of vendors — a JS array filter over that is sub-millisecond, unnoticeable). This is a UI-responsiveness question more than a vendor-count one: it's worth addressing once a single list is regularly rendering somewhere in the 1,000+ vendor range, or once per-row computation gets meaningfully heavier than it is today — whichever comes first.