TellyID Integration Guide
TellyID is the PlayTelly identity and account portal — a standalone React + Vite app that handles login, signup, and user account settings (profile, preferences, security). It exposes a shared session via HttpOnly cookies through AuthAPI, enabling any app in the PlayTelly ecosystem to delegate authentication to it.
Provides:
- Login / signup — credential entry and session creation
- Session check — verify an active session via CoreAPI
GET /api/v1/identity/users/me - Silent token refresh — automatic rotation via AuthAPI
/refresh - Sign-out — cookie clearing and session revocation
- Account settings — profile, preferences, security pages (hosted on TellyID)
- Redirect-back flow —
?redirect_uri=param returns user to caller after login
How It Works
TellyID sits in front of AuthAPI. Your app does not talk to AuthAPI directly for session management — it delegates to TellyID for the login UI and uses AuthAPI only to validate the session cookie after the redirect-back.
sequenceDiagram
participant App as Your App
participant TellyID
participant AuthAPI
participant CoreAPI
App->>CoreAPI: GET /api/v1/identity/users/me (check session)
CoreAPI-->>App: 401 — no valid session
App-->>TellyID: redirect to TellyID /auth?redirect_uri=<your-app>
TellyID->>AuthAPI: POST /api/v1/login
AuthAPI-->>TellyID: Set HttpOnly cookies (access_token, refresh_token)
TellyID-->>App: Redirect to redirect_uri
App->>CoreAPI: GET /api/v1/identity/users/me (cookie sent automatically)
CoreAPI-->>App: { user_id, orgs, app_roles, permissions }
Environment Variables
TellyID
| Variable | Description |
|---|---|
VITE_BACKEND_URL |
AuthAPI base URL, e.g. https://authapi.example.com |
TellyID's API client is created as:
axios.create({
baseURL: `${import.meta.env.VITE_BACKEND_URL}/api/v1`,
withCredentials: true,
})
Your App
| Variable | Required | Description |
|---|---|---|
VITE_AUTH_URL |
Yes | TellyID base URL, e.g. https://tellyid.example.com |
VITE_AUTH_API_URL |
Yes | AuthAPI base URL — used for token refresh and logout |
VITE_CORE_API_URL |
Yes | CoreAPI base URL — used to validate session and for role/permission checks |
Integration Steps
1. Create Auth API Clients
Create two separate Axios clients — one for AuthAPI (cookie-based session) and one for CoreAPI (bearer token / role checks).
// src/api/api.ts
import axios from 'axios'
// Token refresh and logout — reads HttpOnly cookies automatically
const authApiClient = axios.create({
baseURL: `${import.meta.env.VITE_AUTH_API_URL}/api/v1`,
headers: { 'Content-Type': 'application/json' },
withCredentials: true,
})
// Session validation, role and permission checks
const coreApiClient = axios.create({
baseURL: `${import.meta.env.VITE_CORE_API_URL}/api/v1`,
headers: { 'Content-Type': 'application/json' },
withCredentials: true,
})
2. Define Response Types
export interface OrgClaim {
org_id: string
org_slug: string
role: string
}
// Response from CoreAPI GET /api/v1/identity/users/me
export interface MeResponse {
user_id: string
orgs: OrgClaim[]
app_roles: { scope: string; scope_id: string; role: string }[]
permissions: string[]
}
3. Create Service Functions
export const authService = {
refresh: async () => {
const response = await authApiClient.post('/refresh', { client_type: 'web' })
return response.data
},
}
export const identityService = {
getMe: async (): Promise<MeResponse> => {
const response = await coreApiClient.get<MeResponse>('/identity/users/me')
return response.data
},
}
4. Implement the AuthGuard
The AuthGuard component wraps your entire app tree. On mount it checks the session; if there is no valid session it redirects the user to TellyID with a redirect_uri so they come back after login.
// src/components/AuthGuard.tsx
import { useState, useEffect } from 'react'
import axios from 'axios'
import { authService, identityService, type MeResponse } from '@/api/api'
const AUTH_URL = import.meta.env.VITE_AUTH_URL as string | undefined
const CORE_API_URL = import.meta.env.VITE_CORE_API_URL as string | undefined
function redirectToLogin() {
if (!AUTH_URL) return
const authUrl = new URL(`${AUTH_URL}/auth`)
authUrl.searchParams.set('redirect_uri', window.location.origin)
window.location.href = authUrl.toString()
}
async function checkAuthAndRefresh(): Promise<MeResponse | null> {
try {
return await identityService.getMe()
} catch (err) {
if (!axios.isAxiosError(err) || err.response?.status !== 401) return null
try {
await authService.refresh()
return await identityService.getMe()
} catch {
return null
}
}
}
export function AuthGuard({ children }: { children: React.ReactNode }) {
const [ready, setReady] = useState(false)
useEffect(() => {
if (!CORE_API_URL) {
setReady(true) // no auth configured — let the app boot in dev/local mode
return
}
checkAuthAndRefresh()
.then(data => {
if (data) {
setReady(true)
} else {
redirectToLogin()
}
})
.catch(() => redirectToLogin())
}, [])
if (!ready) return null
return <>{children}</>
}
5. Wire Up the Router
Wrap your <Routes> tree in <AuthGuard>:
// src/App.tsx
import { BrowserRouter, Routes, Route } from 'react-router-dom'
import { AuthGuard } from '@/components/AuthGuard'
const App = () => (
<BrowserRouter>
<Routes>
<Route
path="*"
element={
<AuthGuard>
<Routes>
<Route path="/" element={<YourHomePage />} />
{/* other routes */}
</Routes>
</AuthGuard>
}
/>
</Routes>
</BrowserRouter>
)
6. (Optional) Role Guard
If your app is org-scoped and you need to gate routes to specific roles, add an OrgRoleGuard that checks the CoreAPI response:
// src/components/OrgRoleGuard.tsx
import { useEffect } from 'react'
import { useParams, useNavigate } from 'react-router-dom'
import { useQuery } from '@tanstack/react-query'
import { identityService } from '@/api/api'
const ALLOWED_ROLES = new Set(['ORG_ADMIN', 'ORG_OWNER'])
const CORE_API_URL = import.meta.env.VITE_CORE_API_URL as string | undefined
export function OrgRoleGuard({ children }: { children: React.ReactNode }) {
const { orgSlug } = useParams<{ orgSlug: string }>()
const navigate = useNavigate()
const { data, isPending, isError } = useQuery({
queryKey: ['identity-me'],
queryFn: identityService.getMe,
enabled: !!CORE_API_URL,
})
useEffect(() => {
if (!CORE_API_URL || isPending) return
const hasAccess = !isError && !!data?.orgs.some(
org => org.org_slug === orgSlug && ALLOWED_ROLES.has(org.role)
)
if (!hasAccess) navigate(`/${orgSlug}/access-denied`, { replace: true })
}, [data, isPending, isError, orgSlug, navigate])
if (!CORE_API_URL) return <>{children}</>
if (isPending) return null
const hasAccess = !isError && !!data?.orgs.some(
org => org.org_slug === orgSlug && ALLOWED_ROLES.has(org.role)
)
return hasAccess ? <>{children}</> : null
}
Use it inside AuthGuard on org-scoped routes:
<Route path="/:orgSlug/console/*" element={
<OrgRoleGuard>
<ConsoleLayout />
</OrgRoleGuard>
} />
Redirect Flow
When TellyID's Auth page receives a redirect_uri query parameter, it redirects the browser back to that URL after a successful login. Your AuthGuard builds this URL from window.location.origin:
const authUrl = new URL(`${AUTH_URL}/auth`)
authUrl.searchParams.set('redirect_uri', window.location.origin)
window.location.href = authUrl.toString()
// → https://tellyid.example.com/auth?redirect_uri=https://your-app.example.com
After login, TellyID redirects to https://your-app.example.com, where AuthGuard re-runs the session check, finds a valid cookie, and renders the app.
Linking to TellyID Account Settings
TellyID hosts user account management under /settings/*. To send users to their profile or security settings, link directly to TellyID:
const TELLYID_URL = import.meta.env.VITE_AUTH_URL
// Profile page
window.open(`${TELLYID_URL}/settings/profile`)
// Security page
window.open(`${TELLYID_URL}/settings/security`)
TellyID's settings sidebar includes:
| Path | Description |
|---|---|
/settings/profile |
Display name, avatar, timezone |
/settings/preferences |
UI and notification preferences |
/settings/security |
Password change, active sessions |
Sign-Out
To sign the user out, call POST /api/v1/protected/logout on AuthAPI and then redirect to TellyID's /signout page (or handle it locally). The minimal local implementation:
import { authApiClient } from '@/api/api'
async function signOut() {
try {
await authApiClient.post('/protected/logout')
} catch {
// best-effort
}
window.location.href = `${import.meta.env.VITE_AUTH_URL}/signout`
}
TellyID's own /signout route clears all auth cookies and local storage keys before redirecting back to /auth.
Docker / Quickstart
TellyID is included in the PlayTelly Quickstart compose stack under compose/access/TellyID.yml:
services:
tellyid:
build:
context: ../../../TellyID
dockerfile: Dockerfile
args:
VITE_BACKEND_URL: ${BACKEND_URL}
ports:
- "5174:80"
depends_on:
- authapi
The VITE_BACKEND_URL build arg is resolved from the root .env file at compose time. TellyID is served on port 5174 in the local stack.
Summary
| Step | What to do |
|---|---|
| 1 | Point VITE_AUTH_URL at TellyID |
| 2 | Point VITE_AUTH_API_URL at AuthAPI (refresh and logout) |
| 3 | Create authApiClient with withCredentials: true |
| 4 | Implement AuthGuard — check session, redirect to TellyID on 401 |
| 5 | Wrap root <Routes> in <AuthGuard> |
| 6 | (Optional) Add OrgRoleGuard for org-scoped role enforcement |
| 7 | Set VITE_CORE_API_URL — required for session validation and permission checks |
See Also
- AuthAPI — full endpoint reference for login, refresh, and session management
- Identity API — CoreAPI identity domain, org memberships, and permissions
- Zitadel Setup — upstream identity provider configuration