惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

N
News and Events Feed by Topic
Malwarebytes
Malwarebytes
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
C
Cybersecurity and Infrastructure Security Agency CISA
F
Future of Privacy Forum
C
Cisco Blogs
T
The Exploit Database - CXSecurity.com
A
Arctic Wolf
S
Securelist
K
Kaspersky official blog
S
Schneier on Security
T
ThreatConnect
T
Tenable Blog
Spread Privacy
Spread Privacy
T
True Tiger Recordings
AWS News Blog
AWS News Blog
F
Fox-IT International blog
量子位
T
Threatpost
V
Vulnerabilities – Threatpost
C
CERT Recently Published Vulnerability Notes
Cisco Talos Blog
Cisco Talos Blog
GbyAI
GbyAI
宝玉的分享
宝玉的分享
腾讯CDC
G
Google Developers Blog
aimingoo的专栏
aimingoo的专栏
Cyberwarzone
Cyberwarzone
有赞技术团队
有赞技术团队
S
SegmentFault 最新的问题
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
V
Visual Studio Blog
U
Unit 42
雷峰网
雷峰网
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Simon Willison's Weblog
Simon Willison's Weblog
O
OpenAI News
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
The GitHub Blog
The GitHub Blog
The Register - Security
The Register - Security
MyScale Blog
MyScale Blog
小众软件
小众软件
A
About on SuperTechFans
Last Week in AI
Last Week in AI
Y
Y Combinator Blog
博客园 - 三生石上(FineUI控件)
美团技术团队
Google Online Security Blog
Google Online Security Blog
P
Proofpoint News Feed
MongoDB | Blog
MongoDB | Blog

DEV Community

