Consumer Portal
A branded, passwordless portal where the people you report on can sign in, see their tradelines, accept invitations, and manage consents — under your brand, on your subdomain, with FCRA disclosures baked in.
1. What it is
The Consumer Portal is a separate, white-labeled web surface that lives alongside your furnisher dashboard. Where the dashboard is for you — the company furnishing data — the portal is for the people you report on: tenants, borrowers, BNPL customers, HOA members, CDFI loan holders, anyone whose tradeline you push to the bureaus.
Every Metro2 customer gets one. By default it lives at {your-subdomain}.portal.metro2.app. Custom domains (e.g. credit.yourcompany.com) are on the roadmap and ship in Phase 4.
The portal does five things today:
- Passwordless sign-in for the consumer (magic link, optional SMS OTP, optional passkey).
- Accept-invitation flow with a per-furnisher welcome page and prefilled identity.
- Records view — a self-service window into what you are reporting about that specific consumer.
- Consent collection — append-only, ESIGN-grade evidence of every "I agree" the consumer ever clicks.
- FCRA rights footer on every page, so the consumer can never claim they weren't informed.
It is intentionally a different product from the dashboard: different auth model, different identity table (consumers, not profiles), different RLS (self-read only), different brand surface (per-furnisher subdomain). You don't need to do anything special to keep them isolated — they share nothing but a database.
2. Why offer one
Regulatory: FCRA §623(a)(8) is coming
Under the Fair Credit Reporting Act, consumers have the right to dispute information directly with the furnisher (you), separate from the indirect dispute path through the bureau. Most furnishers handle these by email or a contact form — slow, hard to track, and a compliance liability when the 30-day clock starts and nobody is watching.
Phase 3 of the portal will ship a full direct-dispute intake with the 30-day investigation clock, frivolous-determination workflow (per 12 CFR §1022.43), evidence upload, and response delivery. When that lands, every consumer you've already enrolled is automatically eligible — you don't have to migrate anyone. That's a strong reason to invite consumers now, even before the dispute UI is live.
Operational: fewer support tickets
A meaningful slice of every furnisher's inbound support volume is "what are you reporting about me?". The portal answers that question without a human in the loop. We've seen companies cut credit-related support email by 40-60% in the first quarter after rollout.
State-law: AB 2747, CCPA, CO/CT/UT/VA mirrors
California AB 2747 requires landlords to obtain affirmative consent before reporting rent. CCPA/CPRA and the state-mirror laws require a clean "right to know / right to delete" workflow. The portal's consent engine is the single surface you point any regulator at when they ask "how do you collect, store, and revoke consent?"
Strategic: transparency drives down disputes
When the consumer can see in real time exactly what you're reporting and why, frivolous disputes drop. The data they can't see is the data they assume is wrong.
3. Setting up your subdomain
Every company gets a unique portal_subdomain stored on the companies row. The portal middleware reads the incoming hostname and uses the subdomain to figure out which furnisher you are.
Set it in the dashboard
- Go to Settings → Consumer Portal.
- Pick a subdomain. Lowercase letters, digits, and hyphens. 3-32 characters. We'll check availability live.
- Click Save.
- Visit
https://{your-subdomain}.portal.metro2.appin a fresh browser window. You should see the sign-in page, branded with your company name (and logo, if you've uploaded one under Settings → Branding).
Naming tips
- Match your domain. If your main site is
acme-rentals.com, pickacme-rentals. Consumers trust subdomains that look like the brand they already know. - Don't use product names.
credit-portallooks generic and reads as a third-party tool.{yourbrand}-creditreads as you. - Lock it early. Once you start inviting consumers we recommend not changing the subdomain — old invitation emails point at the old hostname and will 404 after a rename.
Path fallback for slug collisions
If the subdomain you want is taken, you can use the path fallback https://app.metro2.com/portal/{your-slug}. It works identically — branding, RLS, cookies, all the same. The only difference is the URL doesn't look like "your" domain.
Custom domains (Phase 4)
Custom domains like credit.yourcompany.com are on the roadmap. When they ship, you'll add a CNAME at your DNS provider pointing to a Metro2-hosted edge endpoint, then issue and verify the TLS certificate from the dashboard. Until then, the {slug}.portal.metro2.app hostname is the canonical URL.
4. Inviting a consumer
Consumers don't self-register. You invite them, an email (and optionally an SMS) goes out, they click the magic link, and on first sign-in their auth.users row is tagged as a consumer and a consumer_furnisher_links row is created linking them to your company.
Where
/dashboard/consumers. The page lists every consumer currently linked to your company plus every pending invitation.
Single invitation
- Click Invite consumer in the top right.
- Fill in email (required), phone (E.164, optional), and external consumer ref — your internal ID for this consumer (lease ID, loan number, customer number). The ref is what lets you match the consumer's portal session back to your own system; it's also what we use to scope which
metro2_recordsthey can see. - Optionally add a prefill payload — free-form JSON. We'll show those fields on the welcome page ( "Hi, your lease at 1234 Oak Apt 2B with Acme Rentals").
- Click Send invitation. The token is generated server-side, expires in 14 days by default, and is delivered by email. If a Twilio number is configured the consumer also gets an SMS with the same link.
Bulk invitation (CSV)
For onboarding existing rent rolls / loan books in one shot:
- Click Bulk invite on the same page.
- Upload a CSV with these columns (header row required):
email,phone,external_consumer_ref,first_name,last_name. Onlyemailis mandatory. - Preview the parsed rows. Bad emails are flagged in red; bad phone numbers (non-E.164) are flagged in yellow and silently dropped from the SMS leg.
- Click Send all. We rate-limit to roughly 60 sends a minute to keep your sending domain healthy; large batches run in the background and you'll get a completion email.
Invitation link mechanics
The link in the email is of the form https://{your-subdomain}.portal.metro2.app/portal/invitation/{token}. The token is a 32-byte URL-safe random string. It is single-use: once accepted_at is set, re-clicking the link returns a 410 ("already used, please sign in"). Expired links also return 410. Both errors render a friendly "ask your furnisher for a new invite" page.
Resending and revoking
From the consumers page, every pending invitation has a Resend button (sends the same token to the same address, refreshesexpires_at) and a Revoke button (sets expires_at to now, making the link dead). After an invitation is accepted, the controls switch to Pause link and Close link, which set the link_status on consumer_furnisher_links accordingly.
5. What the consumer sees
The consumer's journey is intentionally tiny. We measured dropoff on each step in early pilots and stripped everything we could.
Welcome page (first visit)
The consumer clicks the invitation link. We render a one-screen welcome that says "Acme Rentals has invited you to view your credit reporting record." with your logo, your brand color, and any prefill payload you sent. They tap Continue.
Sign-in
One field: email (pre-filled from the invitation). Tap Send sign-in link. A magic link arrives in their inbox. Clicking it returns them to the portal, signed in. No password ever exists.
If you've enabled SMS as a secondary, they also see "Text me a code instead." That triggers Supabase's phone-OTP flow; the consumer types a 6-digit code on the next screen.
Home (after sign-in)
A two-card layout:
- Your accounts — every tradeline you furnish for this consumer, in plain English (account opened, last reported, balance, current status, 24-month payment history grid). Tapping an account drills into the full Metro 2 detail view.
- Your consents — a status list of every consent on file: ESIGN, ToS, AB 2747, soft-pull, marketing email. Each row shows when it was signed, when it was last reviewed, and (where allowed) a Revoke button.
Records list / detail
Every record we're holding for this consumer + furnisher pair, filtered through the v_consumer_record_status view (which respects the consumer's RLS context). They cannot see records belonging to other consumers, or other furnishers' records about them unless they are linked to that furnisher too.
Settings → Security
Where the consumer can:
- Update their email or phone (changes trigger a verification flow).
- Enroll a passkey — uses WebAuthn. Once enrolled, future sign-ins on that device skip the magic-link step entirely.
- Sign out of all devices.
- Request data export (CCPA / state mirrors) — Phase 4. Today the UI is in place but the action queues a manual support request.
6. Authentication options
Magic link (always on)
Powered by Supabase Auth's signInWithOtp({email}). The token in the email redirects back to a URL that embeds your furnisher subdomain — this is important because cookies are subdomain-scoped, so a magic link for acme.portal.metro2.app must land on acme.portal.metro2.app, not on the bareportal.metro2.app.
On first successful sign-in, the consumer's auth.users.raw_app_meta_data.principal_type is set to"consumer". This tag is what every/api/portal/* route checks before doing anything else; without it, the route 404s. This is the principal-type-mismatch defense — a furnisher employee accidentally hitting a portal endpoint will look exactly like a non-existent consumer.
SMS OTP (off unless Twilio is set)
SMS is a secondary channel: not all consumers have email, and SMS fallback dramatically improves accept rates for renter populations. But it requires a paid Twilio (or equivalent) integration. To turn it on:
- Set
TWILIO_ACCOUNT_SID,TWILIO_AUTH_TOKEN, andTWILIO_PHONE_NUMBERas Supabase project secrets, or configure the same in Supabase Auth's Phone Provider settings. - Re-deploy. No code change required.
When Twilio is not configured, the API gracefully degrades. The /api/portal/auth/magic-link endpoint still works normally; SMS-OTP requests return HTTP 503 with { reason: "sms_disabled" }. The UI hides the "text me instead" button when it sees that response, so the consumer never gets a broken-looking screen.
Passkey (opt-in, post-enrollment)
After the first successful magic-link sign-in, the consumer can opt into a passkey in Settings → Security. We use the platform's native WebAuthn flow — on iOS that stores in iCloud Keychain, on Android in Google Password Manager, on macOS / Windows in the local platform authenticator. Aconsumer_auth_methods row records the registration so we can list and revoke devices.
7. Consents
The portal's consent engine is its single most important compliance artifact. Every consent is captured in the consumer_consents table, which is append-only by database trigger. A consent row, once written, cannot be edited. The only legal mutation is setting revoked_at + revoked_reason on a previously-signed row.
What each row freezes
signed_html— the exact HTML of the disclosure the consumer saw at the moment they signed. Not a template ID, the rendered HTML itself. If your lawyer ever asks "what did the consumer see on April 3, 2026?" this is the answer.template_id— version-suffixed (e.g.rent_positive_only_v1). Templates live insrc/content/consent-templates/and once referenced are immutable; new versions ship as new files (v2,v3...).signature_method— one ofclick_wrap,typed,drawn, orpaper_upload.signature_payload— extra context (typed name, draw stroke data, uploaded paper filename).ip_inetanduser_agent— captured server-side at sign time.bureaus_disclosed— when the consent is about sharing with bureaus, which bureaus. Required for ESIGN-style scope clarity.scope_company_id— which furnisher this consent is scoped to (a multi-furnisher consumer can have separate consents per furnisher).
The audit trail
Every action — sign, view, revoke — also writes a row to consumer_consent_audit, with event_type, actor (consumer, furnisher_staff, or system), and a free-form payload. If a regulator subpoenas the lifecycle of a single consent, you join the two tables and you have a complete forensic timeline.
Revocation
From the consumer side, Settings → Consents lets them revoke any consent that is legally revocable. The UI calls POST /api/portal/me/consents/revoke, which sets revoked_at, writes an audit row, and (depending on the consent kind) may pause furnishing of the affected accounts.
From the furnisher side, your dashboard's consumer detail page shows the full consent log and a Re-request button that re-prompts the consumer on next sign-in.
Consent kinds today
esign_act— base ESIGN acknowledgment (required before any other electronic consent is legally valid).tos— Terms of Service.privacy_policy— Privacy Notice acknowledgment.rent_positive_only— California AB 2747 affirmative opt-in for positive-only rent reporting.fcra_soft_pull— soft-pull consent for the tradeline simulator (Option C). Phase 4.outcome_reporting— opt-in to share score outcomes with the analytics suite. Phase 4.marketing_email— opt-in to receive marketing email (separate from transactional; can be revoked without breaking sign-in).
8. Identity verification (Phase 2)
The consumer_identity_verifications table and the library scaffolding under src/lib/portal/identity/ are already in place. They are not wired to a third-party verification provider yet — that integration ships with the rent-vertical wave when the operational and pricing tradeoffs of Persona vs. Stripe Identity vs. Plaid Layer are settled in production usage.
What this means for you today:
- New consumer enrollments don't hit an ID-verification step; the magic-link click is sufficient to prove control of the email/phone.
- When the verification step lands, it will appear as an extra screen in the invitation-accept flow, gated by a furnisher setting (
require_id_verificationon thecompaniesrow). Existing consumers will be grandfathered in unless you flip a per-company "re-verify on next sign-in" toggle. - The audit table is already populated by the simpler email/phone-confirmation events, so you have an audit trail even before formal KYC.
9. RLS guarantees
A consumer signed into the portal has a Supabase session whose JWT carries raw_app_meta_data.principal_type = "consumer" and a known auth_user_id. The Postgres RLS policies on every consumer-scoped table use that to scope reads:
consumers— consumer can only read their own row (matched byauth_user_id).consumer_furnisher_links— consumer can only read rows whereconsumer_idresolves to their own consumer row.consumer_consents&consumer_consent_audit— same self-only filter.v_consumer_record_status— a database view overmetro2_recordsthat joins throughconsumer_furnisher_links.external_consumer_refand applies the same self-only filter. This is the only way the consumer can see Metro 2 record data.consumer_invitations— the consumer can read an invitation by its token only (public-by-token lookup); they cannot list invitations.
Service-role queries (your dashboard, our internal jobs) bypass RLS. Superadmin sessions also bypass RLS via the existingis_superadmin() function. There is no third tier — a regular furnisher staff session does not see any consumer's PII outside the consumers linked to their own company.
As defense-in-depth on top of RLS, every /api/portal/* route also calls requireConsumerSession(req) and findHostLink(session, furnisherId). If the consumer is on acme.portal.metro2.app but has no link to Acme Rentals (only to Beta Properties), the host-link check returns null and the route 404s. They cannot even confirm an account exists on a host they aren't authorised for.
10. FCRA Rights Footer
Every page of the portal renders the <FCRARightsFooter /> component. It contains:
- A link to the official CFPB Summary of Your Rights Under the FCRA (PDF, opens in a new tab).
- A line stating the consumer's right to dispute directly with the furnisher under FCRA §623(a)(8) — naming your company specifically so the consumer knows who they are dealing with.
- A mention that state-specific rights (CA, CO, CT, UT, VA) also apply. Phase 4 expands this into a state-addendum dropdown with the per-state long-form disclosures.
The footer is rendered at the layout level — it cannot be hidden by an individual page. This is intentional: regulatory disclosure is not a UX choice, it's a compliance floor.
11. What's coming next (Phase 3 / 4)
The portal is the consumer-facing surface for everything Metro2 ships from here on. The visible roadmap:
Phase 3 — Direct dispute portal (FCRA §623)
- Per-account Dispute this button. Triggers a guided intake (reason, evidence upload, consumer narrative).
- 30-day investigation clock (auto-extends to 45 if the consumer adds info), surfaced both to the consumer and inside your dashboard.
- Frivolous-determination workflow per 12 CFR §1022.43 — 5-business-day notice template with the cure-path requirement.
- Outcome delivery — verified / corrected / deleted, with the new Metro 2 record diff visible to the consumer.
Phase 3 — Score tracking
- Shell wired to whichever score source you've enabled (rent-vertical self-reported, simulator synthetic, soft-pull partner).
- 24-month score history chart, factor breakdown, what-if simulator.
Phase 3 — Notifications
- Resend (email) + Twilio (SMS) for lifecycle messages — new invitation, dispute filed, dispute resolved, monthly recap.
- Per-consumer notification preferences (frequency, channel).
Phase 4 — PWA + custom domains
- Installable PWA (add to home screen) with offline record view and push notifications.
- Custom-domain hosting (
credit.yourcompany.com) with automated TLS issuance and CNAME verification.
Phase 4 — Full privacy-rights workflow
- CCPA / CPRA "right to know" and "right to delete" intake. Per-state mirrors (CO, CT, UT, VA, TX).
- Cookie-consent banner with granular categories.
- Data-export package (PDF + JSON) generated on demand.
12. API reference
Per Metro2's API routing ADR (ADR 0001) we do not use Next.js dynamic route folders under /api. Identifiers (invitation tokens, consent IDs, etc.) are passed as query parameters or in the JSON body. Page routes under /portal/* still use path segments because the consumer-facing URL design needs them.
Auth routes
All under /api/portal/auth/*. Public — no session required (these are how the session is created in the first place).
| Method | Route | Purpose |
|---|---|---|
POST | /api/portal/auth/magic-link | Send an email magic-link. Body: { email, redirectTo? }. |
POST | /api/portal/auth/verify-otp | Verify an SMS OTP. Body: { phone, token }. 503 with sms_disabled when Twilio not set. |
GET | /api/portal/auth/session | Returns the current consumer session payload, or 401. |
POST | /api/portal/auth/sign-out | Revokes the session on the server side. |
Consumer self-routes ("me")
All under /api/portal/me/*. Require a consumer session via requireConsumerSession(req). Scoped to the signed-in consumer's data only.
| Method | Route | Purpose |
|---|---|---|
GET | /api/portal/me | Profile (email, phone, name, links list). |
GET | /api/portal/me/furnishers | List the furnishers this consumer is linked to. |
GET | /api/portal/me/records | Tradelines for the current host's furnisher. |
GET | /api/portal/me/consents | List active and revoked consents. |
POST | /api/portal/me/consents | Capture a new consent. Body: { kind, signatureMethod, signaturePayload? }. |
POST | /api/portal/me/consents/revoke | Revoke a previously-signed consent. Body: { consentId, reason? }. |
Invitation routes (consumer-facing)
Used by the welcome/accept flow on /portal/invitation/[token].
| Method | Route | Purpose |
|---|---|---|
GET | /api/portal/invitations?token=... | Fetch the welcome-page prefill (company name, email, payload). Public lookup. |
POST | /api/portal/invitations/accept | Accept the invitation. Body: { token }. Creates the link row and tags principal_type=consumer. |
Invitation routes (furnisher-facing)
Used by the dashboard's invite UI. Auth: existing /api/auth/me session (furnisher staff).
| Method | Route | Purpose |
|---|---|---|
GET | /api/consumer-invitations | List pending invitations + active links for the current company. |
POST | /api/consumer-invitations | Create one invitation, or many via an invitations: [...] array. |
All routes return JSON. Errors follow the standard { error: string } envelope with appropriate HTTP status codes (400 / 401 / 404 / 410 / 503).
13. Consumer FAQ
This section is written as if speaking directly to the consumer. Linkable if you want to forward it to your end users.
How do I sign in?
You should have received an email from your landlord, lender, or servicer with a sign-in link. Click the link from any device — no password is needed. The link works once; if it expires or you lose it, contact whoever sent it to you and ask for a new invitation.
Once signed in, you stay signed in on that browser. If you want to use the portal more easily on your phone, go to Settings → Security and enable a passkey — your phone will then sign you in with Face ID or fingerprint.
Can I dispute information here?
Not yet — but soon. The portal will add a direct-dispute feature later this year that lets you challenge specific account information under FCRA §623(a)(8) and gets you a guaranteed response within 30 days. Until that ships, please contact the furnisher (the company you got your invitation from) directly and ask them to investigate the disputed information. You can also dispute through the bureau directly using the process described in the FCRA Summary of Rights linked at the bottom of every page.
Is my data shared with anyone else?
The data you see in the portal is the data the furnisher is reporting about you to consumer reporting agencies (Equifax, Experian, TransUnion, sometimes Innovis or specialty bureaus). Metro2 is the software the furnisher uses to format and deliver that data; we do not sell your data, and we do not share it with other companies. The full list of bureaus your data is shared with is shown on every consent row in Settings → Consents.
Why didn't I get an SMS code?
Not every furnisher has enabled SMS sign-in. If you only see the email magic-link option on the sign-in screen, SMS isn't available for this portal — please use the email option instead. If you did request an SMS code and it didn't arrive, give it two minutes (carriers can be slow), then re-request. Check that your phone number on file is correct under Settings → Security.
What does "revoke consent" do?
Revoking a consent stops the activity that the consent authorised. For example, revoking your positive rent reporting consent under California AB 2747 tells your landlord to stop sending your rent payments to the credit bureaus going forward. It does not automatically delete past reports — for that, you'll need to file a data-deletion request (coming in a future portal update) or contact the furnisher directly.
14. Common questions for furnishers
Can I send custom email from my domain?
Not in v1. Invitation and lifecycle emails are sent from a Metro2-managed sender with your company name in the From display and your logo in the email body. Sending from your own domain requires DKIM / SPF / DMARC setup on a per-customer basis and is planned for Phase 4 alongside custom domain support.
How do I bulk-invite a rent roll or loan book?
Use the Bulk invite button on /dashboard/consumers. Upload a CSV with at minimum an email column; optional columns are phone, external_consumer_ref, first_name, last_name. We preview the file, flag bad rows, and let you send the batch in the background. You'll get an email when the batch completes with a summary of successes and failures.
What if Twilio isn't set up?
Email magic-link works regardless of Twilio status — that's the always-on primary auth method. SMS OTP is a graceful upgrade: when Twilio credentials aren't configured, the API returns a 503 with reason: "sms_disabled" and the UI hides the "text me a code instead" button. The consumer never sees a broken state.
When does the invitation expire?
Default expiry is 14 days from the moment you create the invitation. After expiry, the link returns a friendly "invitation expired" page that asks the consumer to contact you for a new one. You can also revoke an unused invitation at any time from the consumers page; this sets expires_at to now and immediately invalidates the link.
Can the same consumer be invited by multiple furnishers?
Yes. The data model supports multiple consumer ↔ furnisher links per consumer. A consumer who rents from Acme Rentals and has a CDFI loan from Beta Credit will see both furnishers when they sign in. RLS still scopes everything: on acme.portal.metro2.app they see only Acme records; on beta.portal.metro2.app they see only Beta records. The session is per-subdomain.
Will the consumer's identity be verified before they can dispute?
Phase 2 ships the identity-verification scaffolding (table, library, audit hooks). Phase 3 wires it to a third-party provider (Persona, Stripe Identity, or Plaid Layer — final pick still under evaluation) and gates the direct-dispute flow behind a verified ID. Today, control of the email/phone the furnisher provided at invite time is treated as sufficient proof for the read-only surfaces.
What happens if a consumer revokes their consent?
We never delete or rewrite the consent row — we set revoked_at and append a consumer_consent_audit entry. The downstream effect depends on the consent kind. ESIGN and ToS revocations sign the consumer out and require re-acceptance to come back in. AB 2747 rent-reporting revocations stop future furnishing of rent tradelines for that consumer (it does not retroactively delete past reports — that requires a separate data-deletion request). Marketing-email revocations just stop the email. The dashboard shows you the full consent log for each consumer so you always know the live status.
Can I customise the welcome page wording?
Not in v1. The welcome page uses a single fixed template with your brand name, your logo, and the prefill payload you passed in the invitation. The fixed wording is reviewed for FCRA / ESIGN compliance — letting furnishers freely customise it would undermine the consent record. The prefill payload (a free-form JSON object) is the customisation surface we expose: it's rendered as a small key-value block under your brand line.
Where does the "branded" styling come from?
The portal reuses the white-label theme engine built for the partner / FaaS embed product. Your companies row has theme tokens (primary color, logo URL, font) that are injected as CSS variables on every portal page. If you haven't set any, we fall back to a neutral Metro2 theme with your company name in the header.
15. Related guides
- Multi-portfolio routing — how to map consumers to the right furnisher subsidiary / portfolio when you operate more than one Metro 2 program.
- Analytics & Reporting Suite — surfaces consumer-portal engagement metrics (invitation accept rate, consent rate, sign-in frequency) alongside DQ scores and submission reconciliation.
Still stuck? Contact support or browse the Help Center.