Created
January 30, 2026 20:59
-
-
Save jalehman/f290044b1012bc75f1d0092e91a9bf56 to your computer and use it in GitHub Desktop.
LSE Identity Architecture - Option A (Separate Audience)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>LSE Identity Architecture — Option A (Separate Audience)</title> | |
| <style> | |
| :root { | |
| --bg: #0f172a; | |
| --surface: #1e293b; | |
| --surface2: #334155; | |
| --border: #475569; | |
| --text: #e2e8f0; | |
| --text-dim: #94a3b8; | |
| --accent: #38bdf8; | |
| --accent2: #818cf8; | |
| --accent3: #34d399; | |
| --accent4: #fb923c; | |
| --accent5: #f472b6; | |
| --danger: #f87171; | |
| } | |
| * { box-sizing: border-box; margin: 0; padding: 0; } | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; | |
| background: var(--bg); | |
| color: var(--text); | |
| padding: 24px; | |
| line-height: 1.6; | |
| } | |
| h1 { font-size: 1.6em; margin-bottom: 4px; color: var(--accent); } | |
| h2 { font-size: 1.2em; margin: 32px 0 16px; color: var(--accent2); border-bottom: 1px solid var(--surface2); padding-bottom: 8px; } | |
| h3 { font-size: 1em; margin: 16px 0 8px; color: var(--accent3); } | |
| .subtitle { color: var(--text-dim); font-size: 0.9em; margin-bottom: 24px; } | |
| .diagram { | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| border-radius: 12px; | |
| padding: 24px; | |
| margin: 16px 0; | |
| overflow-x: auto; | |
| } | |
| .flow-container { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 24px; | |
| min-width: 700px; | |
| } | |
| .row { | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 16px; | |
| flex-wrap: nowrap; | |
| } | |
| .box { | |
| border: 2px solid var(--border); | |
| border-radius: 10px; | |
| padding: 12px 16px; | |
| text-align: center; | |
| min-width: 140px; | |
| position: relative; | |
| } | |
| .box .label { font-weight: 600; font-size: 0.9em; } | |
| .box .detail { font-size: 0.75em; color: var(--text-dim); margin-top: 4px; } | |
| .box.auth0 { border-color: var(--accent4); background: rgba(251,146,60,0.08); } | |
| .box.identity { border-color: var(--accent); background: rgba(56,189,248,0.08); } | |
| .box.app { border-color: var(--accent2); background: rgba(129,140,248,0.08); } | |
| .box.db { border-color: var(--accent3); background: rgba(52,211,153,0.08); } | |
| .box.m2m { border-color: var(--accent5); background: rgba(244,114,182,0.08); } | |
| .arrow { | |
| color: var(--text-dim); | |
| font-size: 0.8em; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| gap: 2px; | |
| min-width: 80px; | |
| } | |
| .arrow .arrow-line { font-size: 1.2em; letter-spacing: -2px; } | |
| .arrow .arrow-label { font-size: 0.7em; text-align: center; max-width: 100px; } | |
| .v-arrow { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| color: var(--text-dim); | |
| font-size: 0.75em; | |
| padding: 4px 0; | |
| } | |
| .v-arrow .v-line { font-size: 1.2em; } | |
| .section { | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| border-radius: 12px; | |
| padding: 20px; | |
| margin: 12px 0; | |
| } | |
| .config-grid { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 12px; | |
| margin-top: 12px; | |
| } | |
| @media (max-width: 600px) { | |
| .config-grid { grid-template-columns: 1fr; } | |
| } | |
| .config-item { | |
| background: var(--surface2); | |
| border-radius: 8px; | |
| padding: 12px; | |
| } | |
| .config-item .key { color: var(--accent); font-weight: 600; font-size: 0.85em; } | |
| .config-item .val { color: var(--text); font-size: 0.85em; margin-top: 4px; } | |
| .config-item .note { color: var(--text-dim); font-size: 0.75em; margin-top: 4px; } | |
| .step-list { | |
| list-style: none; | |
| counter-reset: steps; | |
| } | |
| .step-list li { | |
| counter-increment: steps; | |
| padding: 12px 0 12px 48px; | |
| position: relative; | |
| border-bottom: 1px solid var(--surface2); | |
| } | |
| .step-list li:last-child { border-bottom: none; } | |
| .step-list li::before { | |
| content: counter(steps); | |
| position: absolute; | |
| left: 0; | |
| top: 12px; | |
| width: 32px; | |
| height: 32px; | |
| background: var(--surface2); | |
| border-radius: 50%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-weight: 700; | |
| font-size: 0.85em; | |
| color: var(--accent); | |
| } | |
| .step-list li strong { color: var(--accent3); } | |
| code { | |
| background: var(--surface2); | |
| padding: 2px 6px; | |
| border-radius: 4px; | |
| font-size: 0.85em; | |
| color: var(--accent); | |
| } | |
| .badge { | |
| display: inline-block; | |
| padding: 2px 8px; | |
| border-radius: 4px; | |
| font-size: 0.7em; | |
| font-weight: 600; | |
| text-transform: uppercase; | |
| } | |
| .badge.new { background: rgba(52,211,153,0.2); color: var(--accent3); } | |
| .badge.exists { background: rgba(148,163,184,0.2); color: var(--text-dim); } | |
| .badge.m2m-badge { background: rgba(244,114,182,0.2); color: var(--accent5); } | |
| .token-table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| margin-top: 12px; | |
| font-size: 0.85em; | |
| } | |
| .token-table th { | |
| text-align: left; | |
| padding: 8px 12px; | |
| background: var(--surface2); | |
| color: var(--accent); | |
| font-weight: 600; | |
| } | |
| .token-table td { | |
| padding: 8px 12px; | |
| border-bottom: 1px solid var(--surface2); | |
| } | |
| .token-table tr:last-child td { border-bottom: none; } | |
| </style> | |
| </head> | |
| <body> | |
| <h1>🏗️ LSE Identity Architecture</h1> | |
| <p class="subtitle">Option A — Separate Auth0 API audience for identity-api</p> | |
| <!-- SYSTEM DIAGRAM --> | |
| <h2>System Overview</h2> | |
| <div class="diagram"> | |
| <div class="flow-container"> | |
| <!-- Top row: LSE Apps --> | |
| <div class="row"> | |
| <div class="box app"> | |
| <div class="label">🎳 Bowler Connect</div> | |
| <div class="detail">connect.luckystrike.cloud</div> | |
| <div class="detail"><span class="badge exists">exists</span></div> | |
| </div> | |
| <div class="box app"> | |
| <div class="label">🎁 Punchh</div> | |
| <div class="detail">Loyalty platform</div> | |
| <div class="detail"><span class="badge exists">future</span></div> | |
| </div> | |
| <div class="box app"> | |
| <div class="label">📱 App8</div> | |
| <div class="detail">Mobile ordering</div> | |
| <div class="detail"><span class="badge exists">future</span></div> | |
| </div> | |
| <div class="box app"> | |
| <div class="label">🌐 LSE Website</div> | |
| <div class="detail">Marketing site</div> | |
| <div class="detail"><span class="badge exists">future</span></div> | |
| </div> | |
| </div> | |
| <!-- Arrow down: All redirect to Auth0 --> | |
| <div class="v-arrow"> | |
| All apps redirect to Auth0 for login (Universal Login) | |
| <div class="v-line">▼ ▼ ▼ ▼</div> | |
| SSO: Auth0 session cookie skips re-auth across apps | |
| </div> | |
| <!-- Auth0 Tenant --> | |
| <div class="row"> | |
| <div class="box auth0" style="min-width: 500px;"> | |
| <div class="label">🔐 Auth0 Tenant</div> | |
| <div class="detail">dev-nx1ufn2l3q7v1wle.us.auth0.com</div> | |
| <div class="detail" style="margin-top: 8px; color: var(--text);"> | |
| Session: 30d idle / 30d absolute • Refresh Token Rotation: ON | |
| </div> | |
| <div style="display: flex; gap: 12px; justify-content: center; margin-top: 12px; flex-wrap: wrap;"> | |
| <div style="background: rgba(251,146,60,0.15); padding: 6px 10px; border-radius: 6px; font-size: 0.75em;"> | |
| <strong>Post-Login Action</strong><br>Calls POST /resolve on identity-api | |
| </div> | |
| <div style="background: rgba(251,146,60,0.15); padding: 6px 10px; border-radius: 6px; font-size: 0.75em;"> | |
| <strong>Custom Claims</strong><br>Sets <code>identity_id</code> (UUID) on tokens | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Arrow down: Post-Login Action calls identity-api --> | |
| <div class="row" style="gap: 80px;"> | |
| <div class="v-arrow" style="flex: 1;"> | |
| <div>User tokens</div> | |
| <div class="v-line">▼</div> | |
| <div>audience: <code>api.luckystrike.cloud</code></div> | |
| </div> | |
| <div class="v-arrow" style="flex: 1;"> | |
| <div>M2M token (Post-Login Action)</div> | |
| <div class="v-line">▼</div> | |
| <div>audience: <code>identity.luckystrike.cloud</code></div> | |
| </div> | |
| </div> | |
| <!-- Services row --> | |
| <div class="row"> | |
| <div class="box app" style="flex: 1;"> | |
| <div class="label">⚡ Bowler API</div> | |
| <div class="detail">api.luckystrike.cloud</div> | |
| <div class="detail">Audience: <code>api.luckystrike.cloud</code></div> | |
| <div class="detail"><span class="badge exists">exists</span></div> | |
| </div> | |
| <div class="arrow"> | |
| <div class="arrow-label">reads canonical user ID from JWT claim</div> | |
| <div class="arrow-line">◄─────</div> | |
| </div> | |
| <div class="box identity" style="flex: 1;"> | |
| <div class="label">🪪 Identity API</div> | |
| <div class="detail">identity.luckystrike.cloud</div> | |
| <div class="detail">Audience: <code>identity.luckystrike.cloud</code></div> | |
| <div class="detail"><span class="badge new">new</span></div> | |
| </div> | |
| </div> | |
| <!-- Arrow down to DB --> | |
| <div class="row" style="gap: 80px;"> | |
| <div class="v-arrow" style="flex: 1;"> | |
| <div class="v-line">▼</div> | |
| <div>public schema</div> | |
| </div> | |
| <div class="v-arrow" style="flex: 1;"> | |
| <div class="v-line">▼</div> | |
| <div>identity schema</div> | |
| </div> | |
| </div> | |
| <!-- Database --> | |
| <div class="row"> | |
| <div class="box db" style="min-width: 400px;"> | |
| <div class="label">🗄️ connected_play database</div> | |
| <div style="display: flex; gap: 12px; justify-content: center; margin-top: 8px;"> | |
| <div style="background: rgba(52,211,153,0.15); padding: 6px 10px; border-radius: 6px; font-size: 0.75em;"> | |
| <strong>public.*</strong><br>customer, game, lane, etc. | |
| </div> | |
| <div style="background: rgba(56,189,248,0.15); padding: 6px 10px; border-radius: 6px; font-size: 0.75em;"> | |
| <strong>identity.users</strong><br>UUID PK, auth0_sub, email, phone | |
| </div> | |
| </div> | |
| <div class="detail" style="margin-top: 8px;">Cross-schema JOINs for reads ✓</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- AUTH0 CONFIGURATION --> | |
| <h2>Auth0 Configuration</h2> | |
| <h3>Auth0 APIs (Audiences)</h3> | |
| <div class="config-grid"> | |
| <div class="config-item"> | |
| <div class="key">Bowler API <span class="badge exists">exists</span></div> | |
| <div class="val"><code>https://api.luckystrike.cloud</code></div> | |
| <div class="note">Used by bowler-connect and bowler-api. No changes needed.</div> | |
| </div> | |
| <div class="config-item"> | |
| <div class="key">Identity API <span class="badge new">new</span></div> | |
| <div class="val"><code>https://identity.luckystrike.cloud</code></div> | |
| <div class="note">New API for identity-api. Create in Auth0 Dashboard → APIs.</div> | |
| </div> | |
| </div> | |
| <h3>Auth0 Applications</h3> | |
| <div class="config-grid"> | |
| <div class="config-item"> | |
| <div class="key">Bowler Connect (Regular Web App) <span class="badge exists">exists</span></div> | |
| <div class="val">Client ID: <code>P8bs1c...RVNob</code></div> | |
| <div class="note">Callback: https://connect.luckystrike.cloud/auth/callback — No changes needed.</div> | |
| </div> | |
| <div class="config-item"> | |
| <div class="key">Identity Sync (M2M) <span class="badge m2m-badge">new · m2m</span></div> | |
| <div class="val">Machine-to-Machine application</div> | |
| <div class="note">Used by Post-Login Action to call identity-api. Authorized for <code>identity.luckystrike.cloud</code> audience.</div> | |
| </div> | |
| </div> | |
| <h3>Tenant Settings</h3> | |
| <div class="config-grid"> | |
| <div class="config-item"> | |
| <div class="key">Session Idle Timeout</div> | |
| <div class="val">30 days (max)</div> | |
| <div class="note">Settings → Sessions → Idle Session Lifetime</div> | |
| </div> | |
| <div class="config-item"> | |
| <div class="key">Session Absolute Timeout</div> | |
| <div class="val">30 days (max)</div> | |
| <div class="note">Settings → Sessions → Absolute Session Lifetime</div> | |
| </div> | |
| <div class="config-item"> | |
| <div class="key">Refresh Token Rotation</div> | |
| <div class="val">Enabled</div> | |
| <div class="note">Applications → Bowler Connect → Settings → Refresh Token Rotation</div> | |
| </div> | |
| <div class="config-item"> | |
| <div class="key">Persistent Sessions</div> | |
| <div class="val">Enabled</div> | |
| <div class="note">Settings → Sessions → Persistent Session</div> | |
| </div> | |
| </div> | |
| <!-- TOKEN FLOWS --> | |
| <h2>Token Flows</h2> | |
| <div class="section"> | |
| <table class="token-table"> | |
| <thead> | |
| <tr> | |
| <th>Flow</th> | |
| <th>Token Type</th> | |
| <th>Audience</th> | |
| <th>Used By</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| <tr> | |
| <td>User login → bowler-connect</td> | |
| <td>Access Token (JWT)</td> | |
| <td><code>api.luckystrike.cloud</code></td> | |
| <td>bowler-api validates</td> | |
| </tr> | |
| <tr> | |
| <td>Post-Login Action → identity-api</td> | |
| <td>M2M Token (client credentials)</td> | |
| <td><code>identity.luckystrike.cloud</code></td> | |
| <td>identity-api validates</td> | |
| </tr> | |
| <tr> | |
| <td>Custom claim on all user tokens</td> | |
| <td>ID + Access Token claim</td> | |
| <td>—</td> | |
| <td><code>https://luckystrike.com/identity_id</code> = canonical UUID</td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| <!-- LOGIN FLOW --> | |
| <h2>Login Flow (Step by Step)</h2> | |
| <div class="section"> | |
| <ol class="step-list"> | |
| <li> | |
| <strong>User opens bowler-connect</strong> (or any LSE app) and clicks "Log In" | |
| </li> | |
| <li> | |
| <strong>App redirects to Auth0</strong> Universal Login page. If the user has an existing Auth0 session cookie (e.g. from another LSE app), <strong>Auth0 skips the login prompt</strong> and issues tokens immediately. | |
| </li> | |
| <li> | |
| <strong>Auth0 runs Post-Login Action</strong> which:<br> | |
| a. Gets an M2M token via client credentials grant (<code>identity.luckystrike.cloud</code> audience)<br> | |
| b. Calls <code>POST /resolve</code> on identity-api with the user's <code>sub</code>, email, phone<br> | |
| c. Identity-api upserts <code>identity.users</code> and returns canonical UUID<br> | |
| d. Action sets <code>identity_id</code> custom claim on ID + access tokens | |
| </li> | |
| <li> | |
| <strong>Auth0 redirects back to app</strong> with tokens containing the canonical <code>identity_id</code> | |
| </li> | |
| <li> | |
| <strong>App calls bowler-api</strong> (or its own backend) with the access token. Backend reads <code>identity_id</code> from the JWT — no extra call to identity-api needed for normal operations. | |
| </li> | |
| </ol> | |
| </div> | |
| <!-- WHAT TO CREATE --> | |
| <h2>Action Items</h2> | |
| <div class="section"> | |
| <ol class="step-list"> | |
| <li> | |
| <strong>Create Auth0 API:</strong> <code>https://identity.luckystrike.cloud</code><br> | |
| Dashboard → APIs → Create API | |
| </li> | |
| <li> | |
| <strong>Create M2M Application:</strong> "Identity Sync"<br> | |
| Dashboard → Applications → Create → Machine to Machine<br> | |
| Authorize for the identity API audience | |
| </li> | |
| <li> | |
| <strong>Set session lifetimes:</strong> 30d idle / 30d absolute<br> | |
| Dashboard → Settings → Sessions | |
| </li> | |
| <li> | |
| <strong>Enable refresh token rotation</strong> on Bowler Connect app<br> | |
| Dashboard → Applications → Bowler Connect → Refresh Token Rotation | |
| </li> | |
| <li> | |
| <strong>Create Post-Login Action:</strong> "Sync Identity"<br> | |
| Dashboard → Actions → Flows → Login → Add Action<br> | |
| Secrets: IDENTITY_API_URL, M2M_CLIENT_ID, M2M_CLIENT_SECRET, M2M_AUDIENCE | |
| </li> | |
| <li> | |
| <strong>Update identity-api auth middleware</strong> to accept M2M tokens<br> | |
| (accept both <code>api.luckystrike.cloud</code> and <code>identity.luckystrike.cloud</code> audiences) | |
| </li> | |
| <li> | |
| <strong>Deploy identity-api</strong> and verify Post-Login Action fires on next login | |
| </li> | |
| </ol> | |
| </div> | |
| <p style="margin-top: 32px; color: var(--text-dim); font-size: 0.8em; text-align: center;"> | |
| LSE Identity Architecture — Option A (Separate Audience) • Connected Play • Jan 2026 | |
| </p> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment