> ## Documentation Index
> Fetch the complete documentation index at: https://docs.myfundingmachine.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Sso custom page runbook

# Marketplace SSO (Custom Page) runbook

Operating notes for the CRM Custom Page sign-in (FM-2892). Internal: user-facing
copy says "CRM".

## How sign-in works

* **Custom Page.** The page asks its CRM parent for `REQUEST_USER_DATA` and
  posts the encrypted answer to `POST /api/auth/validate-sso`, with the page's
  `location_id`. The route decrypts it with the Shared Secret and mints a
  session bound to the payload's `activeLocation`, with `authSource: 'sso'`.
* **Custom Menu Link.** The parent never answers, so the client falls back to
  `POST /api/auth/location-session`, as before.
* **Reload.** A cached SSO session, or any cached session on a URL with
  `fm_entry=custom_page`, is re-checked with the CRM once per page load and
  replaced when the CRM user or Location changed. An admin viewing another
  Location's page is re-bound to the CRM's own Location rather than refused.

`validate-sso` refuses (and mints nothing) when the payload does not decrypt,
has no `activeLocation`, names a Location other than the page's `location_id`,
or names a user the CRM no longer lists on that Location when a new 24-hour
window opens. While the CRM can't be reached it answers 503.

## Console prerequisites (Brock)

1. A Custom Page module, sub-account distribution, in the left navigation.
   URL: `https://<app host>/dashboard?location_id={{location.id}}&fm_entry=custom_page`.
   There is no user-id placeholder; the user comes from the signed payload.
   `fm_entry=custom_page` tells the page to re-check any cached session with
   the CRM on load, so a session left over from a menu link in the same tab is
   replaced by the CRM's signed one.
2. The Shared Secret. Production decrypts with `GHL_SSO_KEY` when set, else
   `GHL_APP_SSO_KEY` (on 2026-10-22 only `GHL_APP_SSO_KEY` exists in Vercel). If
   the console secret is not the value of `GHL_APP_SSO_KEY`, add `GHL_SSO_KEY`
   with the console value. Never change or rotate `GHL_APP_SSO_KEY`: it also
   signs webhooks, billing alerts, report tokens and legacy HS256 sessions. Do
   not regenerate the console secret.

## Turn the gate on for one Location

Admin → Settings → Labs → **Require CRM sign-in page** → Manage → select the
Location → **Enable Access** → Save.

* Never Enable Visibility: an operator who can see the card can turn it on and
  lose the menu-link sign-in.
* The agency-wide switch has no effect on this card, by design.
* First confirm someone on that Location can open the Custom Page and gets
  signed in (`[validate-sso] minted` for that `locationId`).

With the card on, `location-session` refuses that Location (403
`sso_required`, logged as `[location-session] refused` with
`reason: "sso_required"`). A staff session on that Location (super-admin
email, admin Location, or a CRM agency user) that did not come from the Custom
Page stops verifying at once, so it grants nothing anywhere, Convex included;
inside the CRM frame the client recovers by signing in again through the
Custom Page. An operator's older menu-link session keeps working until it
expires (within 24 hours).

## Rollback

Turn **Enable Access** off for the Location. It takes effect on the next
request; nothing to deploy. For the code, revert the merge commit.

## Recovery: a gated admin whose Custom Page does not work

The gate is per Location. FM agency staff and super-admin emails keep admin
access in every Location where the card is off. Open Funding Machine from such
a sub-account's menu link, go to Admin → Settings → Labs, and turn the card
off for the gated Location.

Last resort, a production write that needs explicit approval naming it: delete
the `labsFeatureStates` row with `scope: "location"`, `scopeId: <locationId>`,
`featureKey: "sso-required"`.

## Known limits

* **Replay.** The CRM puts no timestamp in the payload, so a captured payload
  for a user the CRM still lists can be replayed. The 24-hour window
  (`locationSessionAuthAnchors`) keeps every session minted from it inside 24
  hours of the sign-in that opened the window, and a new window needs a live CRM
  check, so a removed user's payload stops working within 24 hours.
* **Agency users of a partner agency** are accepted when the Location's own
  token can read them as agency users; if it can't, the live check refuses
  them. FM agency staff are checked with FM's agency credentials.
* `proxy.ts` still redirects API requests carrying `?sso_key=` to the removed
  GET form of `validate-sso`, which now answers 405. Nothing sent such requests
  from 2026-09-22 to 2026-10-22; the proxy cleanup belongs to the Bearer-first
  step.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.