Skip to content

SSO Onboarding Runbook (generic)

Customer-agnostic procedure for onboarding any organization onto Gevell’s per-tenant SAML 2.0 single sign-on. Substitute the customer’s slug for <slug> and their email domain for <domain> throughout.

The SAML Service Provider is already built and deployed. Onboarding is a configuration + coordination exercise. As of PR #2585 the required PUBLIC_BASE_URL platform fix is merged and deployed, so no code change is needed per customer.

Related: SSO architecture Β· SSO status & plan Β· UTHealth worked example Β· customer-facing docs under docs/user/docs/sso/.


0. What already exists (platform-wide, one-time)

PieceLocationState
SAML SP (metadata / login / ACS)workers/api/src/routes/sso.tsDeployed, Lambda-only mount
SAML crypto + attribute mappingworkers/api/src/services/saml.tsDeployed
JIT provisioning + guardsworkers/api/src/services/sso-provisioning.tsDeployed
Tenant-admin config APIworkers/api/src/routes/tenant-sso-admin.tsDeployed (Node + Lambda)
Tenant-admin config UIapps/web/src/app/tenant-admin/sso/Deployed
DB tables tenant_sso_config, tenant_sso_loginsmigration 254Applied in prod
PUBLIC_BASE_URL pinned on API Lambdainfra/cdk/lib/stacks/api-stack.tsDone (PR #2585)
Absolute session timeoutSupabase β†’ Auth β†’ SessionsSet 2026-09-03

Endpoints the customer’s IdP interacts with (base host is the Lambda API https://api-pdf.gevell.com):

PurposeURL
SP metadata / SP Entity IDhttps://api-pdf.gevell.com/api/sso/<slug>/metadata
ACS (Reply URL)https://api-pdf.gevell.com/api/sso/<slug>/acs
SP-initiated loginhttps://api-pdf.gevell.com/api/sso/<slug>/login?redirect=/dashboard

Binding: HTTP-POST (assertion), HTTP-Redirect (authn request). NameID: emailAddress. IdP signs the assertion; we do not sign authn requests or support encrypted assertions or SLO in v1.


1. Pre-flight (verify once, platform-wide)

Confirm the platform fixes are in place (they persist across deploys via CDK):

Terminal window
# PUBLIC_BASE_URL is pinned (function name is accessible-pdf-production-api,
# NOT accessible-pdf-prod-api)
aws lambda get-function-configuration \
--region us-east-1 \
--function-name accessible-pdf-production-api \
--query 'Environment.Variables.PUBLIC_BASE_URL' --output text
# expect: https://api-pdf.gevell.com
# The SP route answers (Unknown organization until the tenant exists β€” that's fine)
curl -s -o /dev/null -w "%{http_code}\n" https://api-pdf.gevell.com/api/sso/<slug>/metadata

/api/sso is mounted only on the Lambda (api-pdf.gevell.com) β€” never hand the customer an api.gevell.com URL; it 404s.


2. Create the tenant

Run in the prod Supabase SQL editor. Idempotent.

-- Tenant
INSERT INTO public.tenants (slug, display_name, admin_email, support_email, status)
VALUES ('<slug>', '<Display Name>', 'larry@anglin.com', 'hello@gevell.com', 'active')
ON CONFLICT (slug) DO NOTHING;
-- Email domain. Only mark verified once IAM has confirmed ownership
-- (a signed email from an @<domain> address is sufficient for a contracted customer;
-- a DNS TXT challenge is stronger). This allowlist is the anti-takeover control.
INSERT INTO public.tenant_domains (tenant_id, domain, is_primary, verified, verified_at)
SELECT id, '<domain>', TRUE, TRUE, NOW() FROM public.tenants WHERE slug = '<slug>'
ON CONFLICT DO NOTHING;
-- Tenant admin (must have signed in once so an auth.users row exists)
INSERT INTO public.tenant_admins (tenant_id, user_id, role)
SELECT t.id, u.id, 'owner'
FROM public.tenants t, auth.users u
WHERE t.slug = '<slug>' AND u.email = '<admin@domain>'
ON CONFLICT DO NOTHING;

The slug is permanent β€” it appears in every URL the IdP registers.


3. Exchange metadata

Send the customer the SP coordinates (see Β§0) and the customer-facing Connect your identity provider guide. Ask for:

  1. Their IdP metadata URL or XML.
  2. Their IdP stack (Entra ID / Shibboleth / Okta / Google) β€” attribute names differ.
  3. The exact released mail domain(s) β€” add every one to allowedEmailDomains.
  4. A technical contact for certificate-rotation notices.

4. Configure the IdP binding

Preferred: the tenant-admin UI at https://pdf.gevell.com/tenant-admin/sso (paste metadata β†’ Parse β†’ set domains + attributes β†’ Save with enabled = false).

Guard rails: enabled: true is rejected unless idpEntityId, idpSsoUrl, and idpX509Cert are all set and allowedEmailDomains is non-empty. Mutations require an owner/admin/manager role.


5. Test, then go live

  1. Set enabled = true for a test window with the customer’s IT standing by.
  2. Have a test user open https://api-pdf.gevell.com/api/sso/<slug>/login?redirect=/dashboard.
  3. Verify:
SELECT created_at, outcome, reason, email, name_id, ip
FROM public.tenant_sso_logins
WHERE tenant_id = (SELECT id FROM public.tenants WHERE slug = '<slug>')
ORDER BY created_at DESC LIMIT 20;

On outcome = success, leave enabled and deliver the login link. Record the signing cert’s notAfter and set a 60-day rotation reminder β€” cert expiry is the #1 cause of a later outage. See the UTHealth runbook Β§5–§8 for the full deny-reason table and ongoing-operations detail.


6. Rollback

UPDATE public.tenant_sso_config
SET enabled = FALSE, status = 'disabled'
WHERE tenant_id = (SELECT id FROM public.tenants WHERE slug = '<slug>');

Immediate for new logins. Existing sessions expire per the configured absolute timeout; for a hard cut-off, revoke sessions in the Supabase dashboard. See the UTHealth runbook Β§7 for the session-revocation query.


7. v1 limitations to disclose

No SLO Β· no SCIM/deprovisioning Β· no SSO enforcement Β· no SP request signing Β· no encrypted assertions Β· SSO only on pdf.gevell.com. Full detail and the session-lifetime analysis are in the UTHealth runbook Β§9 and sso-status.md. Roadmap for these is Phase 3 (issue #2588).