Back to blog
FILE 0x50·DYNAMO AS INBOX, WITH A PASSKEY

Dynamo as inbox, with a passkey

April 20, 2026 · aws, webauthn, passkeys, email

I built a single-page mail reader on top of the DynamoDB table my mail pipeline writes to. Three-pane layout (inbox list / messages / body), search, attachment links — all served from one Lambda. The interesting part is the auth: a bootstrap token cookie, and after that, passkeys.

The shape

Storing auth in the mail table

Same table, separate partition:

pk = AUTH
sk = CRED#<base64url(credential_id)>   # registered passkeys
sk = CHAL#<id>                          # pending challenges, 5min TTL
sk = SESS#<id>                          # sessions, 30 day TTL

DynamoDB TTL on the ttl attribute auto-expires challenges and old sessions. No cleanup job to run. The mail table is small enough that adding a few hundred AUTH rows is a rounding error.

The bootstrap problem

WebAuthn requires a registered credential to log in. But you need some way to log in the first time to register that credential.

Solution: a one-shot bootstrap token in the URL.

https://inbox.example.com/?token=<random>

Visiting that URL once:

  1. Sets an HttpOnly cookie carrying the bootstrap token
  2. Server recognizes the token, marks the session "authed but no credential"
  3. UI shows the Register Passkey panel
  4. User enters a label ("iPhone"), hits Register, Touch/Face ID captures the credential
  5. Server stores the public key and sign count, drops the bootstrap pseudo-session, opens a real session

After that, plain https://inbox.example.com works without the token. The bootstrap token is rotatable via a Lambda env var if it ever leaks.

The library shopping

py_webauthn (webauthn on PyPI) handles all the heavy lifting — challenge generation, attestation verification, sign count tracking. Bundled as a Lambda layer along with cryptography, cbor2, and pyOpenSSL.

RP ID is the bare hostname; origin is the full https:// URL. Get either wrong and the browser silently refuses to register a credential, which is fun to debug.

What stays in the cheap-and-good zone

What I'd do differently

The bootstrap-token-in-URL flow works but it's the only piece I'm not entirely happy with. If I were building this today I'd probably do a one-time magic link from a verified sender email instead — same "prove you can read inbox.example.com mail" property, no long-lived URL token to worry about. The current setup is fine for one user; it would need a rethink before being multi-user.