Skip to content

Data Storage

Serverless Inbox uses a single S3 bucket and six DynamoDB tables. All tables use on-demand billing and per-account key prefixes to isolate tenant data.

Five DynamoDB tables and the S3 bucket live in a dedicated MailboxDataStack that is never deleted when the mailbox stack is deleted. This prevents CloudFormation conflicts during deploy → delete → redeploy cycles. The data stack is automatically created and managed by a custom resource (MailboxDataDeployer) unless you provide an existing data stack reference via the dataStack prop in MailboxProps.


Retained in data stack. The system registry. Stores all configuration that the platform needs to operate: tenant records, domain bindings, email identity definitions, authentication policies, OAuth client metadata, and sender suppression list entries.

Every Lambda that needs to resolve “who is this tenant” or “what policies apply here” reads from the Config table. It is read far more often than it is written.

Written by: bootstrap, admin-api, sqs-ses-event-processor
Read by: All Lambdas


Retained in data stack. The primary mailstore. Holds every user-owned object in the system:

  • Mailboxes — Inbox, Sent, Drafts, Trash, and any custom folders
  • Emails — Message metadata, flags, and mailbox placement
  • Threads — Conversation groupings
  • Email Identities — Sender from-address definitions per user

The Item table is the busiest table in the system and is designed around the access patterns of the JMAP protocol — most queries target a single account and a single object type.

Note: WebSocket connections are tracked separately in the WebSocket connections table (see below).

Written by: email-processor, email-sender, sqs-ses-event-processor, jmap-api, admin-api
Read by: jmap-api, admin-api, websocket-broadcast, tantivy-indexer


Retained in data stack. Tracks the state version for every account and object type, enabling JMAP /changes queries and incremental search indexing.

Each entry records a create, update, or delete event for a single object, keyed by {accountId}#{objectType}. A separate “state counter” item tracks the current maximum sequence number. Entries carry a TTL and are automatically removed after they expire.

Written by: email-processor, email-sender, sqs-ses-event-processor, jmap-api, admin-api
Read by: jmap-api (for /changes endpoints), tantivy-indexer (to detect what changed)


Retained in data stack. A chronological record of administrative and user actions — who did what, to which resource, and when. Written asynchronously via the audit events queue so that audit persistence never blocks request handling.

Entries can be queried by entity (e.g. “all actions on tenant X”) or by actor (e.g. “all actions by admin user Y”) using the table’s secondary indexes.

Written by: audit-writer (consuming from the audit events queue)
Read by: admin-api (audit log queries)


Retained in data stack. Records delivery feedback received from the email provider for every outbound message: hard bounces, soft bounces, spam complaints, rejections, and delivery confirmations. Used to drive automatic suppression decisions and to give administrators visibility into deliverability per sender and domain.

See Bounce & Complaint Handling for how this data drives suppression.

Written by: sqs-ses-event-processor
Read by: admin-api (deliverability reporting), sqs-ses-event-processor (suppression threshold checks)


Ephemeral — not retained. An internal cache for the OIDC public keys (JWKS) published by the configured identity provider. Populated on a schedule by the jwk-cache-updater Lambda. The authorizer and websocket-connect Lambdas read from this table to verify JWTs locally, avoiding an outbound call to the IdP on every API request.

This table is safe to recreate on each deployment because it is rebuilt from external sources.

Written by: jwk-cache-updater
Read by: authorizer, websocket-connect


Ephemeral — not retained. Tracks active client WebSocket connections for real-time push notification delivery. Entries carry a TTL and are automatically deleted when connections close or expire.

This table is safe to recreate on each deployment because it only holds transient session state.

Written by: websocket-connect
Deleted by: websocket-disconnect


Retained in data stack. A single encrypted S3 bucket serves multiple purposes, separated by key prefix. The same bucket is used by the email processing pipeline, the email sending pipeline, and the search infrastructure.

Temporary landing zone for raw emails deposited by SES during inbound receipt. The email-processor Lambda reads from this prefix, processes the message, and stores the structured content elsewhere. Objects in this prefix are automatically deleted after 7 days as a safety net for unprocessed messages.


Long-lived storage for structured email content — MIME parts, attachments, and inline blobs — organised by tenant and account. The JMAP API and the CDN blob endpoint read from here when serving message bodies and attachments to clients.


Per-account Tantivy full-text search index files, written and read exclusively by the tantivy-indexer Lambda. Each account’s index is stored as a set of segment files that the indexer loads, updates, and persists back to S3 on each indexing run.


Temporary staging area for client-uploaded blobs, which are consumed when an email is created from them. Objects here are automatically deleted after 1 day.