---
title: "Authentication"
description: "Auth.js v5 trust spine — Google, Telegram, Apple, Ring Mailer, phone OTP, crypto wallet; PIN is post-auth wallet unlock (not a login provider)"
locale: "en"
---
# Authentication

> **Info**
> Use **Founder** / **Developer** tabs in the docs sidebar to filter this page. Adapter split, JWT callbacks, and OIDC modules: [Authentication architecture](/docs/architecture/authentication.md).

  Ring signs members in with **Auth.js v5** (JWT sessions). Shipped login paths: **Ring Mailer** (OTP / magic / password), **Google** (OAuth + One Tap), **Telegram** (web OIDC **or** Mini App `initData`), **Apple**, **phone OTP** (Telegram Gateway / WhatsApp), and **crypto wallet** (nonce + signature). **PIN is not a login provider** — it wraps custodial wallet keys after the member already has a session. Email is **not** Resend — see [Ring Mailer](/docs/features/ring-mailer.md).

## Provider comparison

| Path | Auth.js id / type | Member experience | Env to enable | Vitals gate? |
|------|-------------------|-------------------|---------------|--------------|
| Ring Mailer OTP | Credentials `email-otp` | Email + one-time code | `SMTP_*` or `EMAIL_MODE=ethereal`; `OTP_HMAC_SECRET` (or `AUTH_SECRET`) | Yes |
| Ring Mailer magic | Credentials `email-magic` | Link → `/verify#token=…` | Same SMTP / Ethereal | Yes |
| Email password | Credentials `credentials` | Email + password (after set/reset) | Same + stored `passwordHash` | No (not in gate set) |
| Phone OTP | Credentials `phone-otp` | E.164 + code | `TELEGRAM_GATEWAY_TOKEN` and/or WhatsApp Cloud auth template | Yes |
| Google OAuth | `GoogleProvider` | Full redirect consent | `AUTH_GOOGLE_ID` / `AUTH_GOOGLE_SECRET` | Yes |
| Google One Tap | Credentials `google-one-tap` | GIS JWT, verified in `signIn` | Same Google client (audiences) | Yes |
| Apple | `AppleProvider` | Sign in with Apple | `AUTH_APPLE_ID` / `AUTH_APPLE_SECRET` | Yes |
| Telegram web | Custom OIDC `telegram` | `signIn('telegram')` → `oauth.telegram.org` | `AUTH_TELEGRAM_ID` + `AUTH_TELEGRAM_SECRET` | Yes |
| Telegram Mini App | Credentials `telegram-miniapp` | `signIn(…, { initData })` | Bot token via `getTelegramMiniAppBotToken()` | No (resolver supplies name; not in gate list as `telegram-miniapp`) |
| Crypto wallet | Credentials `crypto-wallet` | Address + signed nonce | DB + wallet nonce helpers | Yes |
| Wallet PIN | *Not Auth.js* | Unlock / wrap custodial key | `WALLET_ENCRYPTION_KEY`; Server Actions — **not** `POST /api/wallet/pin-access` (410 Gone) | N/A |

GitHub / Discord OAuth are **not** in the default `auth.ts` provider list.

| Previous / myth | Ring equivalent |
|-----------------|-----------------|
| Resend magic links (`AUTH_RESEND_KEY`) | Ring Mailer SMTP / Ethereal — Resend removed |
| PIN as fifth “login provider” | Post-auth wallet encryption / access tokens — see [Wallet](/docs/features/wallet.md) |
| Firebase Auth client sessions | Auth.js JWT; Firebase Admin only in `firebase-full` adapter path |
| `/register` password signup | Redirects to `/login` — OTP / magic is the product surface |

## Protection model

| Layer | Role |
|-------|------|
| `proxy.ts` | Locale rewrite + optimistic redirect to `ROUTES.LOGIN(locale)` (cookie presence only) |
| `proxy.ts` soft-gate | If JWT `needsOnboarding === true` → `/{locale}/login/onboarding` (exempt: `/login*`, `/auth*`, `/register*`, home `/`) |
| `(authenticated)/[locale]/layout.tsx` | `await auth()` — canonical session gate |
| `(admin)/[locale]/layout.tsx` | Role check (admin / superadmin) |
| `/login` | `LoginAuthenticatedRedirect` — client `useSession` bounce (skips during OAuth callback params) |
| `/register` | Redirects to `/login` |
| API routes | Per-handler `auth()` |

