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_DATAand posts the encrypted answer toPOST /api/auth/validate-sso, with the page’slocation_id. The route decrypts it with the Shared Secret and mints a session bound to the payload’sactiveLocation, withauthSource: '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)
- 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_pagetells 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. - The Shared Secret. Production decrypts with
GHL_SSO_KEYwhen set, elseGHL_APP_SSO_KEY(on 2026-10-22 onlyGHL_APP_SSO_KEYexists in Vercel). If the console secret is not the value ofGHL_APP_SSO_KEY, addGHL_SSO_KEYwith the console value. Never change or rotateGHL_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] mintedfor thatlocationId).
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 thelabsFeatureStates 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.tsstill redirects API requests carrying?sso_key=to the removed GET form ofvalidate-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.

