Guest checkout and email-only user support
Zooly Auth supports a Shopify-style purchase flow where users can buy with just an email (no login). These guest users can later register and link their accounts to preserve purchase history.
The identities table supports two identity states:
Cognito-linked users (authenticated)
user_id is Cognito subEmail-only users (guest checkout)
user_id is generated by our systemguest_email is storedsub yetEmail-only user (before linking):
{
"user_id": "user-guest-123",
"guest_email": "guest@example.com",
"display_name": "Optional Name",
"cognito_sub": null,
"roles": [],
"created_at": 1700000000,
"updated_at": 1700000000
}
Linked user (after registration):
{
"user_id": "user-guest-123",
"guest_email": "guest@example.com",
"display_name": "Optional Name",
"cognito_sub": "cognito-sub-xyz",
"roles": [],
"created_at": 1700000000,
"updated_at": 1700600000
}
Important: Email is NOT stored in DynamoDB. For Cognito-linked users, email comes from Cognito ID token (source of truth). The guest_email field is retained for historical reference.
Use getOrCreateByEmail to get or create a guest identity:
import { getOrCreateByEmail } from '@zooly/auth-db';
// In your checkout endpoint
export async function POST(request: Request) {
const { email, name } = await request.json();
// Get existing identity or create new guest identity
const identity = await getOrCreateByEmail(email, name);
// Use identity.user_id for purchase association
const purchase = await createPurchase({
userId: identity.user_id,
// ... other purchase data
});
return Response.json({
userId: identity.user_id,
purchaseId: purchase.id
});
}
guest_email), returns their user_idguest_email and optional display_nameuser_id for purchase association1. User enters email at checkout
2. Backend calls getOrCreateByEmail(email, name?)
3. System returns user_id (existing or new)
4. Purchase is associated with user_id
5. User completes purchase without registration
When a user registers with an email that was previously used for a guest checkout:
The system automatically links Cognito identities to existing guest identities:
guest@example.comguest_emailuser_idcognito_sub on existing identityguest_email is retained for historical referenceThe linking happens automatically in the signup/login flow:
// In @zooly/auth-srv signup/login functions
import { findIdentityByEmail, linkCognitoIdentity } from '@zooly/auth-db';
async function signUp(email: string, password: string) {
// Create Cognito user
const cognitoUser = await cognito.signUp(email, password);
// Check for existing guest identity
const existingIdentity = await findIdentityByEmail(email);
if (existingIdentity && !existingIdentity.cognito_sub) {
// Link Cognito identity to existing guest identity
await linkCognitoIdentity(existingIdentity.user_id, cognitoUser.sub);
// Return existing user_id (stable across linking)
return existingIdentity.user_id;
}
// Create new identity for new user
const identity = await upsertIdentity({
user_id: cognitoUser.sub,
cognito_sub: cognitoUser.sub,
// ... other fields
});
return identity.user_id;
}
This design enables:
user_idEndpoint for guest checkout:
// In auth app
export async function POST(request: Request) {
const { email, name } = await request.json();
const identity = await getOrCreateByEmail(email, name);
return Response.json({
userId: identity.user_id,
email: identity.guest_email,
name: identity.display_name,
});
}
Usage:
// In your checkout app
const response = await fetch('https://auth.zooly.ai/api/users/get-or-create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: 'guest@example.com',
name: 'Guest User' // optional
}),
});
const { userId } = await response.json();
// Use userId for purchase association
Always associate purchases with user_id, regardless of login state:
// Purchase table
{
purchase_id: "purchase-123",
user_id: "user-guest-123", // Same user_id before and after linking
// ... other purchase data
}
// After user registers and links:
// - user_id remains the same
// - All purchases remain associated
// - User sees full purchase history
After linking, the user profile combines:
user_idIf you're adding guest user support to an existing system:
cognito_sub, no changes neededuser_id and guest_emailuser_id remains stable across linkinguser_id - Never use email as foreign keyguest_email - Keep for historical reference after linkinguser_id consistently across all purchasesOn This Page
OverviewGuest User ModelTwo Identity StatesIdentity StructureCreating Guest UsersgetOrCreateByEmail FunctionBehaviorGuest Checkout FlowLinking on RegistrationAutomatic LinkingImplementationBenefitsAPI EndpointGET /api/users/get-or-createPurchase AssociationUser Profile After LinkingMigration ConsiderationsBest Practices