OAuth callbacks: `/api/auth/*` (excluded from intl middleware). Login path SSOT: `ROUTES.LOGIN(locale)` with unified `from` / `callbackUrl` / `returnTo`. Deep-dive: [Proxy and intl](/docs/architecture/proxy-and-intl.md), [Authentication architecture](/docs/architecture/authentication.md).

### Telegram surfaces (do not mix crypto)

| Surface | Purpose | Secret / crypto |
|---------|---------|-----------------|
| Login via Telegram (web) | Browser session | OIDC — `AUTH_TELEGRAM_ID` / `AUTH_TELEGRAM_SECRET` |
| Mini App `initData` | Silent session in WebApp | HMAC `WebAppData` — prefer `TELEGRAM_MINI_APP_BOT_TOKEN` |
| Link Telegram (profile) | Bind Telegram id while logged in | Login Widget HMAC `SHA256(bot_token)` — `ADMIN_BOT_TOKEN` / `TELEGRAM_LOGIN_BOT_TOKEN` |
| Admin bot | Moderate from chat | Bot API + whitelist — [Manage via Telegram](/docs/features/manage-via-telegram.md) |
| Stars membership | XTR invoices | Same Mini App bot token family — [Subscriptions](/docs/features/subscriptions.md) |

Do **not** reuse Mini App `initData` HMAC for Login Widget linking, and do **not** use Login Widget math for Mini App auth.

## Recommended path

Use the Auth.js specialist playbook (Cursor skill **`authjs-specialist`**) so env and provider wiring stay edge-safe.

**Starter prompt (pause for approval before mutating secrets / cluster):**

> Enable Auth.js v5 login for this Ring clone: prefer Google + Ring Mailer Ethereal for local, Telegram OIDC for UA acquisition. Confirm `auth.config.ts` stays provider-empty; list exact `AUTH_*` / `SMTP_*` / Telegram tokens to set; do not paste production secrets into docs or chat.

**Inputs checklist**

- Clone origin URL (callback base)
- At least one social pair **or** SMTP / Ethereal
- Optional: Telegram BotFather Web Login Client ID/Secret (separate from bot API token)
- Optional: `TELEGRAM_GATEWAY_TOKEN` for phone OTP
- Never commit real client secrets into MDX or git

The skill guides configuration; it does **not** mutate cluster secrets until you approve.

## Manual path

### Prerequisites

- `AUTH_SECRET` set; `AUTH_TRUST_HOST=true` behind reverse proxies
- `DB_BACKEND_MODE` chosen (`k8s-postgres-fcm` | `supabase-fcm` | `firebase-full`) so the Auth.js adapter resolves
- Provider console apps created (Google / Apple / BotFather) with callback `{origin}/api/auth/callback/`

### Pick one smoke path

Local: `EMAIL_MODE=ethereal` + `signIn('email-otp')`, **or** Google with `AUTH_GOOGLE_ID` / `AUTH_GOOGLE_SECRET`. Confirm session cookie and `/profile` load.

### Add Telegram web OIDC (optional)

BotFather → **Bot Settings → Web Login** → Allowed URLs: site origin + `/api/auth/callback/telegram`. Set `AUTH_TELEGRAM_ID` / `AUTH_TELEGRAM_SECRET`. Keep bot API token for profile linking / Mini App separately.

### Add phone OTP or wallet (optional)

Phone: `TELEGRAM_GATEWAY_TOKEN` (and optional WhatsApp Cloud AUTHENTICATION template). Wallet: ensure DB can store nonce / user wallet docs, then `signIn('crypto-wallet', { walletAddress, signedNonce })`. PIN setup belongs to [Wallet](/docs/features/wallet.md) after login — do not treat `POST /api/wallet/pin-access` as live (returns **410**).

