This file provides guidance to Claude Code, Codex, GitHub Copilot, and other coding agents working in this repository.
auth-web is suite-wide login for the SweetRPG platform - the sole implementer of the Auth0
Authorization Code flow (/auth/login, /auth/callback, /auth/logout) and the only frontend
holding Auth0 client credentials. It establishes one shared session (cookie + Redis-backed
store) that every other frontend reads directly, so logging in once through this app is
recognized suite-wide rather than each frontend running its own independent login. See
sweetrpg/platform's openspec/changes/add-user-api-authn-authz for the original design and why
this exists as its own repo (deliberately kept small - login is infrastructure every other
frontend hard-depends on, so it gets the smallest possible blast radius) rather than being
bundled into main-web, users-web, or admin-web. Its authz backend moved from users-api to
a dedicated auth-api - see openspec/changes/split-authz-into-auth-api for why.
- auth-api: called once at login,
POST /authz/check, to establish the session's verified roles server-side - not a local unverified token decode. - Redis: this app's own dedicated instance,
cache.sweetrpg-auth.svc.cluster.local, deployed alongside it insweetrpg-auth. It doubles as the suite-wide session store - seesweetrpg/platform'sdocs/frontend-conventions.md("Per-namespace Redis instances") for how other frontends read it cross-namespace. - Every other frontend reads the session this app writes (same cookie name, same Redis
instance) but never writes to it - they only ever redirect an unauthenticated visitor to this
app's
/auth/login?return_to=<path>.
One Auth0 application backs both dev and local environments today (same tenant, same
client id/secret - see kubernetes/overlays/{dev,local}/secrets.yaml's shared Akeyless path
/sweetrpg/dev/auth/auth0). None of this is scripted or captured in Terraform/Akeyless as
code - it's manual dashboard configuration, so if the application is ever deleted, the tenant
is rebuilt, or a genuinely separate environment (e.g. production) needs its own application,
here's what has to exist for login to work:
- Application type: Regular Web Application (confidential client -
auth-webholdsAUTH0_CLIENT_SECRETand exchanges the authorization code server-side; never a SPA/public client). - Grant type: Authorization Code only.
- Allowed Callback URLs: one entry per environment sharing this application - both
https://dev.sweetrpg.com/auth/callbackandhttps://sweetrpg.local/auth/callbackneed to be present simultaneously fordevandlocalto both work, since they share one application. A production environment would add its own callback URL here, or - more likely, see below - get its own separate application instead. - Allowed Logout URLs: the same set of origins, but the post-logout redirect target
(
returnTo), not the callback path -https://dev.sweetrpg.com/auth/logout-completeandhttps://sweetrpg.local/auth/logout-complete(seeAuth0Config.logoutURL(returnTo:), which derives this fromAUTH0_CALLBACK_URLby swapping/auth/callbackfor/auth/logout-complete). Auth0 only accepts areturnTothat exactly matches one of these registered URLs, so the visitor's actual post-logout destination can't be registered directly - it travels as/auth/logout-complete's ownreturn_toquery parameter instead, which this app validates and redirects to itself (seeopenspec/changes/auth-web-logout-preserve-return-pathinsweetrpg/platform). This is a separate list from Allowed Callback URLs in the Auth0 dashboard - missing an entry here doesn't break login, only logout, and manifests as Auth0's own generic "Oops, something went wrong" error page after clicking "Log out," not an error this app's own logs will show anything useful for (confirmed the hard way: the redirect to Auth0 succeeds, Auth0 is the one rejecting thereturnTovalue). - Connections enabled: at minimum the connection(s) actually used to sign in - confirmed in
use: a social connection (GitHub) and email/password (
Username-Password-Authentication). Enable whichever connections the product actually wants to offer; nothing in this app hardcodes which ones are available, that's entirely an Auth0-dashboard setting. - An API registered for
AUTH0_AUDIENCE(Applications → APIs → Create API, not the application settings page):auth-apiandauth-webboth validate/request tokens against this API's Identifier. Without a real API registered,AUTH0_AUDIENCEhas nothing valid to point at, andauth-api'sverifyIntendedAudiencecheck fails for every token regardless of how correctly everything else is configured - confirmed the hard way: an empty string synced intoAUTH0_AUDIENCE(a stale/never-populated Akeyless value, not a missing environment variable -Environment.gettreats an empty string as present) passedauth-api'sAuth0Config.fromEnvironment()guard letwithout error, then failed every login at token-verification time with no indication why. Confirming the deployed value is actually non-empty (kubectl exec ... -- env | grep AUTH0_AUDIENCE) is a faster diagnostic than assuming the dashboard side is wrong. - Akeyless:
AUTH0_DOMAIN,AUTH0_CLIENT_ID,AUTH0_CLIENT_SECRET,AUTH0_AUDIENCEat/sweetrpg/dev/auth/auth0, read by both this app's andauth-api'sExternalSecrets (seeauth-api'sAGENTS.md, "the one shared Auth0 application"). A new environment needing its own application (production, most likely, rather than sharing thedevtenant) should follow the same/sweetrpg/<env>/auth/auth0path convention rather than inventing a new one.
None of the actual credential values belong in this file or any committed doc - Akeyless is the source of truth; this section documents the shape of what must exist there and in the Auth0 dashboard, not the values themselves.
Unlike catalog-web/admin-web (whose Ingress strips their own /catalog//admin prefix
before the request reaches the app), this app's Ingress does not strip /auth - its routes
are already registered as /auth/login, /auth/callback, /auth/logout, matching the
browser-facing path one-to-one. See kubernetes/overlays/dev/ingress.yaml.
Implements sweetrpg/platform's openspec change full-localization-web-apps.
- Library:
miroslavkovac/Lingo(framework-agnostic); loaded inSources/App/I18n.swift. - String tables live in
Resources/Localizations/<code>.jsonas flat dotted keys (maintenance.since_prefixetc.). English (en.json) is the default and fallback. - Locale resolution order per request:
localecookie (when it maps to a loaded table), then the first Accept-Language tag's base subtag, then English. The resolved flat table is exposed viaRequest.l10n; render contexts embed it and reference strings by key. - Template convention: no hardcoded user-facing text - interpolate from the
l10ntable instead. For interpolated sentences use prefix/suffix key pairs so fragments stay translatable. Do not localize brand names (SweetRPG, GitHub, Auth0, Pilgrimage Software), footer build lines, emails, or raw enum/status identifiers. - Adding a locale: drop a new
<code>.jsonalongsideen.jsonwith the same keys; missing keys fall back to English automatically. - CI gate: the
locale-lintjob in.github/workflows/{ci,pr}.yamlrunsscripts/check-template-strings.sh, which fails on hardcoded template text.
Conventional Commits: <type>(<scope>): <description>.
Git-flow (see docs/git-flow.md in sweetrpg/platform): develop is the integration branch,
master reflects the latest release. Feature/fix branches off develop, PR back into develop.
swift build
swift testswift run serves on :8080. Without AUTH0_DOMAIN/AUTH0_CLIENT_ID set, /auth/login
returns a 503 rather than starting a broken flow. Without REDIS_HOST set, falls back to
in-memory sessions - fine for local development, not for anything another frontend needs to read
from.