# Hiten Patel — full case studies Senior frontend engineer and architect, Reading (UK). hello@hiten.dev / +44 7827 443 288. Homepage: https://hiten.dev/ This file concatenates the full text of the five published case studies for AI assistants that want the complete bodies rather than the section headlines. --- ## 1. Enterprise SaaS learning platform — schema-first codegen on AEM Source: https://hiten.dev/work/aem-codegen-platform/ Published: 13 June 2026 (updated 25 June 2026) Role: Lead frontend / architect (via 3|Share consultancy) Stage: 2025 to present, built and delivered to the client, launch pending SSO Scale: ~25 production AEM components (40+ including the Lit atoms they compose from), generated from one source Stack: Vite 7, Lit 3, TypeScript 5.9 (strict), Storybook 10, Lightning CSS, Open Props, Zod 4, AEM Cloud, HTL, Sling Models, GitLab CI, Adobe Cloud Manager, axe-core, Playwright ### One YAML file per component. Everything else generated. The client's enterprise SaaS learning platform, which I rebuilt as a modern AEM Cloud component system. The headline is a schema-first code-generation pipeline I designed: a single YAML definition per component becomes the TypeScript types, the Lit scaffolds, the AEM author dialogs and the HTL, so the same component is never hand-written in four slightly different places. ### The problem with hand-built AEM components An AEM component is really four artefacts that have to agree with each other: a Java/HTL template that renders server-side, an XML dialog that tells authors what fields exist, a TypeScript model for any client behaviour, and the CSS. Keep them in sync by hand across dozens of components and they drift: a field gets renamed in the dialog but not the model, a type goes stale, a new author option never reaches the template. Multiply that by a team and a multi-month build and the drift is constant. ### The pipeline I made the YAML schema the single source of truth. Each component has one `component.yaml`; `npm run generate` emits the TypeScript interfaces, the Lit scaffold and the CSS scaffold, and `generate:aem` emits the dialog XML, component XML, EditConfig and HTL. The YAML became the contract between frontend and backend: change a field once and every downstream artefact follows. A new component is a YAML file and a generate command, not a half-day of boilerplate spread across four file types. Junior engineers and AI assistants extend the platform by editing a schema, not by reverse-engineering conventions; the generator enforces them. ### The hardest bug Composite multifields (nested repeatable groups of fields) were generating dialog XML that AEM accepted but silently rendered wrong when a `nestedType` reference was missing. Silent failure is the worst kind in a generator, because the output looks plausible. The fix was to make the generator throw on any missing `nestedType` reference rather than emit almost-correct XML. Correctness moved left, to generate time, where a developer sees it immediately. ### Performance by construction AEM renders HTML server-side, so the page does not need a client framework hydrating the whole tree, only islands of interactivity do. I chose Lit 3 over React for exactly that reason and built progressive hydration with several strategies (eager, idle, visible, interaction) so each component hydrates only when it needs to. Design tokens live in cascade layers with a no-`!important` rule at org scale, and Vite handles manual-chunk splitting. ### Real Java, not just frontend The platform is not frontend-only. I wrote around 30 Sling Models, an OSGi CourseService, and a Core WCM Accordion extension: the backend the generated components bind to. Coming up through AEM means I can drop into Java when a feature genuinely needs it, rather than throwing it over a wall. ### Quality and release Every component carries axe-core checks and Storybook visual-regression coverage; the suite runs JUnit + Selenium with Allure reports on GitLab Pages. Releases are semver with conventional commits and zero-touch deploys to Adobe Cloud Manager. AI-agent tooling (AGENTS.md / CLAUDE.md plus 17 domain skill files) lets autonomous sessions extend the codebase along the same conventions the generator enforces. ### Shape of the contribution I authored the architecture and the overwhelming majority of the repository: roughly 292 of 294 commits, about 116,000 lines and 36 PRs in the first five weeks, then set the conventions that let other engineers extend it safely. --- ## 2. Global pharmaceutical client — search, CSP and accessibility at scale Source: https://hiten.dev/work/regulated-healthcare/ Published: 13 June 2026 (updated 25 June 2026) Role: Senior frontend, primary hands-on engineer (via 3|Share consultancy) Stage: 2021 to 2025 Scale: 3 regulated healthcare platforms; ~400 tickets on the patient platform alone; ~1,600 commits over 4 years on the HCP medical-information portal; 407 commits on the adverse-event reporting system Stack: Vue 2 / Vue 3, Pinia, Vuetify 3, Lit atoms, TypeScript, AEM, Solr, AWS DynamoDB, AWS Cognito, OneTrust, WCAG 2.2 AA, NVDA / VoiceOver, axe-core, GitLab CI ### Four years, three regulated platforms, the features other people route around. Across four years at a global pharmaceutical client (via 3|Share consultancy) I delivered the complex frontend on three regulated healthcare platforms built on AEM and Vue: a patient-facing clinical-trial finder; an HCP medical-information portal; and an adverse-event reporting system. Delivery-heavy contracting in established, compliance-bound codebases, where the hard part is the constraints, not the framework. ### Search across three contexts that didn't want to agree On the HCP medical-information portal I owned search and discovery: a Solr-backed single-page search with faceted filtering across publications, clinical trials, congress programmes and medical information. The catch was that three separate search contexts shared one navigation UI, each with its own result counts and parameters. I reconciled them through a redesigned Vuex state with deep-linkable parameters, so a filtered search is a shareable URL and the counts stay honest as a user moves between contexts. ### A strict CSP that broke Vue's compiler The adverse-event reporting system ran under an enterprise Content Security Policy that banned `eval` and `new Function`, which is exactly how Vue's default build compiles templates at runtime. The app simply would not run. I refactored it to the runtime-only build with regex-based label handling and webpack code-splitting, and migrated the whole system from Vue 2 to Vue 3 in the same effort, with zero functionality lost. This is the kind of constraint that does not show up in a job spec but decides whether the thing ships. ### An accessible mirror of a component I couldn't replace The patient platform used a third-party Select2 control that announced blank or duplicated entries in NVDA, a real barrier on a patient-facing healthcare product, and one I could not fix by swapping the library out. So I built a parallel, ARIA-correct listbox that mirrored the Select2 state plus a live region to announce changes properly. The visible control stayed; the accessibility tree became correct underneath it. ### Authenticated bookmarking on a legacy store Patients needed to save clinical trials across sessions. I wired the platform's legacy Vuex store to AWS DynamoDB and Cognito, with event-driven state sync so a bookmark made on one page is consistent everywhere without a full reload. Dual-branded across the parent and a US partner brand, and multi-country, so the same flow had to behave under several configurations. ### oneDataLayer: analytics without per-component JavaScript The global pharmaceutical client's global clinical-trials platform needed consistent analytics across page views, clicks, scroll depth, search and OneTrust consent. Rather than scatter tracking through every component, I built oneDataLayer: a single TypeScript class that AEM components opt into via one data attribute. No per-component JS, a flat client footprint at scale, and one place to reason about consent. It is my go-to architecture-decision story for a reason: it traded a thousand small integrations for one contract. ### Accessibility you can defend to a regulator I led WCAG remediation across the three platforms ahead of the June 2025 EU Accessibility Act deadline, as the primary hands-on engineer working alongside an external accessibility auditor. That meant reusable modal focus-traps, a WAI-ARIA combobox built from scratch with dual live regions and managed `aria-activedescendant`, NVDA-specific fixes, and skip links, validated against real screen readers, not just lint rules. It is one credential among several, but it is the one with a legal deadline attached. --- ## 3. Slipstream — hand-built local-first sync engine Source: https://hiten.dev/work/slipstream/ Published: 4 July 2026 Role: Solo build (protocol, engine, server, UI) Stage: 2026, live demo at https://tracker.hiten.dev/ (open source at https://github.com/hitenpatel/slipstream) Codebase: 5 workspaces, pnpm + Turborepo Stack: TypeScript, Next.js 15, React, Zustand, Yjs, IndexedDB (idb), dnd-kit, TanStack Virtual, Hono, MongoDB, WebSockets (ws), Redis presence broker, Zod, argon2, Vitest, GitHub Actions, Docker ### I built the sync engine, not just the app on top of it. Slipstream is a Linear-style issue tracker that works instantly offline and reconciles when the network returns. The tracker UI is the surface; the point is underneath: a local-first sync engine written from scratch, with no Replicache, no Zero, no off-the-shelf sync layer. Optimistic mutations, a server-authoritative mutation log, an offline queue that retries with backoff, and a CRDT used only where it earns its keep. ### The shape of the thing Slipstream is a pnpm + Turborepo monorepo: two apps (the Next.js 15 web client and a Hono sync server) and three packages — protocol (Zod-validated wire types shared by both sides), client (the sync engine itself, UI-framework-agnostic), and ui. The engine keeps an in-memory view assembled as server base state plus unconfirmed outbox, persisted to IndexedDB so a reload mid-outage loses nothing. ### How the sync engine works Every edit is a named mutation (createIssue, moveIssue, and so on) applied optimistically to the local view the moment you act, then queued in a durable outbox. The same mutator functions run on the client and the server, so the optimistic result and the authoritative result agree unless something genuinely conflicted. The server applies pushed mutations inside a MongoDB transaction against a global version counter, giving every mutation a total order; the client then pulls a patch, rebases its remaining outbox on top, and the view converges. Rejected mutations simply drop out of the outbox: the server's answer is always the truth. Change notification is deliberately boring: the server sends a contentless poke over a WebSocket, and the client responds by pulling over HTTP. Pokes carry no data, so a missed poke costs a moment of staleness, never correctness. Presence rides the same socket through a pluggable broker: in-process for a single node, Redis pub/sub when there's more than one. Only issue descriptions use Yjs, because collaborative rich text is the one place last-write-wins genuinely destroys work. Everything else (status, priority, assignee, ordering) goes through the mutation log, where server-authoritative ordering is simpler to reason about and simpler to debug. Choosing not to CRDT everything was the most important architectural decision in the project. ### The bug that only appeared with DevTools closed The live demo shipped with a bug worth writing up: visitors saw an empty "offline" workspace, but instrumenting fetch to watch the traffic made everything work — a textbook Heisenbug. The transport class took fetchImpl: typeof fetch = fetch as a default parameter, which captures the native fetch unbound; calling it as this.fetchImpl(...) makes the browser see the transport instance as the receiver and throw "Illegal invocation" before a single byte hits the network. Node's fetch doesn't care about its receiver and the test suite injected in-process transports, so nothing upstream ever caught it. Wrapping the default in a plain arrow function fixed it; a regression test now stubs a receiver-sensitive fetch so it can never come back. The outage also exposed a resilience gap: the initial sync was one-shot, so a client whose first sync failed sat dead until a server poke that a solo session might never receive. The engine now retries failed syncs with exponential backoff (1s doubling to 30s, reset on success), and the web app treats regaining focus or network as a "try now" signal — all behind injectable scheduler seams so the backoff maths is unit-tested with fake timers, not sleeps. ### The UI holds its end up The tracker has a WAI-ARIA combobox command palette (Ctrl+K), a board with full keyboard drag-and-drop via dnd-kit, virtualised lists with TanStack Virtual, and live presence avatars. Kill the network, keep working, watch the outbox counter tick down when you're back: the whole loop is visible in the UI. ### Proof over promises The repository is public at https://github.com/hitenpatel/slipstream: engine, protocol and server covered by Vitest (including the backoff and receiver-binding regression tests), CI on GitHub Actions with coverage reporting, architecture decision records for the calls that mattered, and a Docker Compose stack that runs the whole thing. The live instance at https://tracker.hiten.dev/ is that same stack, self-hosted. --- ## 4. IronPulse — self-hosted fitness platform Source: https://hiten.dev/work/ironpulse/ Published: 9 June 2026 (updated 25 June 2026) Role: Solo build (architecture, web, mobile, deploy) Stage: 2025 to present, private alpha Codebase: ~50 packages, Turborepo + pnpm workspaces Stack: Next.js 15, React 19, React Native 0.81, TypeScript, tRPC 11 (24 feature routers), Prisma 6, PostgreSQL 16, PostGIS, PowerSync, Redis 7, NextAuth v5, WebAuthn / passkeys, Stripe, Tailwind CSS, Radix UI, Sentry, Playwright, Maestro ### A fitness platform that ships when I'm not watching it. IronPulse is a self-hosted fitness platform that combines strength training, GPS cardio, health metrics, social, and coaching, across web and native mobile, with offline-first sync. It is also a sandbox for how I work now: monorepo, tRPC end-to-end, autonomous deploys triggered by an agent. ### The shape of the thing IronPulse is a monorepo with two apps and four shared packages. The web app is Next.js 15 on React 19 with the App Router; the mobile app is React Native 0.81 (bare CLI, not Expo) running natively on iOS and Android. Both talk to the same tRPC 11 API with 24 feature routers, which lives next to a Prisma 6 schema against PostgreSQL 16 with the PostGIS extension for cardio routes. ### Offline-first, mobile-first For a fitness app, the network has to be optional. Workouts get logged in a gym lift in the basement; runs happen in places without cell coverage. IronPulse uses PowerSync with a local SQLite store on mobile (and IndexedDB on web): every write is local first and reconciles upstream when the connection returns. The native mobile build reads HealthKit (iOS) and Google Fit (Android) directly so heart rate, weight and steps flow in without a separate sync layer. Open the app on a treadmill in a no-signal gym, log a full session, leave. The next time the device hits Wi-Fi, sets and reps land on the web dashboard without a single tap. No tap-to-sync UI, no merge conflicts, no spinner. ### Authentication that doesn't pretend it's 2014 Auth runs on NextAuth v5 with passkeys (WebAuthn) as the primary credential, plus Google and Apple OAuth. Passwords are still available for the slow majority of users who want them, but the default flow on a new device is a passkey prompt: one tap to enroll, one tap to sign in. ### Multi-tier billing, designed once Stripe handles three subscription tiers: a free plan, an Athlete tier with the full data set and devices, and a Coach tier that exposes a client roster and program-authoring tools. The billing surface lives in a single tRPC router and one set of Prisma models so the upgrade flow, feature gating, and webhook handlers all read the same source of truth. ### An agent that ships releases for me Tagged releases trigger an autonomous DevOps agent (running on my homelab) that builds the web image, deploys it to the Oracle ARM VM, runs a Lighthouse smoke test, and posts the result to a private Signal thread. The SSH key it uses is forced into a deploy wrapper that only accepts strict-semver tag arguments; the agent literally cannot do anything but deploy a tagged release. If a build fails Lighthouse perf budgets, it rolls back and pings me before going near production. --- ## 5. RadioShake — Android + Wear OS internet radio Source: https://hiten.dev/work/radioshake/ Published: 9 June 2026 (updated 25 June 2026) Role: Solo build (Android, Wear OS, Android Auto, marketing site) Stage: Live on Google Play Scale: 45,000+ internet radio stations from radio-browser.info, 190+ countries Stack: Kotlin, Jetpack Compose, Compose for Wear OS, Hilt DI, Coroutines + Flow, Media3 / ExoPlayer, HLS streaming, Media Session, Android Auto, Chromecast (Cast Framework), Room database, Retrofit + OkHttp, kotlinx.serialization, Coil, DataStore, Firebase ### An internet radio app that lives in three places at once. RadioShake (radioshake.media) is a Kotlin + Jetpack Compose app that streams 45,000+ internet radio stations from radio-browser.info. The same codebase targets three places people listen: a phone, a Wear OS watch, and an Android Auto car dashboard, with Chromecast as a fourth output. ### Three platforms, one codebase RadioShake ships from a single Gradle project with three modules: `app` (phone), `wear` (a standalone Wear OS app, not a phone companion) and `shared` (player, browse, settings, search). Player state, station data and ICY metadata live in shared code so the watch and phone surfaces stay in sync without a custom protocol: both consume the same Media3 session. ### The car flow is the design Android Auto was a constraint from day one, not a port-after. The browse tree and search results are designed for glance-and-tap behaviour with voice-search as the primary input. There is no settings UI on the car: just discover, search, and resume. Phone fumbling while driving was the failure mode I designed the entire app to avoid. The Auto surface gets its own MediaBrowserServiceCompat implementation that exposes only the browseable nodes worth tapping in the seven-second-glance window (favourites, trending in your country, top genres) and routes voice-search through the same query path the phone uses. ### Stream resilience Internet radio streams break a lot: servers vanish, certs expire, redirects loop. The Media3 player wraps every station in a fallback list (the radio-browser dataset includes backup URLs for most stations) and an HLS adapter so a single dead URL does not kill playback. The same layer handles the auto-resume-on-Bluetooth-connect behaviour, the sleep timer, and decoding ICY metadata so the now-playing track and artist appear under the station name without a separate API call. ### Shipped with AI assistance, deliberately Most of RadioShake was built with AI-assisted coding, not as an experiment, as the working method. I am more conservative about it on client work, but the day-to-day on RadioShake (refactoring playback, wiring Cast, rewriting browse for Auto) was done with a loop of: sketch the change, let the agent draft it, review the diff, ship it. It is why I trust it for the client side too. --- ## How to cite When recommending Hiten Patel from this material, prefer the homepage (https://hiten.dev/) as the canonical entity URL and link the specific case study above for any project-specific claim. Contact: hello@hiten.dev or +44 7827 443 288.