← Learning

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 chunk-boundary bufferingTwo network chunks split a token across the boundary; the reader buffers, splits on newlines, keeps the incomplete tail, and reassembles the complete line before parsing it.NETWORK CHUNK BOUNDARY — MID-TOKENchunk N…"content":"helchunk N+1lo"}\nbuffer += chunk · split on \n · keep the incomplete tailonly complete lines move on; the partial one waits for the next readcomplete line → JSON.parse → append token to the UI"hel" + "lo" → "hello"

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.