Guides

Social SSO — Google + Discord

AdminUpdated Sep 19, 2026

Social SSO — Google + Discord

One-click agent sign-in via Google or RestoreHub Discord. BYOK client IDs; no platform credentials.

Beyond SAML/OIDC for enterprise IdPs, Chatly supports two consumer-grade social logins that need zero IdP setup on the customer side: Google and RestoreHub Discord. Both are agent- facing (logging into the dashboard, not the widget), both are BYOK at the platform-operator level, and both share the same backend endpoint shape.

Info — When to enable these

Google SSO is the quickest path for any team whose agents already use Google Workspace — one click and they're in. Discord SSO via RestoreHub is targeted at gaming, community, and creator-economy support teams whose ops happen inside Discord.

Warning — These are the only two social providers

There is no Microsoft, Apple, GitHub, or Slack agent login. (The OAuth providers listed under Integrations are a different surface — they connect third-party tools to a workspace, and none of them can sign anyone in.)

Google sign-in

Setup (operator side)

  1. Create an OAuth 2.0 Client ID in Google Cloud Console:

    • Application type: Web application

    • Authorized JavaScript origins:

      • https://app.chatlychat.com

      • http://localhost:5173 (dev)

    • You don't need a redirect URI — the dashboard uses Google Identity Services' credential (ID token) flow, not an authorization-code redirect.

  2. Copy the Client ID (123456789-abc.apps.googleusercontent.com).

  3. Set two env vars on the platform:

Parameters

Name

Type

Description

GOOGLE_CLIENT_ID

env (api)

The OAuth client ID. Used to verify aud on the incoming ID token. Optional — unset means Google sign-in is off.

VITE_GOOGLE_CLIENT_ID

env (dashboard build)

Same value. Baked into the dashboard bundle at build time so the GSI script can initialize.

Info — GOOGLE_CLIENT_SECRET is not part of this

It exists in the env schema and no code path reads it. Google sign-in verifies a signed ID token; there is no code exchange, so there is no secret to present. Calendar sync uses its own GOOGLE_CALENDAR_CLIENT_ID / _SECRET pair.

Restart the dashboard + api containers. The login page surfaces the Google button automatically when VITE_GOOGLE_CLIENT_ID is non-empty.

Flow

  1. Agent clicks the Google button on the login page.

  2. Google Identity Services renders and handles the account chooser.

  3. The browser receives a signed Google ID token (JWT) as response.credential.

  4. Dashboard POSTs { idToken } to /v1/auth/google.

  5. Backend verifies the JWT against Google's JWKS, and checks alg, iss, aud, exp, iat, and email_verified.

  6. Upserts the user by email; mints Chatly access + refresh tokens.

POST /v1/auth/google — Public

{ "idToken": "eyJhbGciOiJSUzI1NiI..." }

Returns the same AuthTokens shape as /v1/auth/login:

{
  "accessToken": "...",
  "refreshToken": "...",
  "accessTokenExpiresAt": "...",
  "refreshTokenExpiresAt": "...",
  "meta": { "workspaceId": "01920f3c-...", "userId": "01920f3d-...", "created": false }
}

created: true means we provisioned a brand-new user + workspace during this call (first-time Google login).

Security notes

Warning — Email-verified is non-negotiable

We reject tokens with email_verified: false — and with the claim absent entirely. Google rarely issues those, but if a user has linked an unverified address we refuse the login rather than risk an account-takeover via a typo in someone else's Gmail.

  • Signature verification uses Google's published JWKS at https://www.googleapis.com/oauth2/v3/certs. We cache for 1 hour.

  • The JWT header's alg must be RS256 and it must carry a kid.

  • iss must be accounts.google.com or https://accounts.google.com.

  • aud must equal GOOGLE_CLIENT_ID exactly. It is compared as a single string — an ID token with a multi-valued audience is rejected.

  • exp and iat are checked with a 30-second clock-skew tolerance.

RestoreHub Discord sign-in

RestoreHub is a service that handles the Discord OAuth dance for you: agents click → Discord popup → success → RestoreHub returns a short-lived JWT that you trade for the user's Discord identity. Chatly uses this as a dashboard login path.

Info — Why a Discord SSO proxy and not direct OAuth

RestoreHub owns the OAuth app + client secret and handles token refresh through their proxy. The trade-off: an extra hop. The win: zero Discord-developer-portal setup on your side.

Setup (operator side)

  1. Register your server at restorehub.net. Pick a slug (e.g. chatly). The slug is public — commit it to source.

  2. In RestoreHub's Widget settings, add allowed origins:

    • https://app.chatlychat.com

    • http://localhost:5173 (dev)

  3. Enable sharing Discord OAuth tokens with widget integrators. That is what unlocks the /widget/access-token exchange below.

  4. Set two env vars on the platform:

Parameters

Name

Type

Description

RESTOREHUB_SERVER_SLUG

env (api)

Presence gate only. If it is unset, POST /v1/auth/restorehub/discord answers 503. The value itself is not sent to RestoreHub and plays no part in validating the token.

VITE_RESTOREHUB_SERVER_SLUG

env (dashboard build)

Baked into the dashboard at build time so the RestoreHub widget can initialize against your server.