### Verify vitals soft-gate

Incomplete display name on gated providers sets `needsOnboarding`. Confirm redirect to `/login/onboarding` then clear via session `update`.

### For founders

## Why this matters for your clone

- Members need **at least one** reliable path (Google, Telegram, Apple, or Ring Mailer). Everything else is acquisition coverage.
- White-label production mail should be **domain-branded** via Ring Mailer or [RingdomX Mail](/docs/features/ring-mailer.md) — not a third-party Resend key.
- Sessions are platform UUIDs (`users.id`). Email can link accounts across providers when linking is enabled; Telegram-only users may have empty email (partial unique index allows it).
- **PIN** is a wallet trust control (4-digit wrap / access tokens in the wallet stack), not a substitute for Google/Telegram/Apple login.

### Typical scenarios

  
  
  
  
  

  Do not create a Resend API key. Configure Auth `SMTP_*` or `EMAIL_MODE=ethereal`. CRM inbox SMTP is a separate plane — [Email AI-CRM](/docs/features/email-ai-crm.md).

### For developers

## Auth.js layout

| File | Role |
|------|------|
| `auth.config.ts` | Edge-safe config (**no** providers) |
| `auth.ts` | Full providers + adapter + JWT / session / `signIn` callbacks |
| `lib/auth-adapter-singleton.ts` | Cached PostgreSQL or Firebase adapter by `DB_BACKEND_MODE` |
| `lib/auth/telegram-oidc.ts` | `TelegramOidcProvider`, JWKS verify |
| `lib/auth/telegram-miniapp-initdata.ts` | WebAppData HMAC + `getTelegramMiniAppBotToken` |
| `lib/auth/telegram-login-widget-hash.ts` | Legacy widget HMAC |
| `features/auth/services/user-resolve.ts` | `resolveOrCreateTelegramUser`, link / unlink helpers |
| `features/auth/lib/vitals-onboarding.ts` | `isVitalsGatedProvider` / `userNeedsVitalsOnboarding` |
| `features/auth/services/phone-otp-delivery.ts` | Gateway / WhatsApp delivery |
| `app/api/auth/[...nextauth]/route.ts` | Auth.js handlers |
| `app/api/auth/telegram/callback/route.ts` | **Linking only** — requires existing session |

OIDC registers only when `isTelegramOidcConfigured()` (`AUTH_TELEGRAM_ID` + `AUTH_TELEGRAM_SECRET`). Credentials `telegram-miniapp` is always registered; authorize fails closed without bot token / valid HMAC. Mini App token fallbacks: `TELEGRAM_MINI_APP_BOT_TOKEN` → `TELEGRAM_BOT_TOKEN` → `ADMIN_BOT_TOKEN` → `TELEGRAM_LOGIN_BOT_TOKEN` → `N9LIFE_BOT_TOKEN`.

Session: JWT strategy, `maxAge` 30 days, `updateAge` 24 hours (`auth.ts`).

### Vitals gate set

`isVitalsGatedProvider`: `google`, `google-one-tap`, `apple`, `email-otp`, `email-magic`, `phone-otp`, `telegram`, `crypto-wallet`. Incomplete name (and email for wallet) sets `needsOnboarding`; `proxy.ts` soft-redirects protected routes.

### Minimum env (placeholders only)

{`AUTH_SECRET=...
AUTH_TRUST_HOST=true

AUTH_GOOGLE_ID=
AUTH_GOOGLE_SECRET=
AUTH_TELEGRAM_ID=
AUTH_TELEGRAM_SECRET=
# AUTH_APPLE_ID= / AUTH_APPLE_SECRET=

# Phone OTP (optional):
# TELEGRAM_GATEWAY_TOKEN=
# TELEGRAM_GATEWAY_SENDER_USERNAME=
# TELEGRAM_GATEWAY_CALLBACK_URL=   # optional URL Telegram Gateway may call; wire only if you own the handler

# Profile Link Telegram (widget):
# ADMIN_BOT_TOKEN= / TELEGRAM_LOGIN_BOT_TOKEN=
# NEXT_PUBLIC_TELEGRAM_LOGIN_BOT_USERNAME=YourBotUsername

# Mini App + Stars:
# TELEGRAM_MINI_APP_BOT_TOKEN=
# TELEGRAM_STARS_WEBHOOK_SECRET=

EMAIL_MODE=ethereal
# or SMTP_HOST / SMTP_USER / SMTP_PASSWORD / SMTP_FROM
# OTP_HMAC_SECRET=`}

  Primary Telegram **web** path is redirect. Login routes may set `Cross-Origin-Opener-Policy: same-origin-allow-popups` so optional Telegram popups work. Strict `same-origin` COOP breaks that library. Mini App posts `initData` — no popup. Profile linking uses the Login Widget without COOP changes on `/profile`.

