Behind the Scenes
How Redline is actually built
Not a marketing page — an honest walkthrough of the architecture, the API, the database, and how the security model actually works.
Overview
Four tools, one job.
Redline is a handful of free, no-signup developer tools sharing one Next.js app: an API Tester for firing one-off HTTP requests, a Webhook Debugger for watching real webhook traffic land in a disposable inbox, a JWT Decoder, and a Timestamp Converter. None of them require an account — there's no auth system, no user table. Access to a webhook inbox is simply knowing its (unguessable) URL; the JWT Decoder and Timestamp Converter don't touch the server at all.
The app was designed with an optional-module architecture from the start — pieces like sign-in or analytics can be switched on later without dragging in code or dependencies for the ones nobody's using. Right now, the only one turned on is the database, because the Webhook Debugger needs somewhere durable to keep captured requests.
Tech stack
What's actually running.
| Layer | Choice | Why |
|---|---|---|
Next.js 16 | React 19, App Router, Turbopack | Server + Client Components, one deploy |
Hono | Typed API router | Mounted under /api via a single catch-all route |
Supabase | Managed Postgres | Persists webhook inbox data across requests |
Tailwind v4 | CSS-first config | Entire theme lives in one CSS file, no separate config |
shadcn/ui + Base UI | Copied-in components | Editable source, not an opaque dependency |
Playwright | End-to-end tests | Runs against a real production build — see Testing |
Repository map
Where things live.
app/
page.tsx # landing page
tools/page.tsx # hub page — a card per tool
tester/page.tsx # API Tester
webhooks/page.tsx # create a new inbox
webhooks/[id]/page.tsx # a single inbox
jwt/page.tsx # JWT Decoder (fully client-side)
timestamp/page.tsx # Timestamp Converter (fully client-side)
api/[[...route]]/route.ts # hands every /api/* request to Hono
server/
hono/app.ts # composes all API routes
hono/routes/ # proxy.ts, webhooks.ts, hook.ts
hono/lib/ssrf-guard.ts # blocks the proxy from hitting private IPs
db/admin.ts # the only client allowed to touch the database
components/
ui/ # shadcn primitives
sections/ # landing-page sections
tester/, webhooks/, jwt/, timestamp/ # each tool's own pieces
lib/
types.ts # shared request/response contracts
relative-time.ts # bidirectional "in 3 hours" / "2 days ago"Pages & routes
Every route, and what owns it.
| Route | Purpose |
|---|---|
/ | Landing page |
/tools | Hub page — a card for each tool |
/tester | API Tester |
/webhooks | Create a new webhook inbox |
/webhooks/:id | A single inbox — ingest URL + live capture |
/jwt | JWT Decoder — fully client-side, nothing sent to a server |
/timestamp | Timestamp Converter — also fully client-side |
/about | Product story & principles |
/contact | Ways to reach us |
/privacy, /terms, /disclaimer, /cookies, /security | Legal pages |
/api/* | Every backend endpoint — see API reference |
How the API Tester works
The round trip, from click to response panel.
- You fill in method, URL, headers, and body in the browser, then hit Send.
- Your browser posts that as JSON to
/api/proxy— never straight to the target API. That's what lets Redline bypass CORS entirely: the request happens server-side, not in your browser. - The server validates the target URL (see Security model), then makes the real request with a 20-second timeout.
- Status, headers, body, timing, and size come back as one JSON object and render in the response panel, color-coded by status class.
- The sent request is saved to your browser's
localStorage, capped at 15 — the only place tester history lives. Redline's server never stores your requests or responses.
How the Webhook Debugger works
The part that actually needs a database.
- Creating an inbox inserts one row into a
webhook_binstable and returns its random ID. - You get an ingest URL —
/api/hook/:id— and hand it to whatever should send you webhooks. - Any request to that URL, any method, any path, is captured in full: method, path, query string, every header, the raw body, and the sender's IP — stored against that inbox's ID. There's no auth on this endpoint by design; the inbox ID is the access control.
- The inbox page polls for new requests every few seconds and shows them newest-first, with a detail view for headers, query params, and body.
- Deleting an inbox cascades to every request captured in it, at the database level.
API reference
Everything under /api.
| Method | Path | Does |
|---|---|---|
| GET | /api/health | Liveness check |
| POST | /api/proxy | Performs the API Tester's request, SSRF-guarded |
| POST | /api/webhooks | Creates an inbox |
| GET | /api/webhooks/:id | Inbox metadata, or 404 |
| GET | /api/webhooks/:id/requests | Up to 200 captured requests, newest first |
| DELETE | /api/webhooks/:id/requests | Clears captured requests |
| DELETE | /api/webhooks/:id | Deletes the inbox |
| ALL | /api/hook/:id | The public ingest endpoint |
Database & row-level security
Two tables, locked down by default.
webhook_bins
id uuid primary key
name text
created_at, last_active_at timestamptz
webhook_requests
id uuid primary key
bin_id uuid references webhook_bins(id) on delete cascade
method text, path text, query jsonb, headers jsonb
body text, content_type text, ip text
received_at timestamptzBoth tables have row-level security enabled with zero policies defined. That's deliberate: the public/anonymous database credential has no access to this data at all, full stop. Every read and write goes through a single privileged server-side client that bypasses RLS — there's only one trusted caller, so there was never a need for granular policies.
Security model
The mechanisms actually doing the work.
SSRF guard on the proxy
Every API Tester target is checked before the request fires: localhost, private and link-local IPs (including the cloud metadata address), and any hostname that resolves to one, are rejected — checked via a real DNS lookup, not just a string match.
The database credential never reaches your browser
It's excluded from client bundles at build time, not just by convention — importing it from browser-facing code fails the build outright.
An inbox's URL is its access control
No accounts means no per-user permission check — an inbox is protected the way a well-known tool like webhook.site protects one: a long, random, unguessable ID. Whoever has the link can see it.
Captured data can be sensitive
A webhook payload is stored exactly as received, including any auth headers a third party sent. Clear or delete an inbox once you're done with it — see the Security Policy.
Design system
One theme file, no separate config.
Three full color scales — scarlet, orange, and a warm neutral grey — are mapped onto semantic tokens (primary, background, destructive, and so on), plus two custom tokens for status-code coloring across both tools.
scarlet-rush-500
scarlet-rush-600
orange-500
shadow-grey-950
shadow-grey-800
shadow-grey-50
Type is three faces: a sans for body and UI, a monospace face used heavily for anything technical (URLs, headers, JSON), and a display serif reserved for statement headings rather than every heading. Every component under components/ui/ is source in the repo, not an installed package — added via a CLI, then owned and editable directly.
Testing
37 end-to-end tests, run against a real production build.
Every page and both tools are covered by Playwright tests that run against an actual production build — not mocked, not a dev server. That includes a realproxied HTTP request in the API Tester tests, and a full create → capture → inspect → delete round trip against the real database for the Webhook Debugger. The database-dependent tests check that real credentials are configured before running, and skip cleanly with a clear reason if they aren't, rather than reporting a false failure.