Engineering
System Design, Live
Real numbers from the running server, not a diagram from a slide deck. Opening this page wakes the API if it's asleep.
Right Now
What the Server Is Doing
Server sleeps when idle and wakes on your first request
Awake For
—
Since this instance last woke
Requests Served
—
Since waking
Cache Hit Ratio
—
Active Links
—
Database Size
—
Bloom Filter
—
| Route | Calls | p50 | p95 | p99 |
|---|---|---|---|---|
| Loading… | ||||
| Structure | Live Value |
|---|---|
| LRU cache | — |
| Bloom filter | — |
| Rate limiter | — |
| Code keyspace | — |
| HyperLogLog | — |
| Last cleanup | — |
| Provider | Role | Model | Status |
|---|---|---|---|
| Jev by TypeSafe | Risk decisions, categories, smart defaults and alias ranking | — | — |
Request X-Ray
What happened inside the server, span by span
Make a request, like shortening a link, and its internal timeline appears here.
Chaos Mode
Break It on Purpose
Inject real failures into your own request and watch the retries, timeouts and fallbacks happen live. Nothing is faked: the same code paths run in production.
Design an Experiment
Faults apply only to your request, not to anyone else using the site.
Nothing Broken Yet
Pick a failure and run the experiment. You'll see how the system detects it, what it falls back to, and how long it costs, span by span.
Request Lifecycle
Two Paths Through the System
Creating a link is write-heavy and careful. Opening one is read-heavy and fast.
Create a Link
- 1Edge: request ID, origin check
- 2Rate limiter: create bucket
- 3Idempotency key check
- 4Validate and normalise URL
- 5Heuristics → safe fetch → Jev risk decision
- 6Bloom + database uniqueness check
- 7Encrypt with AES-256-GCM
- 8Insert, add to Bloom filter, return claim code
Open a Link
- 1Next.js route handler forwards the visitor
- 2Rate limiter: resolve bucket
- 3Bloom filter: definitely absent → 404
- 4LRU cache → database on miss
- 5Lazy expiry check
- 6Rules: lock, password, warning, confirmation
- 7Atomic click update with limit check
- 8302 redirect, analytics written after response
Trade-Offs
Design Decisions
Every choice has a cost. Here's what we picked, why, and what we gave up.
| Decision | Why | Trade-Off |
|---|---|---|
| SQLite with WAL on a Railway volume | One instance, modest write rate, zero network hops to the database. Separate reader and writer connections give snapshot reads. | Can't scale writes horizontally. Moving to Postgres would be the step for multiple instances. |
| In-process LRU cache instead of Redis | A single instance can serve hot links from memory with no extra service to run or pay for while sleeping. | Cache is empty after every wake. Acceptable because cold reads are still a single indexed query. |
| Bloom filter in front of the database | Random or mistyped codes are rejected in microseconds without touching SQLite, which also blunts enumeration attempts. | Can't delete entries, so it's rebuilt during each cleanup sweep. |
| No background timers | Railway sleeps idle services. Any timer or poller would keep it awake and cost money. | Cleanup runs lazily on reads and piggybacks on requests, so expired rows can linger until the next visit. |
| AES-256-GCM with the code as associated data | Destinations can contain tokens. Binding ciphertext to its code means rows can't be swapped between links. | Search by destination isn't possible in SQL; only the domain is stored in plain text. |
| HyperLogLog for unique visitors | Counts uniques in 4 KB per link with ±1.6% error, without storing who visited. | Estimates, not exact numbers. Small counts use linear counting for accuracy. |
| One AI provider: Jev for every decision | Typed answers with calibrated confidence make a better gate than free text. Jev decides risk, category, smart defaults and alias ranking; summaries come from the page itself. | No generated prose, so a summary is only as good as the page's own description. |
| Workspace keys instead of accounts | No passwords to leak or reset. A random browser key sent as a header also makes the API immune to CSRF. | Losing the key loses access, so Settings offers copy, switch and rotate, and claim codes move single links. |
| Server-side redirects through Next.js | Short links stay on the main domain, the API stays private behind the site, and redirects set no cookies on visitors. | One extra hop from the edge to the API for each redirect. |
Failure-First
What Happens When Things Break
Designed for the unhappy path first.
| Failure | How It's Detected | What Happens |
|---|---|---|
| AI provider slow or down | 7 second budget, typed upstream errors | Heuristic verdict is used; the report shows which checker failed |
| Server asleep | Request takes longer than 1.5 seconds | UI says the server is waking; safe requests retry with backoff |
| Double-submitted form | Idempotency-Key header | Second request replays the first result instead of creating a duplicate |
| Two people use the last open | Conditional UPDATE … RETURNING | Exactly one succeeds; the other sees a used-up page |
| Chat app unfurls a burn-after-read link | Bot user agents and a human confirmation step | Previews never consume opens |
| A destination turns malicious later | Verdicts older than 6 hours are re-checked after an open | Later visitors see a warning; the owner sees what changed |
| Client floods the API | Token bucket per client and action | 429 with Retry-After and a friendly message |
| Destination points to an internal network | DNS resolved and checked before every fetch and redirect hop | Rejected at creation and never fetched |
| Unhandled exception | Global handler with request ID | Safe message to the user, full trace in structured logs |