Asynchronous Functions in Dart Edge Computing He creado una empresa ficticia IT/OT para poder encontrar sus vulnerabilidades y reforzar su seguridad en sus activos críticos Why I Built @editora/react I built a tiny UGC script generator because hooks are the hardest part The Phone Is Becoming the New Terminal Why Most AI Music Tools Feel Wrong to Developers Goroutines vs. Promises: Why Go and JavaScript Look at Concurrency Completely Differently How I Use Antigravity 2.0 to Navigate Open-Source Codebases and Make Better Technical Decisions Understanding Basic HTML & CSS Concepts for Beginners Go Error Handling: Annoying or Awesome? Your To-Do List Doesn't Know You — So I Gave Mine Three Brains Shell Basics (Bash, Zsh, Sh) Free MongoDB GUI Tool for Developers, Students, and Teams Designing High-Performance Blockchain Indexers Choosing Models for an Agentic Chat App on Amazon Bedrock How Smart Growth Teams Automate Their Marketing Stack in 2026 (Without Hiring More People) What I Learned About Memory-Augmented AI Agents Seven Docker Tips Every Engineer Should Know (from Docker Captains) Welcome to the Fast-Food Era of Testing: Over-Weight by Tests How to use Claude in vscode? Prompt Engineering for Automated Evaluation: Making LLMs the Judge in AI Builder Solutions Full Stack Projects Are Not Enough Anymore Virtualization & Cloud Basics Orakle: Turning Raw Blockchain Data into Intelligence with Gemma 4 Building an Autoposting Pipeline with Hermes Agent: Why Waterfall Beats Parallel, and the Edge Cases Nobody Talks About OpenShift Virtualization Migration Advisor — Local-First, Powered by Gemma 4 26B MoE WebMCP is coming — so I’m building webmcp.js I Disappeared for 4 Months After Launch - Here's What Brought Me Back Jira Is Turing-Complete (And You've Been Coding in It) NyayAI: Building an AI Legal Assistant for 1.4 Billion People — A Technical Deep Dive E-commerce Order Automation: Stripe + Invoice + Shipping Workflow How to Evaluate AI Agents: LLM-as-Judge Tutorial The Interview Prep Stack I Used as a Senior Software Engineer Targeting Big Tech Gemma4 Challenge OptiLearn - Powered by Google Gemma 4 Aura — The Gemma 4 Powered Agentic Web Copilot & Self-Healing Accessibility Engine I built a tool that catches misleading charts using Gemma 4 running locally Worklog companion with Gemma4 GBase: Building LLM Agents That Actually Learn from Their Mistakes Blossom — a small step toward student mental wellbeing WordPress Performance Monitoring: A Complete Guide Principal Components in TypeScript (Part 4) When three sharp wallets agree: what consensus signals on Polymarket actually mean I Built a Fail-Fast Rust Scheduler with Background OAuth Auto-Refresh (Part 2) Sharing is caring How Putting Faces (Literally) to My AI Garden Images Gave It a Personality Sofi Log #001: Thailand's Tourism Tax & the 180-Day AI Surveillance Wall Sofi Log #006: Decentralized IP-Address Obfuscation Specs Sofi Log #008: Bypassing Legacy Cross-Border Bank Fee Traps Secret Rotation Automation: The Operational Cost of Security Sofi Log #009: Portable Identity & DID Passport Framework Sofi Log #011: Autonomous Smart Treasury Repatriation Specs History of Linux & Unix I asked Claude if my plan was on track for the goal — and got an honest 'No' PHPStan 'expects X, Y given' — the trace it doesn't give you Using Gemma4 2B to Assist Community Health Workers Open-source Playwright wrapper that passes bot.sannysoft.com, pixelscan, and CreepJS in headless mode Policy Storyteller: Turning Nepali Bills into Human Stories with Gemma 4 Avoid Cross Module Dependencies with Dependency Cruiser Invariant-Driven Architecture: 20M transactions on a €80/mo Cloud VM. Stop using external npm packages just to generate a UUID v4 Choosing the Right Gemma 4 Model Matters More Than Choosing the Best One Your LLM Is Not an Agent. Your Framework Is Not Enough. You Need a Harness. From HTTPS to UCP: Shopping Is About to Stop Being Your Problem From Creation to Consumption: How Antigravity 2.0 and Gemini Spark Are Defining the Agentic Era 10 Mistakes I Wish I Knew Before Taking the CKA Exam AI That Actually Does Stuff: Autonomous Agents Explained Exploring AI workflow Orchestration: Comparing Weft, Python & Alternative Pipeline Approaches El Poder del Aprendizaje Federado: Cuando los Algoritmos Distribuidos Entrenan a la IA Email Marketing Automation in 2026: 5 Tools (and 1 Self-Hosted) Through Their APIs A Replay Runbook For Missed Publishing Windows Why timeout handling matters more than most backend logic How I Make $6,800/Month Selling Niche VS Code Extensions Model Routing Cost Checklist: Hosted APIs, Open Models, Or Self-Hosted Inference? ORA-00207 오류 원인과 해결 방법 완벽 가이드 Deno 2.8 Operator Upgrade Checklist: CI, Lockfiles, Node Compatibility, And Rollback AI-Discovered Vulnerabilities Need A Triage Queue, Not A Panic Channel AI Agent Workboards Need Audit Controls Before They Need More Agents Demystifying DevRel: What It Actually Is (And Why Should You Become One?) Your AI, Your Device, Your Data - Introducing Aide Gemma 4 GenAI Coach - GenAI Concepts Made Easy with an Interactive Playground QuietPulse - Mood Tracker Principal Components in TypeScript (Part 3) The pgAudit Attribution Gap: Why Role-Level Logging Fails GDPR and How to Close It Gemma 4 CAD Orchestrator I built a local Postgres triage co-pilot because HIPAA says I can't paste plans into ChatGPT or Claude Live Holographic Editor In Fractal Time Everbench: A document management system with Local Intelligence Instanton in Fractal Time The Hidden Features of Claude How I Built an AI News Brief with Next.js, Supabase, Vercel, and GPT-4o-mini How We Built a Multi-Agent AI Documentation System (And What We Learned) I got tired of writing post-mortems — so I built RCAi for SREs MIA: A Futuristic AI Desktop Assistant Built with Voice, Gestures, and Controlled Chaos Best Programming Language for Backend Web Development: PHP vs Python PayPal Alternatives for Indian Businesses: Best Payment Gateways for International Card Payments (2026) Gemma 4 Made Me Rethink Local AI: Not Just Text, But Images Too Clean Architecture in .NET Explained (The Dependency Rule) I Compiled Rust to WebAssembly and Made My JavaScript 6 Faster
How I migrated magic-link login from Resend to AWS SES + Lambda five days before launch
thebrecht · 2026-05-25 · via DEV Community

I run toui.io, a URL shortener I shipped to the public on April 7, 2026. Eleven days before launch I had passwordless email login working on Resend. Five days before launch I tore it out and rebuilt the same flow on AWS — Lambda + DynamoDB + SES + API Gateway, packaged as a SAM stack.

The whole migration took one afternoon. I want to write about why I did it, what the stack looks like, and the one thing I'd do differently.

This isn't a Resend hit piece. Resend is excellent for product engineers who want clean DX, a great API, and to never think about deliverability. If you're shipping a side project, use it. The math just changes when you start projecting actual production volume.

The math that pushed me off Resend

When I shipped the Resend version on March 27 the price was $0/month — toui.io was a side project sending zero emails. Then I sat down and projected the year-one cost curve.

Resend's pricing, at the time of writing (May 2026):

Tier Monthly cost Email cap
Free $0 3,000/month (100/day)
Pro $20 50,000/month
Pro $35 100,000/month
Scale $90–$1,150 100,000–2,500,000/month

AWS SES pricing, also today:

Volume Monthly cost
First 3,000/month (first 12 months) $0
Anything else $0.10 per 1,000 emails

That's not a small gap. Some scenarios I sketched, with Resend on the right and SES on the left (Resend Scale tier breakpoints taken from the same pricing page):

Monthly volume SES Resend Multiple
10,000 $1.00 $20 (Pro 50k) 20×
100,000 $10.00 $35 (Pro 100k) 3.5×
500,000 $50.00 $350 (Scale)
1,000,000 $100.00 $650 (Scale) 6.5×
2,500,000 $250.00 $1,150 (Scale top) 4.6×

The 20× number at the low end isn't because SES is magic. It's because Resend Pro has a $20 floor — you pay the Pro fee the moment you cross 3,000 emails or 100/day, regardless of whether your actual usage is 10,001 or 49,999. SES's $0.10/1,000 is linear past the free tier.

For toui.io's profile — magic-link login + occasional product emails (welcome, billing receipts, announcements) — I was projecting somewhere between 10k and 100k emails per month in year one. That's solidly inside the "3.5–20×" cost-gap band.

And there's a second thing: Resend Free's 100/day daily cap. URL shorteners get viral spikes. A single shared post can dump 200 signups in an hour. Resend Free shuts that down at 100; Resend Pro fixes it but you're back on the $20+ tier. SES has no monthly cap and a per-second sending rate that scales up automatically as you build reputation.

The math said move now. Five days before launch is the worst possible time to do anything risky, but it's the best possible time to do something whose risk is "rebuild a working flow on a different vendor, in one well-isolated stack."

What I built

The whole magic-link service became a separate AWS stack, deployable independently of the Cloudflare Worker that hosts the rest of toui.io:

Frontend (toui.io)         API Gateway (auth.toui.io)        CF Worker (toui.io)
   │                            │                                │
   ├── POST /send {email} ─────►│                                │
   │                            ├─ DynamoDB: store token (TTL)   │
   │                            ├─ SES: send email with link     │
   ◄── 200 OK ──────────────────┤                                │
   │                            │                                │
   │  (user clicks email link)  │                                │
   │                            │◄── GET /verify?token=xxx       │
   │                            ├─ DynamoDB: validate + delete   │
   │                            ├─ 302 → /auth/magic-callback    │
   │                            │    ?payload=BASE64&sig=HMAC ──►│
   │                            │                                ├─ Verify HMAC
   │                            │                                ├─ D1: create session
   │                            │                                ├─ Set-Cookie
   │                            │                                ├─ 302 → /admin

Enter fullscreen mode Exit fullscreen mode

The pieces, top to bottom:

AWS SAM packages the whole thing as one CloudFormation stack. sam deploy ships everything atomically.

DynamoDB holds tokens. Single table, partition key token (a UUID), with a TTL attribute set to now + 900s (15 minutes). Billing mode PAY_PER_REQUEST — at auth-flow volumes the bill is effectively zero.

One thing worth flagging clearly, because it's easy to miss: DynamoDB's TTL feature is opportunistic cleanup, not a security boundary. AWS's own docs say expired items are deleted "within a few days of their expiration time" — in practice that means the row can linger for up to ~48 hours past the TTL value. So if you let GetItem succeed and trust DynamoDB to have already swept the row away, you're effectively extending the token's lifetime from 15 minutes to up to ~48 hours.

The right shape is to enforce expiry at verify time and let TTL handle the eventual cleanup separately. Either an explicit check after GetItem:

const ttl = Number(result.Item.ttl?.N || '0');
if (ttl < Math.floor(Date.now() / 1000)) {
  return failureRedirect();
}

Enter fullscreen mode Exit fullscreen mode

…or, even cleaner, push the freshness check into the atomic DeleteItem with a ConditionExpression, so a stale token can never both pass verification and get consumed:

await dynamo.send(new DeleteItemCommand({
  TableName: process.env.TOKENS_TABLE!,
  Key: { token: { S: token } },
  ConditionExpression: '#ttl > :now',
  ExpressionAttributeNames: { '#ttl': 'ttl' },
  ExpressionAttributeValues: { ':now': { N: String(Math.floor(Date.now() / 1000)) } },
}));

Enter fullscreen mode Exit fullscreen mode

If the condition fails, the delete throws ConditionalCheckFailedException — catch it, redirect to the failure URL, done. TTL still does its background job; the row will eventually disappear. But the security of "15-minute magic link" is enforced by the verify handler, not by the cleanup mechanism.

This split — TTL for tidy housekeeping, conditional-write for real expiry — is the pattern I'd default to for any short-lived token table.

Lambda (Node.js 20.x, 128MB, 10s timeout) runs three routes:

  • POST /send — validate email, write token to DynamoDB, call ses:SendEmail
  • GET /verify — read token from DynamoDB, delete it, redirect to the Cloudflare Worker with an HMAC-signed payload
  • POST /send-email — generic email send for non-auth flows (welcome, billing, announcements); called by the CF Worker over HMAC

API Gateway HTTP API (v2) fronts the Lambda. Custom domain auth.toui.io, ACM cert, CORS limited to https://toui.io. Throttling: 10 req/s burst, 5 req/s sustained per route.

SES sends the actual email. Verified domain toui.io, custom MAIL FROM mail.toui.io, DKIM CNAMEs + SPF + DMARC all set during the cutover.

Secrets Manager holds HMAC_SECRET and FROM_EMAIL. Lambda reads on cold start and caches in a module-level variable, so warm invocations don't pay the API call.

The CF Worker side stays simple — it never touches DynamoDB. The only bridge between the AWS side and the Cloudflare side is the HMAC-signed redirect. AWS owns the token lifecycle; Cloudflare owns sessions. Neither code path needs cross-cloud credentials.

The cutover itself

Counter to how this story sounds, the migration wasn't dramatic. I had:

  • The design spec written that morning, after the math became clear
  • An existing Resend implementation to mirror for behavior (rate-limit, email body, error messages)
  • AWS SES already verified for toui.io (I'd done that the week before, "just in case")

By the afternoon the SAM stack was deployed, the Cloudflare Worker had been updated to call auth.toui.io instead of the old in-Worker Resend endpoint, and I'd manually sent test magic links from a few different email clients to make sure nothing landed in spam.

Total wall-clock time: about 6 hours. About 90 minutes of that was DNS propagation for the custom domain CNAME — the only step I couldn't speed up. The rest was code that I'd already mentally compiled by the time I started typing.

Surprises and the one thing I'd redo

SES out-of-sandbox: SES accounts start in sandbox mode (can only send to verified addresses). Production needs you to request out-of-sandbox via a support ticket, and AWS reviews it in ~24h. I'd done this previously for an unrelated project on the same account, so I was already approved in ap-northeast-1. If you're doing this fresh, file the ticket the day you create the account, not the day you need to ship.

Email warm-up: A new SES account starts in sandbox mode, capped at 200 emails per 24 hours and 1 email per second. Once you request and receive production access, your starting quota "varies based on your specific use case" (AWS docs language — in practice, a low starting limit that scales with reputation). For toui.io's pre-launch and early-launch traffic this was fine. If you're launching to thousands of users on day one, plan ahead with a dedicated IP and a warm-up schedule.

The one thing I'd redo: I shipped the Lambda's secrets reader as a per-invocation lookup before realizing how chatty that was. The fix is trivial — read on cold start, cache in a module-level variable so warm invocations skip the call — but the first deploy ate a few cents of Secrets Manager API calls before I noticed. AWS's own Lambda + Secrets Manager docs cover the pattern; follow it from the first commit, not the third.

Would I do it again?

Yes — but earlier. Sitting on Resend's free tier for the months before launch cost me nothing in money but cost me a sunk-cost-aware migration on the worst possible week. If I had projected my year-one volume the day I picked Resend, I'd have started on SES.

If you're prototyping and don't know your scale, Resend is still the right answer; the developer experience is genuinely better and the API is well-shaped. If you have a number — even a rough one — that crosses 10k emails/month in your projection, do the math early.

The AWS magic-link service is still the auth backbone of toui.io two months in. The SAM template, Lambda handlers, and email templates live in the aws/magic-link/ directory of the toui.io codebase. If anyone wants a sanitized stand-alone reference repo of just the SAM stack + Lambda code + DynamoDB schema, ping me and I'll cut one — it's small enough to be useful as a starting point for any side project that needs passwordless email auth.

If you've done the same migration in the opposite direction, or stuck with Resend at scale, I'd genuinely like to hear about it — hello@toui.io.


toui.io is a free URL shortener with permanent links, a free public REST API, OG/QR customization, and Telegram + LINE bots. The name is Taiwanese: to-ui — "where to?"