• Console SSO (org-level) — signs in your own team, the org admins and developers, at https://dev-console.nostackai.com (API Studio, Tenant Management, billing). It is not sign-in for your app's users. Okta or Google is configured per org, by an org admin or developer, under Org Settings → Console SSO (a paid-plan feature) — each org has its own domain, so discovery is by email domain, and a domain can belong to only one organization (saving one another organization already uses is refused). Someone who signs in through it for the first time joins your organization: by default they wait for an org admin to approve them in Org Members (they see a waiting screen and can switch to another organization they belong to); The sign-in domain must be verified before anyone can sign in through it: add a DNS TXT record (host _nostackai-challenge.<domain>, value nostackai-verify=org:<your org code>, shown in the form) and press Verify. Once it is verified you can also choose Account approved automatically. The screen has three parts: Email and password (always on), Company sign-in (Off, Okta or OIDC, or Google Workspace, by your verified domain) and Personal accounts (Google for gmail.com today). Choosing Off under Company sign-in turns it off without losing the provider's settings or the verified domain; switching between Okta and Google Workspace keeps both; pick the provider again to turn it back on. Typing a company email on the sign-in page sends the person straight to your Okta or Google sign-in; for Google, add https://dev-console.nostackai.com/auth/callback/google under Authorized redirect URIs in your Google Cloud OAuth client.
  • Workspace SSO (tenant-level, a paid-plan feature) — signs in the real end-users of *your deployed app* at https://<app-domain>.nostackai.com/app/{orgCode}/.... Configured per workspace, under Tenant Settings → SSO, as either Google or Okta/Auth0. Google needs your Google OAuth client ID; Okta needs its issuer and client ID. Signing in through a workspace's SSO means belonging to that workspace: a new person joins it as pending (no access) until a workspace admin approves them with PUT /tenant/members (or an org admin approves them in the console: Tenant Management → App Users → Approve), or is approved automatically when the workspace chose Account approved automatically. In both cases the sign-in domain must first be proved with a DNS TXT record (host _nostackai-challenge.<domain>, value nostackai-verify=tenant:<org code>:<workspace code>, shown in Tenant Settings → SSO): until it is verified, sign-in through that domain does not work. A sign-in domain finds the workspace for a company email — never a tenant_code — and for Google it is needed only for Google Workspace company emails. Personal gmail.com accounts have their own setting, Personal Google accounts (Gmail): the client ID your app's Google button uses, registered on the workspace those people should join.

Why the storage is split

One auth table backs the consoles for every org on the platform. An app domain, once deployed, has no idea the console or that shared table even exist. Tenant SSO config lives where the app actually runs.

System diagram — org-level (Authorization Code + PKCE)

text
Org admin saves config → Org Settings → Console SSO → AUTH_TABLE (shared, all orgs)

Team member types their work email
        ↓
console-app looks up the org by EMAIL DOMAIN
        ↓
browser redirects to Okta/Auth0 (authorization code + PKCE)
        ↓
user authenticates with your IdP (SSO / MFA / whatever it enforces)
        ↓
IdP redirects to console-app's /auth/callback/okta with a CODE
        ↓
console-app exchanges the code + PKCE verifier for tokens
        (no client secret needed — PKCE replaces it)
        ↓
signed in to https://dev-console.nostackai.com

System diagram — tenant-level (Implicit)

text
Tenant admin saves config → Tenant Settings → SSO → this tenant's own APP_TABLE
        (a reverse lookup — org#<orgCode> → email domain → tenant_code — is written
        alongside it, so sign-in never needs a tenant_code from the client)

App user types their work email ("Continue with Company Email")
        ↓
your app calls POST /app/{orgCode}/ssoconfig { email } with a client_id header
        ↓ (looked up by email domain; returns issuer, client_id, auth_endpoint — never the secret)
browser redirects to Okta/Auth0 (implicit flow — response_type=id_token)
        ↓
