sendoka/
├── proxy.ts # Dashboard auth gate (renamed from middleware.ts in Next 16)
├── next.config.ts
├── drizzle.config.ts
├── vercel.ts # Cron registration (typed Vercel config)
├── vitest.config.ts # Test config
├── .env.example
├── AGENTS.md / CLAUDE.md
├── public/
├── docs/
└── src/
├── app/
│ ├── layout.tsx
│ ├── page.tsx # Marketing landing
│ ├── providers.tsx # SessionProvider + ToastProvider
│ ├── globals.css
│ ├── (auth)/ # Unauthenticated route group
│ │ ├── login/page.tsx
│ │ ├── register/page.tsx
│ │ ├── forgot-password/page.tsx
│ │ └── reset-password/page.tsx
│ ├── invite/page.tsx # Team invite acceptance
│ ├── docs/page.tsx # Public API reference
│ ├── dashboard/
│ │ ├── layout.tsx
│ │ ├── page.tsx
│ │ ├── messages/page.tsx
│ │ ├── api-keys/page.tsx
│ │ ├── domains/page.tsx
│ │ ├── webhooks/page.tsx
│ │ └── settings/
│ │ ├── page.tsx
│ │ ├── billing/page.tsx
│ │ └── team/
│ │ ├── page.tsx
│ │ └── invites.tsx # client component
│ └── api/
│ ├── auth/
│ │ ├── [...nextauth]/route.ts
│ │ ├── register/route.ts
│ │ ├── verify-email/route.ts
│ │ ├── forgot-password/route.ts
│ │ └── reset-password/route.ts
│ ├── internal/ # Session-auth'd
│ │ ├── api-keys/route.ts
│ │ ├── billing/route.ts
│ │ ├── domains/route.ts
│ │ ├── messages/route.ts
│ │ ├── webhooks/route.ts
│ │ ├── webhooks/rotate/route.ts
│ │ ├── webhook-deliveries/route.ts
│ │ ├── templates/route.ts
│ │ ├── team/invite/route.ts
│ │ ├── team/accept/route.ts
│ │ ├── org/route.ts
│ │ ├── org/switch/route.ts
│ │ ├── audit/route.ts
│ │ └── two-factor/
│ │ ├── setup/route.ts
│ │ ├── verify/route.ts
│ │ └── disable/route.ts
│ ├── v1/ # Public API
│ │ ├── emails/
│ │ │ ├── route.ts
│ │ │ ├── batch/route.ts
│ │ │ └── [emailId]/route.ts
│ │ └── sms/
│ │ ├── route.ts
│ │ ├── batch/route.ts
│ │ └── [smsId]/route.ts
│ ├── webhooks/
│ │ ├── ses/route.ts # SES events via SNS
│ │ ├── sns-sms/route.ts # SMS delivery status
│ │ └── stripe/route.ts
│ └── cron/
│ ├── send-scheduled/route.ts
│ ├── retry-webhooks/route.ts
│ ├── cleanup-idempotency/route.ts
│ └── report-overage/route.ts
├── components/
│ ├── dashboard/
│ │ ├── upgrade-button.tsx
│ │ └── org-switcher.tsx
│ └── ui/
│ ├── toast.tsx
│ ├── skeleton.tsx
│ ├── confirm.tsx
│ └── error-boundary.tsx
└── lib/
├── db/
│ ├── index.ts # drizzle + raw sql client
│ ├── migrations/ # generated SQL
│ └── schema/
│ ├── index.ts # barrel — 16 tables
│ ├── users.ts
│ ├── organizations.ts
│ ├── api-keys.ts
│ ├── messages.ts # +scheduled_at
│ ├── domains.ts
│ ├── webhooks.ts # +previous_secret
│ ├── webhook-deliveries.ts
│ ├── usage.ts
│ ├── idempotency.ts
│ ├── audit.ts
│ ├── email-verifications.ts
│ ├── password-resets.ts
│ ├── team-invites.ts
│ ├── templates.ts
│ └── two-factor.ts
├── auth/
│ ├── options.ts # NextAuth + Google/GitHub + TOTP
│ ├── api-key.ts
│ ├── active-org.ts # cookie-based org switcher
│ ├── email-verification.ts
│ └── totp.ts
├── api/
│ ├── middleware.ts # withApiAuth
│ ├── errors.ts
│ ├── validation.ts # +scheduled_at, attachments, template
│ ├── rate-limit.ts # per-plan
│ ├── idempotency.ts
│ ├── usage.ts # accepts `by` count
│ ├── usage-limit.ts
│ ├── webhook-fanout.ts # delivery log + retry
│ ├── sns-verify.ts # SNS signature check
│ ├── cursor.ts # compound-key pagination
│ ├── audit.ts # logAudit helper
│ └── template.ts # {{var}} substitution
├── billing/
│ ├── stripe.ts
│ └── plans.ts
├── providers/
│ ├── types.ts # EmailAttachment added
│ ├── email.ts # raw MIME + attachments
│ ├── sms.ts
│ └── domain.ts
└── utils/
├── id.ts
└── crypto.ts
Tests: co-located `*.test.ts` files in src/lib/**.