Skip to main content

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.