The frontend
Streaming, the honest way
Next.js App Router, a design-token system, and one SSE reader doing the only genuinely tricky thing on the client: turning a byte stream into tokens without corrupting one.
How it works
Route groups, tokens, and a careful reader
The App Router is split into route groups — (marketing), (auth), (dashboard), (docs) — each with its own layout, one codebase. A small design-token system (the canvas, the brand amber, the four crew colors) themes every surface, this page included.
The crew chat streams over SSE, but not via EventSource — it’s an authenticated POST, so the client reads the stream by hand with fetch() + response.body.getReader() + a TextDecoder.
The one careful bit: a network chunk can end mid-line — even mid-token. So the reader appends each chunk to a buffer, splits on newlines, and keeps the last (possibly-incomplete) line in the buffer for next time. Only complete lines get JSON.parsed, and decoding with { stream: true } means a multi-byte character split across chunks survives too.
SSE is just lines of data:. The hard part is that a network chunk can end mid-token. The reader keeps the incomplete tail in a buffer and only parses complete lines — so "hel" + "lo" becomes "hello", not a parse error.
Each delta is appended to the current message in state and rendered immediately — no spinner, the text just grows — and the message is marked streaming until the done event. An AbortController aborts any in-flight stream before a new one starts, which also powers the stop button.
Decisions & trade-offs
What each choice cost
fetch + getReader, not EventSource
auth header + POST body
EventSource can’t send an Authorization header or a POST body. The crew endpoint needs both, so I read the stream by hand. More code — but it’s the only way to stream an authenticated POST.
Buffer the boundary
one line · no corruption
The reader keeps the incomplete tail between reads. Skip that and the first token unlucky enough to straddle a chunk boundary throws a parse error mid-stream. It’s the single most important line in the file.
Optimistic token append
feels instant · finalize on done
Each delta is appended to the current message and rendered immediately; the message stays marked streaming until the done event. No loader — the text just grows.
Design tokens, not ad-hoc colors
one source themes everything
Canvas, brand, the crew colors live as tokens. Change one value and the app, the marketing site, and these diagrams move together — which is exactly what bit me when a token edit needed a dev-server restart to take.
The learning
Robust SSE parsing was never about the protocol — it was about not trusting where one network chunk ends and the next begins.
What I'd tell you
Three things I'd say out loud
The protocol is trivial; the boundary is everything. SSE is just data: lines. Every bug I hit was a chunk boundary — a token split in two, a multi-byte character halved. The fix is line-delimited framing 101: buffer, split on the delimiter, keep the remainder. Every streaming protocol re-teaches you this.
Decode as a stream, not a string. decode(value) per chunk silently corrupts any UTF-8 character spanning two chunks. { stream: true } is the difference between “works in my tests” and “works when an emoji lands on a boundary.”
Abort the old stream before the new one. Without the AbortController, a fast second message leaves the first stream writing into state behind it. Cleaning up the in-flight request isn’t politeness — it’s correctness.
The frontend is mostly unremarkable React, and that’s the point — the crew, not the client, is where the intelligence lives. The one part worth studying is twenty lines of stream-reading, and what they teach isn’t about SSE: the network hands you bytes, not messages, and the boundary between them is your problem.
A personal experiment — cost-capped and opinionated. The views here are my own, not my employer’s, and this is a learning writeup, not an enterprise blueprint.