Restart the dashboard + api. The Discord button appears on the login page automatically.

Flow

Agent clicks "Continue with Discord"
   │
   ▼
RestoreHub widget script opens the Discord OAuth popup
   │
   ▼
Popup posts back { token } (RestoreHub JWT)
   │
   ▼
Dashboard POSTs /v1/auth/restorehub/discord
   │
   ▼
Backend Path A: POST https://api.restorehub.net/api/v1/widget/access-token
   ├─ 200 + accessToken → GET https://discord.com/api/v10/users/@me
   │                      → verified email + global_name + avatar
   ├─ 401 → reject outright, no fallthrough
   └─ anything else (403/404/5xx/network) → Path B
   │
   ▼
Backend Path B: POST https://api.restorehub.net/api/v1/widget/verify-token
   → identity only: Discord user id, and email if RestoreHub has one
   │
   ▼
Upsert user by email → mint Chatly tokens

POST /v1/auth/restorehub/discord — Public

{ "token": "eyJ...RestoreHub JWT..." }

Returns the same AuthTokens shape as Google login.

Two-path token exchange — why

/widget/access-token gives us a Discord OAuth access token, which lets us call Discord's /users/@me and read the user's verified email, display name, and avatar. That's the happy path.

If it answers anything other than 200 or 401 — or the network call throws — we fall through to /widget/verify-token, which is identity-only: we get the Discord user id and, if RestoreHub holds one, an email. A 401 means the JWT is genuinely bad, so we reject rather than fall through.

The Discord id is validated as a snowflake (17–20 digits) before it is used for anything.

Discord without a verified email

Chatly requires an email on every user (users.email is NOT NULL). If neither path yields one — including the case where Discord reports the address as unverified — we return 422 with details.reason = "discord_no_email", so the dashboard can say "verify your email in Discord settings" rather than fabricate a placeholder address.

CSP

The dashboard's CSP is generated at container start by infra/docker/dashboard-entrypoint.sh from infra/docker/nginx.dashboard.conf.template. The social-login-relevant parts:

script-src   'self' 'unsafe-inline' https://accounts.google.com https://apis.google.com https://restorehub.net …;
connect-src  'self' {api} {ws} https://accounts.google.com https://restorehub.net https://api.restorehub.net …;
frame-src    'self' https://accounts.google.com https://restorehub.net;

connect-src and frame-src are assembled from env vars — CSP_CONNECT_EXTRA and CSP_FRAME_EXTRA add origins without editing the template. Don't hand-edit the generated default.conf; it is overwritten on every start.

Common behaviour across both

Parameters

Name

Type

Description

Account linking

by email

Both providers link by email. If the address already has a live membership, the sign-in lands in the workspace of the oldest such membership — deterministic, so the same person lands in the same place every time. A soft-deleted membership does not count.

First-time sign-in

auto-creates workspace

If the email has no live membership, we provision a new workspace with the user as owner. meta.created tells the dashboard so it can route to onboarding.

Name and avatar

best-effort refresh

Taken from the provider on each sign-in, but never over a name the user already set.

Email verification

stamped

A social sign-in sets emailVerifiedAt if it was not already set — the provider has verified ownership.

Danger — Social login does not enforce TOTP

Password login runs a two-factor challenge; these two endpoints mint session tokens directly. If an agent has TOTP enrolled, a Google or Discord sign-in bypasses it. Treat the social buttons as equivalent in strength to the provider account behind them, and leave them unset on a deployment where the second factor is load-bearing.

Warning — No dedicated rate limit

/v1/auth/login, /register, and the password-reset routes each carry their own throttle bucket. The two social endpoints carry none — only whatever limits your reverse proxy applies.

Combining with workspace-level SSO

You can run social SSO alongside workspace-scoped SAML/OIDC. There is no "require SSO for this workspace" setting to conflict with: every login path a deployment has enabled stays available. To force everyone through one path, unset the social env vars — that is the switch.

Which means, concretely:

  • Enabling SAML/OIDC for a workspace does not disable password login or the social buttons for its members.

  • Social sign-in is deployment-wide, not per-workspace. One workspace cannot turn it off for itself.

Troubleshooting

Warning — Google button stays hidden

VITE_GOOGLE_CLIENT_ID wasn't baked into the dashboard build. Vite inlines env vars at build time — re-run the dashboard build with the value set.

Warning — Discord popup completes but nothing happens

Either your origin isn't in RestoreHub's allow-list, or your CSP connect-src is blocking the callback. Check DevTools console — CSP violations are reported there.

Warning — 503 on POST /v1/auth/restorehub/discord

RESTOREHUB_SERVER_SLUG is unset on the API container. The dashboard-side VITE_ variable alone is not enough — both have to be set.

Info — 'aud mismatch' on Google verify

The GOOGLE_CLIENT_ID on the api side and the VITE_GOOGLE_CLIENT_ID on the dashboard side don't match. They must be the same string — the backend rejects any token whose aud claim differs from what's configured.

Info — First-time login lands in an empty workspace

That's expected — meta.created = true means we just provisioned the workspace. The dashboard should route to the onboarding wizard.

Was this page helpful?