← All work

TYVRAA

A brand site that loads nothing until you ask it to

Role
Sole developer - design system, build, integrations, third-party audit
Period
2026
Status
In production
Stack
Next.js 16 · React 19 · TypeScript · Tailwind CSS 4 · Vitest
0
requests before consent
1
dependency I refused

A German-language site for a Rocket League player, streamer and coach - quiet typography, generous whitespace, controlled copper accents, and not a single external connection opened before a visitor asks for one.

01Consent before connection

The Twitch player is not loaded when the page loads. Only after an explicit "agree and load the player" click is an iframe created, muted and without autoplay, and the connection can be closed again afterwards. The parent domain Twitch requires comes from the current hostname rather than a hard-coded list, so localhost and production behave identically.

Below 500 pixels no player is loaded at all - an external card is shown instead - and if Twitch is unreachable the outbound link still works. Fonts and images are served from the site itself, so a visitor who never clicks anything never touches another company’s infrastructure.

02The booking system that did not ship

Coaching slots were meant to run through a separately hosted instance of an open-source booking product, with the site holding no slot database of its own and showing no invented availability.

Reading its source before deploying it turned up confirmed race conditions, webhook handling faults and payment validation gaps. It is therefore not approved for live payments - staging only, or an audited and pinned fork - and until a booking service is signed off, the site keeps offering the contact route instead. The findings, the staging checklist and the privacy and licence review are written up in the repository rather than left as a memory.

03The contact form, and one trap in it

Validation runs in the browser and again in the route handler, with a honeypot field and the transport separated from validation and interface, so switching provider means replacing one function.

Mail goes out through the same provider that hosts the site, which removes a vendor instead of adding one. Without configuration the endpoint answers 503 and the interface states plainly that nothing was sent. The sender address sits in Reply-To so answers reach the visitor while the sending domain keeps its DMARC alignment, and the API token carries exactly one permission.

The trap: the provider answers HTTP 200 even for a message it has permanently rejected. The transport therefore also checks the success field and the permanent-bounce list, because a form that reports success on a bounced message is worse than one that reports an error.

04Decisions worth explaining

Locking the palette against extensions

The design is deliberately light with dark contrast sections and copper accents, and Dark Reader turns that into mud. A darkreader lock plus an explicit colour-scheme declaration keeps it as designed. If a dark version is ever wanted, the way to get one is to build it - not to let an extension invent it.

Documenting the build workarounds

The build runs on webpack instead of Turbopack, and type checking goes through the TypeScript API instead of a separate CLI process, because the defaults did not terminate reliably in the container this deploys from. Both are written down with their reasons, so the next person does not undo them and rediscover the same afternoon.

Questions about this project?

I am happy to walk through the architecture, the parts that did not work, or the code itself.