Skip to content
← All patterns

security

Verification Service

Is this code still valid?

Send verification and password-reset codes that expire after 15 minutes.

Verification codes, remembered devices, rate-limit counters and a security audit log share one table. Each kind of item expires on its own schedule: codes after 15 minutes, device tokens after 30 days and audit entries after 90 days. An index by user finds a user's earlier codes, so the app can invalidate them before sending a new one.

The model

VerificationCode

A time-limited code for email/phone verification or password reset

pk
CODE#<code>
sk
CODE#<code>

Attributes: PK (S), SK (S), GSI1PK (S), GSI1SK (S), code (S), userId (S), codeType (S), deliveryMethod (S), deliveryTarget (S), isUsed (BOOL), createdAt (S), ttl (N)

DeviceToken

A registered device for push notifications or remember-me functionality

pk
DEVICE#<deviceId>
sk
TOKEN#<tokenId>

Attributes: PK (S), SK (S), GSI1PK (S), GSI1SK (S), deviceId (S), tokenId (S), userId (S), deviceType (S), deviceName (S), pushToken (S), lastActiveAt (S), ipAddress (S), userAgent (S), createdAt (S), ttl (N)

AuditLog

Security event log for compliance and forensics

pk
USER#<userId>
sk
AUDIT#<timestamp>

Attributes: PK (S), SK (S), userId (S), eventType (S), eventDetails (M), ipAddress (S), userAgent (S), deviceId (S), success (BOOL), failureReason (S), timestamp (S), ttl (N)

RateLimitCounter

Track rate limits for code requests per user

pk
RATELIMIT#<userId>#<action>
sk
WINDOW#<windowStart>

Attributes: PK (S), SK (S), userId (S), action (S), windowStart (S), count (N), ttl (N)

Access patterns

  • GetItemValidate verification code

    Check if a code is valid (exists, not expired, not used)

  • Query · GSI1Invalidate previous codes

    Query and delete user's existing codes of the same type before creating new one

  • TransactWriteItemsRegister device token

    Create or update a device token for push notifications

  • Query · GSI1Revoke all user's tokens

    Delete all device tokens for a user (e.g., on password change)

  • QueryQuery user audit history

    Get security events for a user within a time range

  • QueryCheck rate limit

    Check if user has exceeded rate limit for an action

Design notes

TTL handles all expirationwhy

No background jobs needed to clean up expired codes or tokens. DynamoDB TTL automatically deletes items after expiration. AWS documents deletion as typically within a few days of the ttl passing, with no guarantee attached, so always check the ttl attribute when validating.

Code lookup vs user lookupwhy

VerificationCode has dual access: by code (PK) for validation, by user (GSI1) for invalidation. Same item, different access paths. This avoids needing to know the user ID when validating.

Audit log TTL for compliancetrade-off

90-day retention matches many compliance requirements. Adjust based on your needs. For permanent audit logs, omit the TTL attribute or set it far in the future.

Rate limit with atomic counterwhy

RateLimitCounter uses ADD for atomic increment. If count exceeds threshold, reject the request. The TTL ensures old windows are cleaned up automatically.

Alternative: Redis for ephemeral dataalternative

For very high-volume verification codes (millions/day), consider Redis with TTL. DynamoDB is cost-effective for moderate volume but Redis may be more economical at extreme scale.

Registering a device is a transaction the page cannot runtrade-off

Register device token is a TransactWriteItems: it puts the device token and appends the device_added entry to the user's audit log, so a device is never registered without a record of it. The engine in this page does not support TransactWriteItems, so it is not offered as a run.

Taught in the course

verification-serviceDynamoDB workbench
Ready to run
Explore an access patternSelect to load & run

Check if a code is valid (exists, not expired, not used)

Request
Execute against the local Dynoxide engine
ReturnedFiltered outChanged
PK(pk)
SK(sk)
GSI1PK
GSI1SK
code
codeType
createdAt
deliveryMethod
deliveryTarget
isUsed
ttl
userId
deviceId
deviceName
deviceType
ipAddress
lastActiveAt
pushToken
tokenId
userAgent
eventDetails
eventType
success
timestamp
failureReason
action
count
windowStart
CODE#847291
CODE#847291S
USER#u-001S
CODE#email_verify#2026-10-13T23:50:00ZS
847291S
email_verifyS
2026-10-13T23:50:00ZS
emailS
user@example.comS
falseBOOL
1791936300N
u-001S
—
—
—
—
—
—
—
—
—
—
—
—
—
—
—
—
CODE#392847
CODE#392847S
USER#u-002S
CODE#password_reset#2026-10-14T00:00:00ZS
392847S
password_resetS
2026-10-14T00:00:00ZS
emailS
jane@example.comS
falseBOOL
1791936900N
u-002S
—
—
—
—
—
—
—
—
—
—
—
—
—
—
—
—
DEVICE#d-iphone-001
TOKEN#tok-001S
USER#u-001S
DEVICE#d-iphone-001S
—
—
2026-09-30T01:00:00ZS
—
—
—
1793322000N
u-001S
d-iphone-001S
iPhone 15 ProS
iosS
192.168.1.100S
2026-10-13T22:00:00ZS
APNs:abc123xyz789...S
tok-001S
MyApp/2.0 iOS/17.0S
—
—
—
—
—
—
—
—
USER#u-001
AUDIT#2026-10-13T21:15:00ZS
—
—
—
—
—
—
—
—
1799702100N
u-001S
d-iphone-001S
—
—
192.168.1.100S
—
—
—
—
{1}M
loginS
trueBOOL
2026-10-13T21:15:00ZS
—
—
—
—
USER#u-001
AUDIT#2026-10-13T23:30:00ZS
—
—
—
—
—
—
—
—
1799710200N
u-001S
—
—
—
192.168.1.100S
—
—
—
Mozilla/5.0...S
{2}M
code_sentS
trueBOOL
2026-10-13T23:30:00ZS
—
—
—
—
USER#u-002
AUDIT#2026-10-13T23:45:00ZS
—
—
—
—
—
—
—
—
1799711100N
u-002S
—
—
—
10.0.0.50S
—
—
—
—
{1}M
loginS
falseBOOL
2026-10-13T23:45:00ZS
invalid_passwordS
—
—
—
RATELIMIT#u-002#code_request
WINDOW#2026-10-13T23:00:00ZS
—
—
—
—
—
—
—
—
1791939600N
u-002S
—
—
—
—
—
—
—
—
—
—
—
—
—
code_requestS
3N
2026-10-13T23:00:00ZS
Awaiting request
Your next query starts here.

Choose an access pattern above, or build your own request. See what comes back and what it costs.

Write transactions, streams, tags and TTL are among the operations this browser build leaves out. dynoxide's native build has them.