Zooly Auth is built on a centralized authentication model where all apps share a single identity provider.
flowchart TB
subgraph client [Client Browser]
LoginPage[Login Page]
AuthContext[Auth Context]
end
subgraph guestClient [Guest Checkout Client]
GuestCheckout[Guest Checkout]
end
subgraph authApp [auth.zooly.ai]
LoginAPI["/api/auth/login"]
MeAPI["/api/me"]
GuestGetOrCreateAPI["/api/users/get-or-create"]
LogoutAPI["/api/auth/logout"]
CallbackAPI["/api/auth/callback"]
end
subgraph aws [AWS]
Cognito[Cognito User Pool]
DynamoDB[DynamoDB identities]
end
subgraph packages [Packages]
AuthClient["@zooly/auth-client"]
AuthSrv["@zooly/auth-srv"]
AuthDB["@zooly/auth-db"]
end
LoginPage --> LoginAPI
GuestCheckout --> GuestGetOrCreateAPI
LoginAPI --> Cognito
LoginAPI --> AuthDB
GuestGetOrCreateAPI --> AuthDB
AuthDB --> DynamoDB
MeAPI --> AuthSrv
AuthSrv --> Cognito
AuthSrv --> AuthDB
zooly.ai - Productiondev.zooly.ai - Developmentauth.zooly.ai - Single entrypoint for all authenticationApps can be organized as:
zooly.ai/<app> or app1.zooly.ai, app2.zooly.aidev.zooly.ai/<app> or dev-app1.zooly.ai etc.All apps under *.zooly.ai share the same authentication session.
The authentication system is organized into three main packages:
@zooly/auth-dbDynamoDB access layer for identity storage:
getIdentity(userId) - Get identity by user IDupsertIdentity(identity) - Create or update identityupdateRoles(userId, roles) - Update user rolesfindIdentityByEmail(email) - Find identity by emailgetOrCreateByEmail(email, name?) - Get or create guest identitylinkCognitoIdentity(userId, cognitoSub) - Link Cognito user to existing identity@zooly/auth-srvServer-side authentication functions:
signIn(email, password) - Authenticate usersignUp(email, password, displayName?) - Register new userconfirmSignUp(email, code) - Verify email with coderesendConfirmationCode(email) - Resend verification codeforgotPassword(email) - Initiate password resetconfirmForgotPassword(email, code, newPassword) - Complete password resetaws-jwt-verify (ID tokens)refreshTokens) using REFRESH_TOKEN_AUTH flowverifyOrRefreshToken) that refreshes expired tokens automatically@zooly/auth-clientReact components and utilities for authentication UI:
LoginPage - Main authentication page componentLoginForm, PasswordForm, SignUpForm, ConfirmSignUpForm, ForgotPasswordForm, ResetPasswordFormAuthContextProvider and useAuth hook for user state managementvalidateReturnTo utility for safe redirect handlingIdentity storage is split between Cognito and DynamoDB:
Cognito
└─ sub (identity anchor)
└─ email (source of truth)
Identity Service
└─ DynamoDB (sub → profile data)
Apps
└─ call /api/me
├─ verify ID token JWT
├─ extract email from ID token (Cognito source of truth)
├─ read profile from DynamoDB using sub
└─ return unified profile (email from Cognito + profile from DynamoDB)
sub: Stable user ID (identity anchor)email: Source of truth for user emailTable: zooly-auth-identities
Primary Key: user_id (string) - Cognito sub for linked users, generated for email-only users
Item Structure:
{
"user_id": "sub or generated",
"guest_email": "guest@example.com",
"display_name": "Elia",
"avatar_url": "https://...",
"roles": ["admin"],
"cognito_sub": "sub",
"created_at": 1700000000,
"updated_at": 1700500000
}
Important: Email is NOT stored in DynamoDB. For Cognito-linked users, email comes from Cognito ID token (source of truth). Only guest_email is stored for guest checkout users before linking.
.zooly.ai (shared across all subdomains)Secure alwaysHttpOnly where possibleSameSite=Lax by defaultauth-token): Stores Cognito ID token in HttpOnly cookieauth-refresh-token): Stores Cognito refresh token in HttpOnly cookieBackends derive identity from:
JWT Claims (from Cognito ID token):
sub - Stable user ID (used as DynamoDB key)email - From Cognito ID token (source of truth, not stored in DynamoDB)DynamoDB Profile (from identities table):
display_nameavatar_urlroles (string[] - stored in DynamoDB)guest_email (only for guest checkout users before linking)Roles are stored in DynamoDB and retrieved when looking up the identity:
roles: string[] in the identities tableroles: [] (empty array)/api/me response from DynamoDB, not from Cognito tokenExample roles:
["admin"] => admin role["editor"] => editor role[] => default userBackends validate JWT signatures using JWKS (AWS verifier library):
iss)exp)sub)No shared secret is used; keys are fetched and cached via JWKS.
When an ID token expires, the system automatically refreshes it:
REFRESH_TOKEN_AUTH flow with refresh tokenauth-token cookie with new ID token in responseKey Points:
On This Page
System ArchitectureDomain TopologyApex DomainCentral Auth AppApp DomainsPackage Structure[object Object][object Object][object Object]Identity Storage ArchitectureCognito StorageDynamoDB StorageSession ManagementCookie ConfigurationToken StrategyAuthorization ModelIdentity DerivationRolesBackend JWT VerificationToken Refresh Flow