Zstem

Full Stack App

by Zstem · published 2026-07-16 · 0 forks

APIDatabaseDrawingEventsArchitectureDocsSequencefull-stackserverlessauth

React + Lambda + DynamoDB URL shortener

Open & fork this on Zstem →
Visitor / SPACloudFrontSPA AssetsAPI Gateway (HTTP API)Cognito User PoolRedirect Fn (hot path)Links API Fnlinks tableclick-events queueAnalytics FnFull Stack Appzstem.design

Architecture

Event flow

Sequence

Participants: Browser, CloudFront, API Gateway, Redirect Fn, links table, click-events (SQS), Analytics Fn, Destination site

API contract

Database

links
code7-char base62 (or user-chosen custom code); partition key
long_urlvalidated http(s) URL, max 2048 chars
domainshort domain this code lives on
owner_idCognito sub; GSI1 PK. Null for anonymous links
click_counttotal, incremented by the Analytics Fn (ADD)
created_atGSI1 SK — "my links, newest first"
expires_atepoch seconds; DynamoDB TTL attribute. Absent = never expires
clicks_daily
code
dayUTC day bucket, e.g. 2026-07-08
clicksADD-ed per analytics batch
referrersJSON map of top referrers -> count
countriesJSON map of ISO country -> count (from CloudFront-Viewer-Country)
domains
domaine.g. go.acme.com
owner_id
statuspending_dns | verified | failed
dns_targetCNAME target the customer must create
acm_cert_arnset once DNS validation completes
created_at

About this design

What this is

A serverless URL shortener with the shape of a real product: a hot-path redirect Lambda in front of DynamoDB, TTL-based link expiry, asynchronous click analytics over SQS, a Cognito-protected management API, and optional custom short domains. A React admin SPA (served from S3 via CloudFront) is where links get created; following a link needs no account and touches almost nothing.

The design optimises exactly one thing: GET /{code} must be one read and one 301. Everything else — counting, rollups, ownership, expiry — is arranged so it never adds latency to that path.

How it works

Redirect hot path — CloudFront forwards /{code} to API Gateway (no authorizer), the Redirect Fn does a single GetItem on the links table (PK = code), sends a link.clicked message to SQS without awaiting the result, and returns 301 Location: long_url. Missing codes return 404; TTL-expired ones 410.

Expiry — expires_at is the DynamoDB TTL attribute. Expired links are deleted by DynamoDB itself; the Redirect Fn also checks the timestamp so links that expired seconds ago (TTL deletion lags) still return 410.

Analytics — the Analytics Fn consumes SQS batches (up to 100 clicks), aggregates per code per UTC day, and issues a few UpdateItem ADDs: total clickcount on the link plus a clicksdaily rollup row with referrer/country maps. Stats queries never scan raw events. A DLQ catches poison messages.

Management — /links and /domains sit behind a Cognito JWT authorizer. Creating a link is a conditional Put (attributenotexists(code)), so custom-code collisions surface as 409 instead of silent overwrites. GSI1 (ownerid, createdat) lists "my links" newest first.

Custom domains — a domains row tracks DNS verification and the ACM cert; verified domains attach to CloudFront as aliases, and codes carry a domain attribute.

The sequence diagram traces the redirect hot path end to end, including the async aggregation leg; the event flow shows the link.clicked contract.

How to extend

Shave the hot path further: move redirects to CloudFront Functions/Lambda@Edge with DynamoDB global tables, or add short-TTL CloudFront caching per code.

Richer analytics: tee link.clicked to Kinesis Firehose → S3 and query raw events with Athena; add bot filtering before counting.

Product features: QR codes per link, password-protected links, per-owner rate limits (WAF), link preview/unfurl metadata.

Abuse controls: check long_url against Safe Browsing on create, and add a disabled flag the Redirect Fn honours.

Open & fork this on Zstem →