Skip to content

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