---
title: "Web applications, technically — Thoughtful Robots"
description: "How Thoughtful Robots engineers web applications: framework selection, design systems, typed API contracts, rendering and BFF boundaries, operational workflows, testing, responsive media, and production delivery."
source: "https://thoughtfulrobots.ai/web-applications-tech"
---

# The interface is where every system promise becomes real.

A frontend is not a coat of paint over the system underneath. It is where API contracts, state, permissions, latency, media and human judgement have to resolve into one predictable experience. We choose the framework after those constraints are known, then engineer every boundary the person using it will eventually meet — including the failure paths.

## What we build through.

These are engineering responsibilities before they are framework choices. The tools are where our work has landed in practice; the linked sections explain the contract each layer has to keep.

| Layer | What we use |
| --- | --- |
| [Framework & runtime](#framework-runtime) | SvelteKit Next.js React |
| [Design system](#design-system) | Design tokens shadcn/ui Bits UI Radix UI Tailwind CSS CVA |
| [Contracts & state](#contracts-state) | OpenAPI Orval TanStack Query TanStack Form Zod |
| [Rendering & BFF](#rendering-bff) | Prerendering SSR Server Components Route handlers |
| [Operational UI](#operational-ui) | Tables Wizards Optimistic updates Background jobs |
| [Tests & quality gates](#testing) | Vitest Testing Library MSW Playwright Axe |
| [Responsive media](#responsive-media) | Responsive images HLS tus Streaming UI WebView bridges |
| [Production](#production) | Lighthouse PostHog CSP Cloudflare AWS CI gates |

## Choose the application framework *after the constraints are known.*

We do not choose a frontend framework from a preference list. We choose it from the application’s rendering profile, deployment target, interaction density, existing team and the systems it has to sit beside. The useful decision is not React versus Svelte in the abstract. It is which runtime leaves the product with the fewest accidental seams — between server and browser, content and interaction, the API contract and the component consuming it.

- Which routes can be static, which need server rendering, and which are interaction-first

- Whether the application runs at the edge, on a Node server, or inside an existing platform

- How tightly the frontend should share types and runtime code with its backend

- Whether content editing, commerce, streaming or an embedded WebView changes the shape

- What the team already knows and will still be able to maintain after handover

- Which ecosystem capabilities are genuinely needed rather than merely available

- What the framework adds to the browser bundle, build pipeline and operational surface

- SvelteKit for lean products, content-heavy applications and edge-ready delivery Svelte 5 SvelteKit Cloudflare

- Next.js where React, Payload, Server Components or streaming interfaces earn their weight React App Router Payload

- Incremental adoption when a sound application already exists Inherited codebase Migration in slices

SvelteKit and Next.js are choices, not boundaries. We can dig into any serious frontend framework, inherit one already in production, and become useful without first replacing it. The decision checklist travels; the framework name does not.

## Build the shared language *before the page count grows.*

A design system is not a gallery of buttons. It is the contract between design decisions and product code: tokens that carry intent, accessible primitives that own interaction, domain components that understand the product, and compositions that let a new screen feel native without copying the old one. We keep those layers distinct so a visual change lands once, while business behaviour stays close to the workflow that owns it.

- Semantic tokens for colour, type, spacing, motion and elevation CSS custom properties Design tokens

- Accessible primitives for focus, keyboard control, overlays and form semantics shadcn/ui Bits UI Radix UI

- Variants expressed as typed component APIs rather than class-name conventions CVA Tailwind Variants

- Domain components for the repeated product decisions Typed props Composition

- Loading, empty, error, disabled and destructive states designed with the default state State matrix

- Responsive behaviour owned by the component that changes Container queries Mobile first

Tokens carry the visual decisions. Primitives carry interaction and accessibility. Domain components carry the language of the product — a review item, a delivery slot, a source card — and pages arrange them for one task. Mixing those responsibilities is how a component library becomes either too generic to help or too specific to reuse.

We use local primitives first and introduce a shared abstraction only when repetition proves it. That keeps the system coherent without turning every one-off decision into a permanent API.

## Make the data contract *hard to misuse.*

The frontend should not discover the API by failing against it. We generate typed clients from the schema, keep server data in a query layer with explicit cache and invalidation rules, and separate it from URL state, local interaction state and durable application state. That separation is what lets a screen remain predictable when requests overlap, tabs are restored, permissions change or an optimistic update is rejected.

- Generate request, response and error types from the API contract OpenAPI Orval

- Give every server read a stable key, lifetime and invalidation path TanStack Query Query keys

- Cancel stale requests when navigation or filters move on AbortSignal

- Keep filters, pagination and shareable selections in the URL URL state

- Model complex forms with typed validation and field-level errors TanStack Form Zod

- Use optimistic updates only when the reconciliation path is defined Optimistic UI Rollback

- Preserve one error envelope from transport to the component rendering it Structured errors

Generated types do not replace product judgement. They remove contract drift so that judgement can stay focused on what the interface should do when the data is late, partial, forbidden or wrong.

## Choose the rendering boundary *route by route.*

Static, server-rendered and client-rendered are not competing application architectures. They are delivery modes we assign to routes. A marketing page can be generated once, an authenticated workspace can assemble its first response on the server, and the interaction after hydration can stay entirely in the browser. The backend-for-frontend sits where the browser should not: holding credentials, translating sessions, aggregating calls and enforcing cache boundaries.

- Generate stable content and CMS routes at build time Prerendering Static output

- Render authenticated or SEO-sensitive first responses on the server SSR Server Components

- Hydrate only the interaction that needs browser state Client components Progressive enhancement

- Stream slow results without blocking the whole response Streaming Suspense

- Session cookies, OAuth and OTP exchanges that must not expose private credentials

- Permission-aware aggregation across APIs the browser should not call directly

- Turnstile and other server-verified anti-abuse controls

- Media signing, upload completion and safe proxying

- Per-user cache control and revalidation headers

- Protocol translation where the product needs a smaller, stable surface

A BFF is a trust boundary, not a second backend by habit. If it only forwards every request unchanged, it has added a network hop without earning one.

## Design for the work *including when it goes wrong.*

Internal tools are where edge cases become the normal workload. The interface has to keep context across filters, edits and long-running operations; make partial failure legible; and let a person understand what will change before a consequential action lands. We model these workflows explicitly rather than stretching CRUD screens until they almost fit.

- Data tables with URL-backed filters, sorting and pagination TanStack Table Saved views

- Multi-step workflows that can be resumed without re-entering known data Wizards Draft state

- Bulk operations that preview scope before mutation Dry runs Selection models

- Long-running imports, exports and processing shown as durable jobs Background jobs Polling

- Review queues that preserve provenance and the reason something needs attention Review state Audit trail

- Partial failures reported per item, with a safe retry path Idempotency Retry

- Empty, stale, forbidden and degraded states treated as product states State matrix

The happy path is usually the shortest part of an operational application. The quality of the product shows up in what it lets a person understand, recover and safely try again.

## Test each boundary *with the cheapest useful tool.*

A browser test is too expensive for every branch of logic, and a unit test cannot tell us whether a keyboard user can finish the workflow. We layer tests by responsibility, then make the API simulation work on both sides of the rendering boundary. That last part matters: mocking browser fetch while an SSR loader still reaches a real backend is not an isolated test suite.

- Pure rules, transformations and state machines Vitest

- Components against a real DOM and browser events Testing Library Vitest Browser

- Deterministic APIs across browser fetch, SSR loaders and the BFF MSW Server handlers

- Money paths across desktop and mobile browser engines Playwright Chromium WebKit

- New serious or critical accessibility violations

- A regression in the critical user journeys

- An API contract that no longer generates or type-checks

- A performance or bundle budget crossing its reviewed limit

- Unexpected console, network or hydration errors

- A visual change outside the approved component or page scope

We keep traces, screenshots and failure artifacts because a red gate without inspectable evidence only moves debugging from CI to somebody’s laptop.

## Treat the viewport and the network *as runtime inputs.*

Responsive engineering is not shrinking the desktop layout. It is choosing what remains visible, what changes interaction model, and what the device should not download at all. Images, video, uploads and streamed interfaces each need their own delivery and recovery contract; otherwise the screen works only on the connection and device it was built on.

- Components own the breakpoint where their interaction changes Container queries Media queries

- Images carry dimensions, responsive sources and modern formats srcset CDN transforms Placeholders

- Large uploads use signed destinations, checksums and resumable transfer Signed URLs SparkMD5 tus

- Adaptive video playback follows connection and device capability HLS.js Media sessions

- Structured UI streams recover from interruption without losing the resolved state ReadableStream Event protocol

- Animation responds to reduced motion and never blocks the task Rive GSAP prefers-reduced-motion

- The same web surface can run inside an explicit native bridge WebView bridge Shared tokens

The smallest screen is not a lesser product, and the fastest connection is not the baseline. Both are constraints the component should be able to explain in code.

## Operate the frontend *after it ships.*

A frontend has production infrastructure even when it deploys as static files. It has secrets and public configuration, browser caches, third-party scripts, security headers, release artifacts and errors that only exist on a customer’s device. We instrument that surface, set budgets before it slows down, and make each deployment small enough to inspect and quick enough to reverse.

- Authentication and authorization enforced at server boundaries HttpOnly cookies Scoped roles

- Browser capabilities restricted to what the application needs CSP Security headers

- Public configuration separated from server-only secrets Environment contract

- Abuse constrained before expensive work begins Rate limits Turnstile

- Core Web Vitals and bundle size tracked across releases Lighthouse CI Bundle analysis

- Client and server exceptions captured with safe, redacted context PostHog Structured logs

- Product events defined as schemas rather than ad hoc strings Event contracts

- Feature changes isolated behind observable rollout controls Feature flags

We deploy static and edge-ready SvelteKit applications to Cloudflare, and standalone Next.js applications where a Node runtime, Payload or long-running server work belongs. Preview environments carry the same configuration contract as production, smoke tests run against the deployed URL, and rollback stays a release operation rather than a rebuild under pressure.

Where a platform team already owns deployment, we fit its pipeline and produce the evidence it needs. The application should not require a special lane just because its frontend framework has an opinion.

The frontend is not finished when the build passes. It is finished when a failed release is visible, attributable and reversible.

## We operate the interfaces after handover.

These are not preferences assembled for a stack diagram. They come from running public products, content platforms, data-heavy operational consoles, CMS editors and streamed AI workspaces — across edge deployments, Node applications, browsers and embedded mobile surfaces.

Owning those systems after launch is why the page is opinionated about generated contracts, cache boundaries, resumable media, mobile browser tests, security headers and failure evidence. They are the things that matter when a browser, API or deployment does something the happy path did not predict.

## An AI-native team, with a factory behind it.

Team Foundry is our software factory — what the team uses day in, day out to deliver projects. It accelerates the work and validates it: every change arrives with the checks it passed, the session that produced it, and a person accountable for it.
