Send.dev - Technical Architecture & Backend Stack

A developer-first platform for sending communications (email → SMS → print), built on AWS for delivery infrastructure with Convex for real-time data and Upstash for high-performance caching.

Last Updated: January 2025


Architecture Overview

┌─────────────────────────────────────────────────────────────────────────────────┐
│                              SEND.DEV ARCHITECTURE                               │
├─────────────────────────────────────────────────────────────────────────────────┤
│                                                                                  │
│  ┌──────────────────┐        ┌──────────────────────────────────────────────┐   │
│  │    Next.js 15    │        │              AWS LAMBDA LAYER                │   │
│  │    Dashboard     │───────▶│  ┌────────────────────────────────────────┐  │   │
│  │   (send.dev)     │        │  │         API Gateway v2 (HTTP)          │  │   │
│  └──────────────────┘        │  │  • REST endpoints (/v1/*)              │  │   │
│                              │  │  • CORS enabled                         │  │   │
│  ┌──────────────────┐        │  │  • Custom domain support                │  │   │
│  │    Customer      │───────▶│  └────────────────────────────────────────┘  │   │
│  │      APIs        │        │                     │                        │   │
│  │   (REST/SDK)     │        │                     ▼                        │   │
│  └──────────────────┘        │  ┌────────────────────────────────────────┐  │   │
│                              │  │          Lambda Functions               │  │   │
│                              │  │  • send.ts (email API)                 │  │   │
│                              │  │  • templates.ts (CRUD)                 │  │   │
│                              │  │  • domains.ts (verification)           │  │   │
│                              │  │  • webhooks.ts (management)            │  │   │
│                              │  │  • analytics.ts (metrics)              │  │   │
│                              │  └────────────────────────────────────────┘  │   │
│                              └──────────────────────────────────────────────┘   │
│                                                   │                             │
│                     ┌─────────────────────────────┼─────────────────────────┐   │
│                     │                             │                         │   │
│                     ▼                             ▼                         ▼   │
│  ┌──────────────────────────┐  ┌──────────────────────────┐  ┌─────────────────┐│
│  │     UPSTASH REDIS        │  │        CONVEX            │  │    AWS SES      ││
│  │     (Cache Layer)        │  │    (Source of Truth)     │  │ (Email Delivery)││
│  │                          │  │                          │  │                 ││
│  │  • API Key cache         │  │  • users                 │  │  • SendEmail    ││
│  │  • Rate limiting         │  │  • apiKeys (master)      │  │  • DKIM/SPF     ││
│  │  • Usage counters        │  │  • templates             │  │  • Bounce/Comp  ││
│  │  • Session tokens        │  │  • emailLogs             │  │  • Tracking     ││
│  │                          │  │  • analytics             │  │                 ││
│  │  O(1) lookups ~1ms       │  │  • domains               │  │                 ││
│  │                          │  │  • webhooks              │  │                 ││
│  └──────────────────────────┘  └──────────────────────────┘  └────────┬────────┘│
│              │                             │                          │         │
│              │                             │                          │         │
│              └─────────────────────────────┼──────────────────────────┘         │
│                                            │                                    │
│  ┌──────────────────────────────────────────────────────────────────────────┐   │
│  │                         ASYNC PROCESSING                                  │   │
│  │                                                                           │   │
│  │  ┌────────────────┐  ┌────────────────┐  ┌────────────────┐              │   │
│  │  │   SQS Queue    │  │ SQS Priority   │  │   SQS DLQ      │              │   │
│  │  │  (standard)    │  │   (fast)       │  │  (failures)    │              │   │
│  │  └───────┬────────┘  └───────┬────────┘  └───────┬────────┘              │   │
│  │          │                   │                   │                        │   │
│  │          └───────────────────┼───────────────────┘                        │   │
│  │                              │                                            │   │
│  │                              ▼                                            │   │
│  │                   ┌────────────────────┐                                  │   │
│  │                   │   email-worker.ts  │                                  │   │
│  │                   │   (SQS Consumer)   │                                  │   │
│  │                   └────────────────────┘                                  │   │
│  │                                                                           │   │
│  │  ┌────────────────┐  ┌────────────────┐  ┌────────────────┐              │   │
│  │  │  SNS Topic     │  │  EventBridge   │  │ Upstash QStash │              │   │
│  │  │ (SES Events)   │─▶│  (Routing)     │─▶│ (Webhooks)     │              │   │
│  │  └────────────────┘  └────────────────┘  └────────────────┘              │   │
│  └──────────────────────────────────────────────────────────────────────────┘   │
│                                                                                  │
│  ┌──────────────────────────────────────────────────────────────────────────┐   │
│  │                              STORAGE                                      │   │
│  │                                                                           │   │
│  │  ┌────────────────┐  ┌────────────────┐  ┌────────────────┐              │   │
│  │  │ S3 Attachments │  │  S3 Archive    │  │  S3 Tracking   │              │   │
│  │  │  (30-day TTL)  │  │ (90d→Glacier)  │  │  (pixels)      │              │   │
│  │  └────────────────┘  └────────────────┘  └────────────────┘              │   │
│  └──────────────────────────────────────────────────────────────────────────┘   │
│                                                                                  │
└─────────────────────────────────────────────────────────────────────────────────┘

Complete Technology Stack

Core Infrastructure

LayerTechnologyPurpose
FrontendNext.js 15 + TurbopackDashboard web application
UI ComponentsShadCN/ui + @do/catalystConsistent design system
StylingTailwind CSS v4Utility-first CSS
API GatewayAWS API Gateway v2 (HTTP)REST endpoint routing
ComputeAWS Lambda (Node.js 20)Serverless functions
IaCSST v3 (Serverless Stack)Infrastructure as Code
MonorepoTurborepo + pnpmBuild system & dependencies
Code QualityBiome.jsLinting and formatting

Data Layer

StorageTechnologyPurpose
Primary DatabaseConvexReal-time data, source of truth
Cache LayerUpstash RedisAPI key cache, rate limiting
Rate Limiting@upstash/ratelimitPer-key enforcement
Webhook DeliveryUpstash QStashGuaranteed delivery with retry
File StorageAWS S3Attachments, archive, tracking

Email Delivery

ProviderRoleStatus
AWS SESPrimary (high volume, low cost)✅ Implemented
ResendFallback + React Email support✅ Implemented
SendGridFuture third provider🔜 Planned

SES Tenant Isolation (Multi-Tenant Reputation)

Send.dev uses AWS SES Tenants to provide per-organization email reputation isolation. This ensures that one customer's poor sending practices don't affect other customers' deliverability.

ComponentPurpose
SES TenantPer-organization reputation container
Configuration SetPer-tenant event tracking and metrics
Reputation PolicyAuto-pause rules ("standard", "strict", "none")
EventBridge RuleCaptures reputation findings for automation

Architecture:

┌─────────────────────────────────────────────────────────────────┐
│                    MULTI-TENANT ISOLATION                        │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  Organization A                    Organization B                │
│  ┌──────────────────────┐         ┌──────────────────────┐      │
│  │ Tenant: org-abc12345 │         │ Tenant: org-xyz67890 │      │
│  │ Config: send-dev-abc │         │ Config: send-dev-xyz │      │
│  │ Reputation: ISOLATED │         │ Reputation: ISOLATED │      │
│  └──────────┬───────────┘         └──────────┬───────────┘      │
│             │                                │                   │
│  ┌──────────▼───────────┐         ┌──────────▼───────────┐      │
│  │ Domains:             │         │ Domains:             │      │
│  │ • example.com        │         │ • myapp.io           │      │
│  │ • mail.example.com   │         │ • notifications.app  │      │
│  └──────────────────────┘         └──────────────────────┘      │
│                                                                  │
│  If Org A has high bounce rate → Only Org A is affected         │
│  Org B continues with normal deliverability                      │
└─────────────────────────────────────────────────────────────────┘

Reputation Policy Options:

  • standard (default): Auto-pause on HIGH severity findings only
  • strict: Auto-pause on ANY reputation finding
  • none: Monitoring only, never auto-pause

See: AWS SES Tenants Documentation

Authentication

TypeTechnologyPurpose
User AuthWorkOS AuthKitOAuth, OTP, SSO
API AuthAPI Keys (Convex + Upstash)Bearer token validation

API Key Architecture

Design Philosophy

Convex = Source of Truth | Upstash = Performance Cache

┌─────────────────────────────────────────────────────────────────────────────┐
│                          API KEY FLOW                                        │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  ┌─────────────┐      ┌─────────────────────────────────────────────────┐   │
│  │   API       │      │                 UPSTASH REDIS                    │   │
│  │  Request    │─────▶│  Key: apikey:{prefix}                           │   │
│  │             │      │  TTL: 5 minutes                                  │   │
│  └─────────────┘      │                                                  │   │
│        │              │  ┌─────────────────────────────────────────────┐ │   │
│        │              │  │ {                                           │ │   │
│        │              │  │   keyHash: "sha256...",                     │ │   │
│        │              │  │   userId: "user_xxx",                       │ │   │
│        │              │  │   organizationId: "org_xxx",                │ │   │
│        │ CACHE        │  │   permissions: ["send:email", ...],         │ │   │
│        │ HIT?         │  │   rateLimit: { requests: 1000, period: 60 },│ │   │
│        │              │  │   services: ["send", "sms", "voip"],        │ │   │
│        │              │  │   environment: "live" | "test",             │ │   │
│        │              │  │   status: "active",                         │ │   │
│        │              │  │   expiresAt: 1234567890                     │ │   │
│        │              │  │ }                                           │ │   │
│        │              │  └─────────────────────────────────────────────┘ │   │
│        │              └─────────────────────────────────────────────────────┘   │
│        │                           │                                         │
│        │ YES ◄─────────────────────┘                                         │
│        │                                                                     │
│        │ NO (cache miss)                                                     │
│        ▼                                                                     │
│  ┌─────────────────────────────────────────────────────────────────────────┐ │
│  │                           CONVEX DATABASE                                │ │
│  │                        (Source of Truth)                                 │ │
│  │                                                                          │ │
│  │  Table: apiKeys                                                          │ │
│  │  ┌─────────────────────────────────────────────────────────────────────┐│ │
│  │  │ _id, keyHash, keyPrefix, userId, organizationId, name,              ││ │
│  │  │ permissions, rateLimit, services, environment, status,              ││ │
│  │  │ expiresAt, lastUsedAt, createdAt, deletedAt                         ││ │
│  │  └─────────────────────────────────────────────────────────────────────┘│ │
│  │                                                                          │ │
│  │  On lookup: Fetch from Convex → Write to Upstash cache → Return         │ │
│  └─────────────────────────────────────────────────────────────────────────┘ │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

Key Format (do.dev Ecosystem)

do_live_abcdef1234567890xyz     # Production key (32 char random suffix)
do_test_abcdef1234567890xyz     # Test/sandbox key

┌──────────┬────────────────────────────┐
│  Prefix  │         Random ID          │
│ (lookup) │      (never stored)        │
├──────────┼────────────────────────────┤
│ do_live_ │ abcdef1234567890xyz        │
│ (8 char) │ (24 char, cryptographic)   │
└──────────┴────────────────────────────┘

Storage: Only keyHash (SHA-256) + keyPrefix stored
Lookup: Extract prefix → Check Upstash → Fallback to Convex → Verify hash

Cache Strategy

OperationUpstashConvex
Key ValidationCheck first (O(1), ~1ms)Fallback on cache miss
Key CreationWrite-throughPrimary write
Key RevocationImmediate deleteUpdate status
Rate Limiting@upstash/ratelimit-
Usage TrackingAtomic INCRPeriodic sync

Upstash Redis Structure

KEYS:
─────────────────────────────────────────────────────────────────
apikey:{prefix}              → JSON (key metadata, 5min TTL)
apikey:usage:{prefix}        → HASH (requests, lastUsed, byService)
ratelimit:{prefix}:{window}  → (managed by @upstash/ratelimit)

SECONDARY INDEXES (Convex handles these):
─────────────────────────────────────────────────────────────────
user:keys:{userId}           → Convex query (by_user index)
org:keys:{orgId}             → Convex query (by_organization index)

Why This Architecture?

RequirementSolution
Cross-service keysSame do_live_xxx key works for send.dev, sms.dev, voip.dev
Sub-ms validationUpstash Redis global replication
Data durabilityConvex as authoritative source
Rate limiting@upstash/ratelimit (sliding window)
Audit trailFull history in Convex
Real-time syncConvex subscriptions for dashboard

Convex Database Schema

Tables Overview

TablePurposeKey Fields
usersUser profiles (WorkOS sync)authId, email, roles, timestamps
apiKeysAPI key management (master)keyHash, keyPrefix, permissions, services
desktopSessionsToken refresh storagesessionId, refreshToken, expiresAt
templatesEmail templatessubject, htmlContent, variables, status
emailLogsEmail trackingmessageId, status, opens, clicks, sentiment
analyticsMetrics aggregationperiod, timestamp, emailsSent, delivered
domainsDomain verificationdomain, dnsRecords, dkimTokens, status
webhooksEvent notificationsurl, events, secret, health tracking
suppressionsBounce/complaint listemail, reason, source, timestamp

Schema Definition

// packages/convex-send/convex/schema.ts

import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";

export default defineSchema({
  users: defineTable({
    authId: v.string(),
    email: v.string(),
    name: v.optional(v.string()),
    avatarUrl: v.optional(v.string()),
    roles: v.array(v.string()),
    organizationId: v.optional(v.string()),
    createdAt: v.number(),
    updatedAt: v.number(),
  })
    .index("by_auth_id", ["authId"])
    .index("by_email", ["email"]),

  apiKeys: defineTable({
    keyHash: v.string(),           // SHA-256 hash (never store raw key)
    keyPrefix: v.string(),         // First 16 chars for lookup (do_live_abcdef12)
    userId: v.string(),
    organizationId: v.optional(v.string()),
    name: v.string(),
    permissions: v.array(v.string()),  // ["send:email", "templates:read", ...]
    services: v.array(v.string()),     // ["send", "sms", "voip"] - do.dev ecosystem
    environment: v.string(),           // "live" | "test"
    rateLimit: v.object({
      requests: v.number(),
      period: v.number(),              // seconds
    }),
    status: v.string(),                // "active" | "revoked" | "expired"
    expiresAt: v.optional(v.number()),
    lastUsedAt: v.optional(v.number()),
    createdAt: v.number(),
    deletedAt: v.optional(v.number()),
  })
    .index("by_key_prefix", ["keyPrefix"])
    .index("by_user", ["userId"])
    .index("by_organization", ["organizationId"])
    .index("by_status", ["status"]),

  templates: defineTable({
    userId: v.string(),
    organizationId: v.optional(v.string()),
    name: v.string(),
    subject: v.string(),
    htmlContent: v.string(),
    textContent: v.optional(v.string()),
    variables: v.array(v.string()),    // Extracted variable names
    status: v.string(),                // "draft" | "active" | "archived"
    version: v.number(),
    createdAt: v.number(),
    updatedAt: v.number(),
  })
    .index("by_user", ["userId"])
    .index("by_organization", ["organizationId"]),

  emailLogs: defineTable({
    messageId: v.string(),             // send_xxxxxxxxxxxxx
    userId: v.string(),
    organizationId: v.optional(v.string()),
    apiKeyPrefix: v.string(),
    templateId: v.optional(v.id("templates")),
    from: v.string(),
    to: v.string(),
    subject: v.string(),
    status: v.string(),                // "pending" | "sent" | "delivered" | "bounced" | "complained"
    provider: v.string(),              // "ses" | "resend"
    providerMessageId: v.optional(v.string()),
    opens: v.number(),
    clicks: v.number(),
    links: v.optional(v.array(v.object({
      url: v.string(),
      clicks: v.number(),
    }))),
    firstOpenedAt: v.optional(v.number()),
    lastOpenedAt: v.optional(v.number()),
    deliveredAt: v.optional(v.number()),
    bouncedAt: v.optional(v.number()),
    bounceType: v.optional(v.string()),
    complainedAt: v.optional(v.number()),
    errorMessage: v.optional(v.string()),
    createdAt: v.number(),
    updatedAt: v.number(),
  })
    .index("by_message_id", ["messageId"])
    .index("by_user", ["userId"])
    .index("by_organization", ["organizationId"])
    .index("by_status", ["status"])
    .index("by_created", ["createdAt"]),

  analytics: defineTable({
    userId: v.string(),
    organizationId: v.optional(v.string()),
    period: v.string(),                // "hourly" | "daily"
    timestamp: v.number(),             // Start of period
    emailsSent: v.number(),
    emailsDelivered: v.number(),
    emailsBounced: v.number(),
    emailsComplained: v.number(),
    emailsOpened: v.number(),
    emailsClicked: v.number(),
    uniqueOpens: v.number(),
    uniqueClicks: v.number(),
  })
    .index("by_user_period", ["userId", "period", "timestamp"])
    .index("by_org_period", ["organizationId", "period", "timestamp"]),

  domains: defineTable({
    userId: v.string(),
    organizationId: v.optional(v.string()),
    domain: v.string(),
    status: v.string(),                // "pending" | "verifying" | "verified" | "failed"
    dkimStatus: v.string(),            // "pending" | "success" | "failed"
    verificationToken: v.string(),
    dkimTokens: v.array(v.string()),   // 3 tokens from SES
    dnsRecords: v.array(v.object({
      type: v.string(),
      name: v.string(),
      value: v.string(),
    })),
    verifiedAt: v.optional(v.number()),
    createdAt: v.number(),
    updatedAt: v.number(),
  })
    .index("by_domain", ["domain"])
    .index("by_user", ["userId"])
    .index("by_organization", ["organizationId"]),

  webhooks: defineTable({
    userId: v.string(),
    organizationId: v.optional(v.string()),
    url: v.string(),
    secret: v.string(),                // For HMAC signing
    events: v.array(v.string()),       // ["email.sent", "email.delivered", ...]
    status: v.string(),                // "active" | "paused" | "failed"
    failureCount: v.number(),
    lastDeliveredAt: v.optional(v.number()),
    lastFailedAt: v.optional(v.number()),
    createdAt: v.number(),
    updatedAt: v.number(),
  })
    .index("by_user", ["userId"])
    .index("by_organization", ["organizationId"]),

  suppressions: defineTable({
    email: v.string(),
    userId: v.string(),
    organizationId: v.optional(v.string()),
    reason: v.string(),                // "bounce" | "complaint" | "unsubscribe" | "manual"
    bounceType: v.optional(v.string()),
    source: v.string(),                // "ses" | "manual"
    createdAt: v.number(),
  })
    .index("by_email", ["email"])
    .index("by_user", ["userId"]),
});

AWS Infrastructure (SST)

Stack Configuration

// apps/projects/send/aws/sst.config.ts

export default $config({
  app(input) {
    return {
      name: "send-dev",
      removal: input?.stage === "production" ? "retain" : "remove",
      home: "aws",
      providers: {
        aws: { region: "us-east-1" },
      },
    };
  },
  async run() {
    // === SQS QUEUES ===
    const dlq = new sst.aws.Queue("SendEmailDLQ", {
      retention: "14 days",
    });

    const emailQueue = new sst.aws.Queue("SendEmailQueue", {
      dlq: dlq.arn,
      visibilityTimeout: "60 seconds",
    });

    const priorityQueue = new sst.aws.Queue("SendEmailPriorityQueue", {
      dlq: dlq.arn,
      visibilityTimeout: "30 seconds",
    });

    // === S3 BUCKETS ===
    const attachmentsBucket = new sst.aws.Bucket("SendAttachments", {
      lifecycle: [{ expiration: "30 days" }],
    });

    const archiveBucket = new sst.aws.Bucket("SendEmailArchive", {
      lifecycle: [
        { transition: { days: 90, storageClass: "GLACIER" } },
      ],
    });

    const trackingBucket = new sst.aws.Bucket("SendTracking", {
      cors: [{ allowedOrigins: ["*"], allowedMethods: ["GET"] }],
    });

    // === LAMBDA WORKERS ===
    const emailWorker = new sst.aws.Function("SendEmailWorker", {
      handler: "functions/workers/email-worker.handler",
      timeout: "60 seconds",
      memory: "512 MB",
      environment: {
        CONVEX_URL: process.env.CONVEX_URL!,
        RESEND_API_KEY: process.env.RESEND_API_KEY!,
        UPSTASH_REDIS_URL: process.env.UPSTASH_REDIS_URL!,
        UPSTASH_REDIS_TOKEN: process.env.UPSTASH_REDIS_TOKEN!,
      },
    });

    emailQueue.subscribe(emailWorker, { batch: { size: 10 } });
    priorityQueue.subscribe(emailWorker, { batch: { size: 5 } });

    // === API GATEWAY ===
    const api = new sst.aws.ApiGatewayV2("SendAPI", {
      cors: {
        allowOrigins: ["*"],
        allowMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
        allowHeaders: ["Content-Type", "Authorization"],
      },
    });

    // Email endpoints
    api.route("POST /v1/emails", "functions/emails/send.handler");
    api.route("POST /v1/emails/batch", "functions/emails/batch.handler");
    api.route("GET /v1/emails/{id}", "functions/emails/get.handler");

    // Template endpoints
    api.route("GET /v1/templates", "functions/templates/list.handler");
    api.route("POST /v1/templates", "functions/templates/create.handler");
    api.route("GET /v1/templates/{id}", "functions/templates/get.handler");
    api.route("PUT /v1/templates/{id}", "functions/templates/update.handler");
    api.route("DELETE /v1/templates/{id}", "functions/templates/delete.handler");

    // Domain endpoints
    api.route("GET /v1/domains", "functions/domains/list.handler");
    api.route("POST /v1/domains", "functions/domains/create.handler");
    api.route("GET /v1/domains/{id}/verify", "functions/domains/verify.handler");

    // Webhook endpoints
    api.route("GET /v1/webhooks", "functions/webhooks/list.handler");
    api.route("POST /v1/webhooks", "functions/webhooks/create.handler");
    api.route("PUT /v1/webhooks/{id}", "functions/webhooks/update.handler");
    api.route("DELETE /v1/webhooks/{id}", "functions/webhooks/delete.handler");

    // Analytics endpoints
    api.route("GET /v1/analytics/overview", "functions/analytics/overview.handler");
    api.route("GET /v1/analytics/events", "functions/analytics/events.handler");

    return {
      api: api.url,
      queues: {
        email: emailQueue.url,
        priority: priorityQueue.url,
        dlq: dlq.url,
      },
      buckets: {
        attachments: attachmentsBucket.name,
        archive: archiveBucket.name,
        tracking: trackingBucket.name,
      },
    };
  },
});

Lambda Functions Structure

apps/projects/send/aws/
├── sst.config.ts
├── functions/
│   ├── emails/
│   │   ├── send.ts           # POST /v1/emails
│   │   ├── batch.ts          # POST /v1/emails/batch
│   │   └── get.ts            # GET /v1/emails/{id}
│   ├── templates/
│   │   ├── list.ts
│   │   ├── create.ts
│   │   ├── get.ts
│   │   ├── update.ts
│   │   └── delete.ts
│   ├── domains/
│   │   ├── list.ts
│   │   ├── create.ts
│   │   └── verify.ts
│   ├── webhooks/
│   │   ├── list.ts
│   │   ├── create.ts
│   │   ├── update.ts
│   │   └── delete.ts
│   ├── analytics/
│   │   ├── overview.ts
│   │   └── events.ts
│   ├── workers/
│   │   ├── email-worker.ts   # SQS consumer - sends emails
│   │   ├── dlq-processor.ts  # DLQ handler
│   │   └── event-processor.ts # SNS/SES event handler
│   └── lib/
│       ├── auth.ts           # API key validation (Upstash + Convex)
│       ├── convex.ts         # Convex client
│       ├── upstash.ts        # Upstash Redis client
│       ├── validation.ts     # Request validation
│       └── providers/
│           ├── ses.ts        # AWS SES integration
│           └── resend.ts     # Resend integration

Email Processing Flow

┌─────────────────────────────────────────────────────────────────────────────┐
│                          EMAIL SEND FLOW                                     │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  1. API REQUEST                                                              │
│  ───────────────                                                             │
│  POST /v1/emails                                                             │
│  Authorization: Bearer do_live_xxxxxxxxxxxxx                                 │
│  {                                                                           │
│    "from": "hello@example.com",                                              │
│    "to": "user@email.com",                                                   │
│    "subject": "Welcome!",                                                    │
│    "html": "<h1>Hello</h1>",                                                 │
│    "priority": "normal"                                                      │
│  }                                                                           │
│                                                                              │
│  2. AUTH & VALIDATION (Lambda: send.ts)                                      │
│  ───────────────────────────────────────                                     │
│  ┌────────────────────────────────────────────────────────────────────────┐ │
│  │ a) Extract API key from header                                          │ │
│  │ b) Check Upstash cache: apikey:{prefix}                                 │ │
│  │    └─ Cache hit? → Verify hash, check permissions                       │ │
│  │    └─ Cache miss? → Query Convex → Cache result (5min TTL)              │ │
│  │ c) Check rate limit: @upstash/ratelimit                                 │ │
│  │ d) Validate request payload                                             │ │
│  │ e) Check domain authorization (Convex: domains table)                   │ │
│  │ f) Check suppression list (Convex: suppressions table)                  │ │
│  └────────────────────────────────────────────────────────────────────────┘ │
│                                                                              │
│  3. CREATE LOG & QUEUE                                                       │
│  ─────────────────────                                                       │
│  ┌────────────────────────────────────────────────────────────────────────┐ │
│  │ a) Generate messageId: send_xxxxxxxxxxxxx                               │ │
│  │ b) Create emailLog in Convex (status: "pending")                        │ │
│  │ c) Queue to SQS (normal or priority based on request)                   │ │
│  │ d) Increment usage counter in Upstash                                   │ │
│  │ e) Return { id: messageId, status: "queued" }                           │ │
│  └────────────────────────────────────────────────────────────────────────┘ │
│                                                                              │
│  4. ASYNC PROCESSING (Lambda: email-worker.ts)                               │
│  ─────────────────────────────────────────────                               │
│  ┌────────────────────────────────────────────────────────────────────────┐ │
│  │ a) Receive batch from SQS (up to 10 messages)                           │ │
│  │ b) For each message:                                                    │ │
│  │    ├─ Fetch template if templateId provided (Convex)                    │ │
│  │    ├─ Render variables (Handlebars)                                     │ │
│  │    ├─ Select provider (SES primary, Resend fallback)                    │ │
│  │    ├─ Send email via provider                                           │ │
│  │    ├─ Update emailLog (status: "sent", providerMessageId)               │ │
│  │    └─ On error: Update log, message goes to DLQ after 3 retries         │ │
│  └────────────────────────────────────────────────────────────────────────┘ │
│                                                                              │
│  5. EVENT PROCESSING (Lambda: event-processor.ts)                            │
│  ─────────────────────────────────────────────                               │
│  ┌────────────────────────────────────────────────────────────────────────┐ │
│  │ SES → SNS → Lambda                                                      │ │
│  │                                                                          │ │
│  │ Events: send, delivery, bounce, complaint, open, click                  │ │
│  │                                                                          │ │
│  │ a) Parse SNS message                                                    │ │
│  │ b) Extract messageId from tags                                          │ │
│  │ c) Update emailLog in Convex                                            │ │
│  │ d) Update analytics counters                                            │ │
│  │ e) If bounce/complaint: Add to suppressions table                       │ │
│  │ f) Queue webhook delivery via Upstash QStash                            │ │
│  └────────────────────────────────────────────────────────────────────────┘ │
│                                                                              │
│  6. WEBHOOK DELIVERY (Upstash QStash)                                        │
│  ────────────────────────────────────                                        │
│  ┌────────────────────────────────────────────────────────────────────────┐ │
│  │ a) Fetch active webhooks for user (Convex)                              │ │
│  │ b) For each webhook matching event type:                                │ │
│  │    ├─ Sign payload with HMAC-SHA256 (webhook.secret)                    │ │
│  │    ├─ POST to webhook.url with signature header                         │ │
│  │    ├─ Retry with exponential backoff (up to 5 attempts)                 │ │
│  │    └─ Update webhook health status in Convex                            │ │
│  └────────────────────────────────────────────────────────────────────────┘ │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

API Endpoints

Email API

POST   /v1/emails           # Send single email
POST   /v1/emails/batch     # Send batch (up to 1000)
GET    /v1/emails/{id}      # Get email status and events

Templates API

GET    /v1/templates        # List templates
POST   /v1/templates        # Create template
GET    /v1/templates/{id}   # Get template
PUT    /v1/templates/{id}   # Update template
DELETE /v1/templates/{id}   # Delete template

Domains API

GET    /v1/domains          # List domains
POST   /v1/domains          # Add domain
GET    /v1/domains/{id}/verify  # Get DNS records needed
POST   /v1/domains/{id}/verify  # Trigger verification

Webhooks API

GET    /v1/webhooks         # List webhooks
POST   /v1/webhooks         # Create webhook
PUT    /v1/webhooks/{id}    # Update webhook
DELETE /v1/webhooks/{id}    # Delete webhook

Analytics API

GET    /v1/analytics/overview      # Dashboard stats
GET    /v1/analytics/deliverability # Bounce/complaint rates
GET    /v1/analytics/events        # Event stream

Shared Packages

@do/api-keys (Planned)

Shared API key management for the do.dev ecosystem.

packages/api-keys/
├── src/
│   ├── index.ts           # Main exports
│   ├── client.ts          # Upstash Redis client
│   ├── generate.ts        # Key generation (do_live_xxx, do_test_xxx)
│   ├── validate.ts        # Cache lookup + hash verification
│   ├── permissions.ts     # Permission definitions and checking
│   ├── ratelimit.ts       # @upstash/ratelimit integration
│   └── types.ts           # TypeScript interfaces
├── package.json
└── tsconfig.json

Usage across do.dev services:

import { validateApiKey, checkRateLimit } from "@do/api-keys";

// In Lambda auth middleware
const key = await validateApiKey(authHeader, {
  service: "send",                    // Check service access
  permissions: ["send:email"],        // Required permissions
});

if (!key) {
  return { statusCode: 401, body: "Unauthorized" };
}

const { allowed } = await checkRateLimit(key.prefix);
if (!allowed) {
  return { statusCode: 429, body: "Rate limit exceeded" };
}

Environment Variables

Required for AWS Lambda

# Convex
CONVEX_URL=https://standing-bird-371.convex.cloud
CONVEX_DEPLOY_KEY=prod:standing-bird-371:xxxxxx

# Upstash Redis (API Key Cache + Rate Limiting)
UPSTASH_REDIS_URL=https://xxx.upstash.io
UPSTASH_REDIS_TOKEN=AxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxQ=

# Upstash QStash (Webhook Delivery)
QSTASH_URL=https://qstash.upstash.io
QSTASH_TOKEN=xxxxxxxxxx

# Email Providers
RESEND_API_KEY=re_xxxxxxxxxxxxx
# AWS SES uses IAM role credentials

# Optional
SES_CONFIGURATION_SET=send-dev-production

Required for Next.js Dashboard

# Convex
NEXT_PUBLIC_CONVEX_URL=https://standing-bird-371.convex.cloud

# WorkOS Auth
WORKOS_API_KEY=sk_xxxxxx
WORKOS_CLIENT_ID=client_xxxxxx
NEXT_PUBLIC_WORKOS_REDIRECT_URI=https://send.dev/api/auth/callback

Phase Rollout Plan

Phase 1: API Key Infrastructure

TaskStatus
Create @do/api-keys package🔜 Planned
Upstash Redis setup🔜 Planned
Update Lambda auth layer🔜 Planned
Dashboard API key management🔜 Planned

Phase 2: Email Core Completion

TaskStatus
Template variable rendering (Handlebars)🔜 Planned
Attachment handling (S3 presigned URLs)✅ Infrastructure ready
SES event processing (SNS subscription)🔜 Planned
Suppression list management🔜 Planned

Phase 3: Webhook Delivery

TaskStatus
Upstash QStash integration🔜 Planned
Webhook signatures (HMAC-SHA256)🔜 Planned
Delivery tracking & retry🔜 Planned
Dashboard webhook management🔜 Planned

Phase 4: Dashboard & Analytics

TaskStatus
Metrics dashboard (charts)🔜 Planned
Email logs browser🔜 Planned
Template editor🔜 Planned
Domain manager🔜 Planned

Phase 5: SMS (Future)

TaskStatus
AWS Pinpoint integration📋 Backlog
Phone number provisioning📋 Backlog
Two-way messaging📋 Backlog

Phase 6: Print (Future)

TaskStatus
Lob API integration📋 Backlog
Address verification📋 Backlog
Template designer📋 Backlog

Cost Estimates (Monthly)

Service100K emails/mo1M emails/mo
AWS Lambda$5$25
AWS API Gateway$3$15
AWS SQS$1$5
AWS SES$10$100
AWS S3$5$20
ConvexFree tier$25
Upstash Redis$10$30
Upstash QStash$5$20
Vercel (Frontend)$20$20
Total~$60~$260

Security Considerations

API Security

  • API keys with SHA-256 hashing (never stored plain)
  • Scoped permissions per key
  • Rate limiting per key
  • Key expiration support
  • IP allowlisting (optional feature)

Data Security

  • TLS everywhere
  • S3 encryption at rest
  • Convex encryption at rest
  • PII handling (email content encryption)
  • 90-day message retention default

Email Security

  • Domain verification required
  • DKIM/SPF/DMARC enforcement
  • Automatic suppression lists
  • Reputation monitoring per domain
  • Abuse detection (volume spikes)

Infrastructure

  • AWS IAM least-privilege
  • Secrets in environment variables
  • Audit logging (CloudTrail)
  • Monitoring & alerting (CloudWatch)

On this page