- 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>, valuenostackai-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, addhttps://dev-console.nostackai.com/auth/callback/googleunder 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>, valuenostackai-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)
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)
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:
| Field | Value |
|---|---|
| Provider | Okta |
| Issuer URL | https://{yourOktaDomain}/oauth2/default |
| Auth endpoint | auto-filled: {issuer}/v1/authorize |
| Token endpoint | auto-filled: {issuer}/v1/token |
| JWKS URI | auto-filled: {issuer}/v1/keys |
| Logout endpoint | auto-filled: {issuer}/v1/logout (optional) |
| Client ID | from the Okta app's General tab |
| Client Secret | not needed — PKCE, browser-safe |
| Scopes | openid email profile |
| Email domain | your company's domain, e.g. acme.com |
This flow's Okta app:
| Setting | Value |
|---|---|
| App type | Single-Page Application |
| Grant type | Authorization Code (PKCE automatic for SPA — nothing to toggle) |
| Sign-in redirect URI | https://dev-console.nostackai.com/auth/callback/okta |
| Sign-out redirect URI | {console origin}/login |
| Assignments | your 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:
| Field | Value |
|---|---|
| Issuer URL | https://{yourOktaDomain}/oauth2/default or Auth0's https://{yourAuth0Domain}/ |
| Email domain | your users' email domain, e.g. acme.com — required; resolves sign-in to this tenant |
| Auth endpoint | auto-filled from issuer |
| Token endpoint | auto-filled — stored but not used by this flow |
| JWKS URI | auto-filled from issuer |
| Logout endpoint | auto-filled, optional |
| Client ID | from this tenant's own Okta/Auth0 app |
| Client Secret | optional — implicit flow doesn't need one |
| Scopes | openid email profile |
| Active | on |
This flow's Okta/Auth0 app — a separate app from the org-level one:
| Setting | Value |
|---|---|
| App type | Single-Page Application |
| Grant type | Implicit — 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 URI | wherever your sign-in page lives — for the playground: https://playground.nostackai.com/settings |
| Assignments / Connections | this tenant's real end-users |
Playground app (Settings → Connection):
| Field | Value |
|---|---|
| Base URL / APP_URL | your app domain, e.g. https://dause2.nostackai.com |
| Org code | e.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 |
| typed 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.