### Tests

- `__tests__/auth/telegram-miniapp-initdata.test.ts`
- `__tests__/auth/telegram-login-crypto.test.ts`

Full OIDC / adapter detail: [Authentication architecture](/docs/architecture/authentication.md). Integrator snippets: [Examples — authentication](/docs/examples/authentication.md).

## Frequently asked questions

### Impact

#### Will my members still sign in if I only enable Google?

Yes. Providers are additive. Unconfigured Telegram OIDC simply omits the OIDC provider from `auth.ts`; Mini App Credentials still registers but fails authorize without a bot token.

#### Does PIN replace social login?

No. PIN wraps or unlocks custodial wallet material after Auth.js already issued a session. Treat login and wallet PIN as separate trust layers.

#### Is `/register` still a signup page?

No. It redirects to `/login` (locale + return params preserved). Use OTP / magic / social on the login surface.

### Migration

#### We still have `AUTH_RESEND_KEY` in old runbooks

Remove it. Wire Ring Mailer `SMTP_*` or Ethereal. Token table migration: `038_email_login_tokens.sql` (see Ring Mailer docs).

#### We called `POST /api/wallet/pin-access`

That HTTP route returns **410 Gone** (custodial signing is server-side). Use wallet Server Actions / [Wallet](/docs/features/wallet.md) docs for current PIN-wrap and access-token flows.

### Ops

#### Telegram Login works in BotFather but not on the site

Confirm Allowed URLs include both origin and `/api/auth/callback/telegram`, and that `AUTH_TELEGRAM_*` are the **Web Login** client credentials — not the bot API token used for admin / widget / Mini App.

#### Phone OTP never arrives

Check `TELEGRAM_GATEWAY_TOKEN` (and optional WhatsApp Cloud AUTHENTICATION template + `OTP_HMAC_SECRET`). Delivery is owned by `features/auth/services/phone-otp-delivery.ts`, not Ring Mailer SMTP.

#### Sessions drop behind a reverse proxy

Set `AUTH_TRUST_HOST=true` and a stable public origin for callbacks. See [Deployment — environment](/docs/deployment/environment.md).

## Related documentation

  
- [architecture/authentication](/docs/architecture/authentication.md) — Deep-dive: Auth.js file split, adapters, OIDC + Mini App modules.

  
- [examples/authentication](/docs/examples/authentication.md) — Next-step: copy-paste signIn(

  
- [features/ring-mailer](/docs/features/ring-mailer.md) — Same-workflow: email OTP / magic when members skip social login.

  
- [features/wallet](/docs/features/wallet.md) — Depends-on: custodial wallet PIN wrap and access tokens after login.

  
- [features/email-ai-crm](/docs/features/email-ai-crm.md) — See-also: community inbox SMTP is a separate plane from Auth Ring Mailer.

  
- [features/manage-via-telegram](/docs/features/manage-via-telegram.md) — See-also: admin Telegram bot — different token and RBAC from member Login.

  
- [features/subscriptions](/docs/features/subscriptions.md) — Next-step: telegram_stars SubscriptionConductor provider (XTR invoices).

  
- [deployment/environment](/docs/deployment/environment.md) — Depends-on: full AUTH_* and SMTP env reference for clones.

  
- [api/authentication](/docs/api/authentication.md) — See-also: Auth.js session endpoints and role hierarchy.
