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_URLplatform 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)
| Piece | Location | State |
|---|---|---|
| SAML SP (metadata / login / ACS) | workers/api/src/routes/sso.ts | Deployed, Lambda-only mount |
| SAML crypto + attribute mapping | workers/api/src/services/saml.ts | Deployed |
| JIT provisioning + guards | workers/api/src/services/sso-provisioning.ts | Deployed |
| Tenant-admin config API | workers/api/src/routes/tenant-sso-admin.ts | Deployed (Node + Lambda) |
| Tenant-admin config UI | apps/web/src/app/tenant-admin/sso/ | Deployed |
DB tables tenant_sso_config, tenant_sso_logins | migration 254 | Applied in prod |
PUBLIC_BASE_URL pinned on API Lambda | infra/cdk/lib/stacks/api-stack.ts | Done (PR #2585) |
| Absolute session timeout | Supabase β Auth β Sessions | Set 2026-09-03 |
Endpoints the customerβs IdP interacts with (base host is the Lambda API
https://api-pdf.gevell.com):
| Purpose | URL |
|---|---|
| SP metadata / SP Entity ID | https://api-pdf.gevell.com/api/sso/<slug>/metadata |
| ACS (Reply URL) | https://api-pdf.gevell.com/api/sso/<slug>/acs |
| SP-initiated login | https://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):
# 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.
-- TenantINSERT 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 uWHERE 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:
- Their IdP metadata URL or XML.
- Their IdP stack (Entra ID / Shibboleth / Okta / Google) β attribute names differ.
- The exact released mail domain(s) β add every one to
allowedEmailDomains. - 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
- Set
enabled = truefor a test window with the customerβs IT standing by. - Have a test user open
https://api-pdf.gevell.com/api/sso/<slug>/login?redirect=/dashboard. - Verify:
SELECT created_at, outcome, reason, email, name_id, ipFROM public.tenant_sso_loginsWHERE 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_configSET 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).