user authenticates with your IdP
        ↓
IdP redirects back to YOUR OWN page (e.g. /settings) with the
        ID TOKEN in the URL fragment — no code, no exchange step
        ↓
your app calls GET /app/{orgCode}/token with that ID token
        ↓
nostackai decodes the (still unverified) token's email, resolves the SAME tenant by
        domain, then verifies signature (JWKS) + issuer + audience (= client_id)
        ↓
signed in — real app JWT issued

Setup reference — who configures what, and where

Org-level and tenant-level SSO each need their own, separate Okta/Auth0 app — different grant types and redirect URIs mean you can't reuse one app integration for both.

Org-level SSO — Org Settings → Console SSO:

FieldValue
ProviderOkta
Issuer URLhttps://{yourOktaDomain}/oauth2/default
Auth endpointauto-filled: {issuer}/v1/authorize
Token endpointauto-filled: {issuer}/v1/token
JWKS URIauto-filled: {issuer}/v1/keys
Logout endpointauto-filled: {issuer}/v1/logout (optional)
Client IDfrom the Okta app's General tab
Client Secretnot needed — PKCE, browser-safe
Scopesopenid email profile
Email domainyour company's domain, e.g. acme.com

This flow's Okta app:

SettingValue
App typeSingle-Page Application
Grant typeAuthorization Code (PKCE automatic for SPA — nothing to toggle)
Sign-in redirect URIhttps://dev-console.nostackai.com/auth/callback/okta
Sign-out redirect URI{console origin}/login
Assignmentsyour own team members

No playground involvement here — you test this by signing in at https://dev-console.nostackai.com itself.

Tenant-level SSO — Tenant Settings → SSO:

FieldValue
Issuer URLhttps://{yourOktaDomain}/oauth2/default or Auth0's https://{yourAuth0Domain}/
Email domainyour users' email domain, e.g. acme.com — required; resolves sign-in to this tenant
Auth endpointauto-filled from issuer
Token endpointauto-filled — stored but not used by this flow
JWKS URIauto-filled from issuer
Logout endpointauto-filled, optional
Client IDfrom this tenant's own Okta/Auth0 app
Client Secretoptional — implicit flow doesn't need one
Scopesopenid email profile
Activeon

This flow's Okta/Auth0 app — a separate app from the org-level one:

SettingValue
App typeSingle-Page Application
Grant typeImplicit — Okta: enable "Allow ID Token with implicit grant type" on the Sign On tab; Auth0: check "Implicit" under Advanced Settings → Grant Types
Sign-in redirect URIwherever your sign-in page lives — for the playground: https://playground.nostackai.com/settings
Assignments / Connectionsthis tenant's real end-users

Playground app (Settings → Connection):

FieldValue
Base URL / APP_URLyour app domain, e.g. https://dause2.nostackai.com
Org codee.g. sellif-ftqf
Client ID (app)your app's own registered client_id — sent as the client_id header on both /token and /ssoconfig — not the Okta/Auth0 client id
Emailtyped fresh in the OAuth/Okta SSO section at sign-in time — no tenant_code field exists

The "Client ID" naming collision

Tenant Settings' Client ID identifies your app to Okta/Auth0. The playground's Connection "Client ID" identifies your app to nostackai itself — sent as a header on both /token and /ssoconfig — and has nothing to do with your IdP. They're unrelated values that happen to share a name. You never type the Okta/Auth0 client id into the playground at all — it's fetched live from POST /ssoconfig the moment you click sign-in.

What client_id and client_secret actually do

client_id is used twice: embedded in the browser's redirect to your IdP (so it knows which registered app is asking, and checks the redirect URI against that app's records), and again server-side, where nostackai requires the returned ID token's audience claim to match it — rejecting a token that was issued for a different application. client_secret is never used anywhere in this implicit flow — not sent by the browser, not read by verification — it's stored encrypted purely for a possible future server-side flow.