Auth Verify (Magic Link Token Verification)
System Intent
- What this is: end-to-end magic-link verification across three layers — the landing-web page verifies the token with the server, hands off the code via deep link to the mobile app, and the mobile app auto-submits the code to complete sign-in.
Mermaid Diagram
flowchart TD
Email["User clicks magic link in email"] --> Web["landing-web /auth/verify\n(+page.svelte)"]
Web -->|POST /auth/verify\nemail + token + session| Lambda["auth/verify Lambda\n(app.py)"]
Lambda -->|Cognito challenge response| Cognito["AWS Cognito"]
Cognito -->|AuthenticationResult| Lambda
Lambda -->|create_handoff_code| DB["PostgreSQL"]
Lambda -->|handoffCode + expiresIn| Web
Web -->|window.location = encache://auth/verify?code=...| App["Mobile app\n(verify.tsx)"]
Web -->|fallback: display code manually| Manual["User copies code"]
App -->|auto-submit via deepLinkCode param| Complete["completePasswordlessSignInWithCode"]
Manual -->|user types code| Complete
Flows
Flow: deepLinkVerify
- Core files:
apps/landing-web/src/routes/auth/verify/+page.svelte, main/app/app/(auth)/verify.tsx, main/server/api/auth/verify/app.py
Types
VerifyRequest {
email: string (required)
token: string (required — from magic link query param)
session: string (required — from magic link query param)
}
HandoffCodeResponse {
handoffCode: string (short-lived opaque code)
expiresIn: number (seconds until code expires)
}
StandardError {
status: number (HTTP status)
code: string (stable machine-readable code)
message: string (human-readable summary)
}
Paths
| path | input | output | path-type | notes |
deepLinkVerify.success | VerifyRequest | deep link opens app with code param | happy path | web page redirects to encache://auth/verify?code=<handoffCode>; app populates TextInput with code via setCode(), then code-watching effect auto-submits; user sees the code in the input before sign-in completes |
deepLinkVerify.fallback | VerifyRequest | manual code display on web page | fallback | used when app is not installed or device does not support encache:// scheme |
deepLinkVerify.missing-params | malformed URL | 400 AUTH_TOKEN_REQUIRED / AUTH_SESSION_REQUIRED | error | web page shows generic error |
deepLinkVerify.invalid-token | wrong or expired token | 400 AUTH_TOKEN_INVALID | error | Cognito rejects the challenge response |
deepLinkVerify.invalid-email | bad email format | 400 AUTH_EMAIL_INVALID | error | |
Pseudocode
# landing-web (+page.svelte) — runs onMount
params = parseQueryString(window.location.search) # email, token, session
response = POST /auth/verify { email, token, session }
if not response.ok or not response.data.handoffCode:
status = "error"
return
handoffCode = response.data.handoffCode
window.history.replaceState(...) # strip sensitive params from URL
deepLink = "encache://auth/verify?code=" + encodeURIComponent(handoffCode)
window.location.href = deepLink # attempt to open app; silently ignored on desktop
status = "code" # show manual fallback UI regardless
# mobile app (verify.tsx) — useEffect on deepLinkCode (autofill effect)
deepLinkCode = useLocalSearchParams().code
if deepLinkCode matches /^\d{6}$/:
setCode(deepLinkCode) # populate TextInput so user sees the code
# mobile app (verify.tsx) — useEffect on code (submission effect)
trimmed = code.trim()
if trimmed.length < 6: lastSubmittedRef = ""; return
if not /^\d{6}$/.test(trimmed): return
if submitting or success: return
if lastSubmittedRef == trimmed: return # guard against double-submission
lastSubmittedRef = trimmed
handleVerify(trimmed)
# mobile app — useEffect on deepLinkCode (focus effect)
if not deepLinkCode:
focus(inputRef) # focus manual input when no deep-link code present
# mobile app — handleVerify
if not /^\d{6}$/.test(trimmedCode):
setError(...)
return
await completePasswordlessSignInWithCode(trimmedCode)
Logs
| Source | Location |
| auth/verify Lambda | CloudWatch: /aws/lambda/encache-auth-verify |
Deployment
- Mechanism:
SAM (Lambda), SvelteKit (landing-web), Expo (mobile app) - Notes:
PUBLIC_API_URL must be set in the landing-web environment for the POST to reach the correct API Gateway endpoint.
Error Cases
| Condition | Response |
| Invalid email format | 400 AUTH_EMAIL_INVALID |
| Missing token | 400 AUTH_TOKEN_REQUIRED |
| Missing session | 400 AUTH_SESSION_REQUIRED |
| Wrong or expired token | 400 AUTH_TOKEN_INVALID |
| Deep link not supported (desktop/app not installed) | Manual code display fallback shown on web page |
deepLinkCode param not a 6-digit number | Mobile ignores it; manual input field is focused instead |