Web application development
Applications that run in a browser: dashboards, admin panels, customer portals, internal tools and customer-facing products. We own what happens between the URL and the pixel — rendering, state, forms, real-time updates, keyboard access and how fast it all feels on a mid-range laptop.
Who this is for
- You have a working API or database and no usable interface on top of it — the team is running the business out of SQL clients and spreadsheets.
- You have one screen or one flow that needs building or fixing — a report page, a multi-step form, a table that locks the browser at 5,000 rows — and you want it done properly rather than bolted on.
- Your React app has grown past what the original team planned for: megabytes of JavaScript on first load, prop-drilling through eight levels, and a form library nobody understands.
- You are shipping a product front end that has to be fast, keyboard-navigable and readable by a screen reader, because a customer's procurement checklist now asks.
What usually goes wrong
The problems this work exists to solve
Everything is client-rendered, so the first paint is a spinner
We map every route to a rendering mode instead of applying one to the whole app. Content that must be indexed or read before login is server-rendered or statically generated. Data-dense screens behind auth stream from the server with Suspense boundaries drawn along data-dependency lines, so the shell and navigation paint while the slow query is still running. Screens that are pure interaction — a canvas editor, a drag-and-drop builder — stay client-rendered, because server-rendering them buys nothing and costs cache complexity. The trade-off is explicit: server rendering moves cost onto your servers and makes auth-aware caching harder, so we only pay it where a user actually notices.
State is a single global store holding data that belongs to the server
Most React state bugs we are called in for come from one mistake: server data copied into a client store, then edited in two places. We split them. Server data lives in a query cache with explicit cache keys, staleness rules and invalidation after mutation. Genuine client state — a wizard step, an open panel, an unsaved draft — lives in a small store or a state machine. Anything a user should be able to share, bookmark or reach with the back button — filters, sort order, page number, selected tab — lives in the URL query string, not in memory. That one move removes a whole class of "it works until you refresh" tickets.
The big form re-renders the entire page on every keystroke
Long forms — onboarding, KYC, claim submission, product configuration — go wrong when every field is a controlled component feeding one state object. We use uncontrolled inputs with a form library that subscribes per field, so typing in field 40 does not touch fields 1 to 39. Validation is schema-first: one schema is the source of truth, imported by the browser for instant feedback and by the server handler for the check that actually matters. Conditional fields, repeating sections, cross-field rules and server-returned field errors are all modelled in that schema. Duplicated validation is validation that drifts apart.
The data table is the product, and it dies at scale
Admin and operations tools live or die on one grid. Past a few thousand rows, rendering every row is the bottleneck; past a few hundred thousand records, so is shipping them to the browser at all. We virtualise rows and columns so the DOM only holds what is on screen, then move filtering, sorting and pagination to the server behind a keyset-paginated endpoint once the dataset outgrows a single response. Column resize, pinned columns, inline edit, row selection across pages and CSV export are designed together, because retrofitting selection-across-pages onto a virtualised grid is a rewrite.
Live data is faked with a setInterval that hammers the API
We pick the transport from the write pattern, not from novelty. Low-frequency status changes get polling with backoff and conditional requests. Server-to-client streams — job progress, notifications, a ticker — get Server-Sent Events, which reconnect on their own and survive infrastructure that mangles protocol upgrades — provided proxy buffering is explicitly disabled, which is the usual reason a working stream goes silent in production. Genuinely bidirectional work — collaborative editing, presence, live cursors — gets WebSockets, and then we have to answer reconnection, message ordering and what the UI shows during a dropped connection. Optimistic updates always ship with a reconciliation rule and an idempotency key, so a retried mutation cannot apply twice.
Keyboard and screen-reader users cannot complete the core flow
The usual causes are a div acting as a button, a modal that does not trap or restore focus, a custom dropdown with no ARIA roles, and route changes that leave focus stranded in a stale document. We build interactive widgets on headless primitives that already implement the keyboard contract, manage focus explicitly on navigation and on dialog open or close, announce async results through live regions, and set colour tokens that meet contrast at the token level so no individual screen can quietly fail. We target WCAG 2.2 AA and test with a real screen reader, not only an automated scan — automated tooling catches only part of what a user hits.
Scope
What you actually get
Rendering and routing plan
A route-by-route decision: static, server-rendered, streamed or client-only, with the caching rule and the auth boundary for each. Written down, because the next person to add a route needs to know which pattern it belongs to.
Component library and design tokens
Accessible primitives — dialog, menu, combobox, tabs, date picker, toast — built once on headless foundations, plus colour, spacing and type tokens defined for light and dark. Documented in Storybook so a component gets reused instead of re-invented inside a feature folder.
Forms and validation layer
Shared schemas, per-field subscriptions, conditional and repeating sections, autosave for long flows, file upload with progress and resumption, and consistent handling of server-side field errors. Error messages written for the person filling the form, not for the developer.
Data layer and state model
Query cache with named keys and invalidation on mutation, URL-backed filter and pagination state, and a small client store for true UI state. Loading, empty, error and permission-denied states designed for every data surface rather than added during QA.
Real-time and offline-tolerant behaviour
The chosen transport, plus reconnect and backoff, stale-data indicators, conflict handling when two people edit the same record, and optimistic updates that roll back cleanly. The app should degrade to a readable state on a hotel wifi connection, not a blank screen.
Accessibility conformance work
Keyboard paths through every core flow, focus management on route and dialog transitions, ARIA on custom widgets, contrast-checked tokens, reduced-motion support, and a written report of what was fixed and what remains.
Performance budgets wired into CI
Per-route JavaScript budgets and Core Web Vitals thresholds checked on pull requests, so a heavy dependency fails the build instead of quietly landing. Includes the bundle analysis that shows which import cost what.
Technology
Technology options
We are technology-agnostic. These are the choices we reach for, and how the decision actually gets made.
Framework
Next.js when the same app has public indexable pages and auth-aware server rendering, and when server components genuinely cut the JavaScript shipped. A plain Vite SPA when everything sits behind a login: no SEO need, no first-paint data need, a simpler mental model, and a static build anyone can host. The React server component model is a real cognitive cost on a small team, and we do not charge you that cost for an internal tool.
State and data
Query cache for server data by default. Redux Toolkit only where there is a large, genuinely shared client-side domain model and a team that already knows it — otherwise it becomes a second, stale copy of the database. XState when a flow has real states and illegal transitions, such as a payment or KYC wizard, where a pile of booleans always produces an impossible combination eventually.
UI layer
Headless primitives plus your own tokens when the product carries a brand and you want to own the markup. MUI or Ant Design when the surface is an internal back office, nobody will show it in a pitch deck, and shipping fifty screens quickly matters more than visual identity. The cost of a component kit is the day one dialog has to behave differently from the kit's opinion.
Tables, charts and editors
TanStack Table when we control the rendering and want it virtualised our way. AG Grid when users expect spreadsheet behaviour — grouping, pivoting, range selection, Excel-grade export — and the licence cost is cheaper than building it. The charting library follows the chart types you actually need, since the lightweight ones stop at the first unusual axis requirement.
Real-time transport
SSE first for one-way streams: it is a plain HTTP response, it reconnects natively, and it survives infrastructure that mangles upgrades. WebSockets when the client writes as often as it reads. CRDTs only when concurrent editing of one document is a product requirement, because you then own merge semantics, document history and storage growth for the rest of the product's life.
How it runs
The shape of the engagement
- 01
Scope call and interface inventory
We list the screens, the roles that see each one, and the data each needs. For an existing app we also read the code and run a build analysis. This is where a Rs 25,000-shaped task gets confirmed as one, or where we say early that it is bigger than you thought.
- 02
Data contracts before pixels
We agree what each screen calls and what comes back — shapes, pagination style, error format, permission semantics. Missing or awkward endpoints get identified now, while they are a conversation with your backend team rather than a workaround in the browser.
- 03
One thin vertical slice, deployed
The first increment is a single real screen: routed, authenticated, fetching live data, styled with the actual tokens, on a preview URL you can click. It settles rendering strategy, state pattern and folder structure while changing them is still cheap.
- 04
Build in reviewable increments
Feature branches produce a preview deployment per pull request. Anything half-built sits behind a feature flag so it can merge without blocking a release. You review working software on a URL, weekly, rather than a status document.
- 05
Harden: states, keyboard, budgets
Before a flow is called done it gets its loading, empty, error and denied states, a keyboard pass, a screen-reader pass on the primary path, and a bundle check against the route budget. This is a pass on the way through, not a phase at the end that gets cut.
- 06
Handover that survives us leaving
Storybook for the component library, short architecture decision records explaining why the rendering and state choices were made, the CI gates left switched on, and a walkthrough with whoever inherits the code. You should be able to hire a React developer and have them productive without calling us.
Engineering
Architecture, security and performance
The decisions that are expensive to change later, and where we stand on them.
Architecture
- Rendering is a per-route decision with a written rule
- Applying one rendering mode across a whole application is how apps end up either slow to first paint or impossible to cache. We classify each route by whether it needs indexing, whether its first paint depends on user-specific data, and how interactive it is after load. Marketing and documentation go static. Dashboards stream from the server with the shell painting first. Editors stay client-side. The classification lives in the repo so the pattern holds after we hand over.
- Feature-sliced folders, not type-sliced ones
- Code is grouped by feature — billing, patients, orders — each owning its components, hooks, schemas and tests, with a shared layer for primitives and a lint rule preventing cross-feature imports. Grouping by type instead (all components here, all hooks there) means every change touches four directories and the shared folder becomes an unowned bin. Feature slices also make it obvious when a feature has grown enough to justify its own route-level code split.
- The client boundary is drawn deliberately
- With server components, every prop crossing into a client component is serialised into the HTML payload. That is both a performance question and a disclosure question: a whole user or order record passed to a client component ships every field, including the ones the UI never renders. We keep interactive leaves small, pass only the fields they need, and audit that boundary as part of code review.
- Design tokens over per-component styling
- Colour, spacing, radius, type scale and elevation are defined once as tokens with light and dark values, and components consume only tokens. Theming, dark mode and contrast compliance then become one file rather than a sweep through hundreds of components. It also means a rebrand is a token change, which is the difference between a week and a quarter.
- The front end assumes the network is unreliable
- Every mutation has defined behaviour for slow, failed and retried. Retryable writes carry an idempotency key so a double-submit or an automatic retry cannot create two records. Reads have a stale-while-revalidate rule. The UI distinguishes "loading" from "you have no data" from "we could not reach the server", because collapsing those three into one spinner is what generates support tickets.
Security
- Session tokens never live in localStorage
- Anything readable by JavaScript is exfiltratable by one successful cross-site scripting payload, including one arriving through a compromised npm dependency. Sessions ride in httpOnly, Secure, SameSite cookies with short-lived access tokens and rotating refresh tokens. Where a token must reach browser-side code, it is scoped narrowly and kept in memory only for the tab's lifetime.
- Content Security Policy, and what it actually costs to tighten one
- A nonce-based CSP that excludes unsafe-inline and unsafe-eval is the target, and frame-ancestors to stop clickjacking is free — take that on day one. Getting to nonce-strict is the part teams underestimate: animation and charting libraries that server-render inline style attributes, a theme bootstrap script that has to run before first paint, and third-party embeds all break under it. The honest sequence is report-only first, read the violation reports for a fortnight, remove or nonce the real offenders, then enforce. This site is itself mid-sequence — report-only today, for exactly those reasons. Alongside that: HTML rendered from user or CMS input goes through a sanitiser, and raw HTML injection stays in one reviewed helper rather than spread across components.
- Hiding a button is not authorisation
- Role checks in the UI are a usability feature. Every route handler and every API call re-checks permission server-side, and we test that by calling the endpoint directly with a lower-privileged session. The same applies to object identity: a screen that loads a record by ID must be answered by an endpoint that verifies the record belongs to the caller's tenant, or you have a broken-access-control hole with a friendly interface on it.
- Uploads go direct to storage, and are served from another origin
- Files upload with short-lived pre-signed URLs constrained by size and content type, so payloads never transit the application server. They are then served from a separate origin or a download-only path with an explicit content-disposition, so an uploaded HTML or SVG file cannot execute in the app's own security context and read its cookies.
- Nothing secret crosses the bundle boundary
- Client bundles are readable by anyone. API keys, private endpoints, internal hostnames and gate logic hiding unreleased pricing all leak if they are placed in a public environment variable. We check what actually appears in the shipped bundle rather than trusting naming conventions, and keep privileged calls behind server routes.
Performance
- Budgets are enforced in CI, not measured after launch
- Each route gets a JavaScript budget and Core Web Vitals thresholds checked on every pull request. A dependency that pushes a route over its budget fails the build with the bundle diff attached. Performance treated as a periodic cleanup always loses to the next feature deadline; treated as a build gate, it holds.
- Interaction latency is usually hydration and re-renders
- Slow-feeling apps are rarely slow to load — they are slow to respond. We reduce shipped JavaScript by keeping interactive components at the leaves, split routes and heavy widgets so a charting library is not parsed on the login page, memoise the expensive subtrees rather than everything, and move filtering or sorting of large collections off the main thread or onto the server. Long tasks show up in field data, so we look at real user measurements, not only a lab score.
- Largest paint is fonts, images and late-inserted banners
- Fonts are self-hosted, subset and preloaded with a matched fallback so text is readable immediately and does not shift when the webfont lands. Images carry intrinsic dimensions, responsive sources and modern formats. Anything injected after load — cookie bars, notification strips, ad slots — reserves its space up front, because layout shift under the element a user is about to tap is worse than a slightly slower load.
- Waterfalls are an architecture problem, not a caching problem
- A screen that fetches a user, then their org, then that org's invoices makes three sequential round trips before anything renders. We co-locate and parallelise requests, deduplicate identical fetches within a render, and prefetch on hover or focus for likely navigations. When the real cost is an endpoint returning too much or requiring too many calls, that is a backend conversation and we raise it as one instead of hiding it behind a longer spinner.
- Perceived speed is engineered separately from actual speed
- Skeletons match the final layout so content does not jump into place. Mutations apply optimistically where the failure case is recoverable. Navigation prefetches on intent. None of this improves a synthetic score, and it is the difference between an app that feels immediate and one that feels like paperwork.
Size of work
Where your project probably sits
A rough map so you can locate yourself before talking to us. Actual scope comes from a conversation, not a table.
| Level | Example | Engagement shape |
|---|---|---|
| Focused task | One screen, one form flow, a data table that freezes at a few thousand rows, an accessibility fix pass before a procurement review, or a bundle-size investigation with the fixes applied. | One engineer, one to two weeks, scoped to a written in/out list and delivered on a preview URL you can click before it merges. From about Rs 25,000. |
| Internal tool or admin panel | An operations console over an existing API: login, roles, fifteen to thirty screens, searchable tables, bulk actions, an audit view and exports. | A few weeks to a couple of months, usually one or two engineers, weekly demo on a live environment. Often paired with a component library so your team can add the next twenty screens without us. |
| Customer-facing product front end | A portal or SaaS interface with a design system, billing and account surfaces, real-time updates, role-based views, internationalisation and accessibility conformance. | A months-long engagement with a small cross-functional group — front-end engineers plus design and QA involvement — running in reviewable increments with fortnightly planning. |
| Front-end platform | Several applications sharing one component library and token set, with route-level code ownership, a versioned design system and multiple teams committing to it. | Continuous engagement with a dedicated team, quarterly roadmap, and explicit ownership handover milestones so the platform ends up maintained by your engineers. |
Honest limits
When this is not what you need
Telling you this early is cheaper for both of us than discovering it in month two.
- You need a native app — camera and sensor access, background processing, offline-first sync, push notifications, an App Store presence. A web app can do some of that badly. Start at mobile app development instead.
- You need a brochure site, a blog or a landing page. A CMS template configured well will cost a fraction of a custom front end and be easier for your marketing team to edit. We will say so rather than build you an application you did not need.
- The real product is the API or the integration platform, and the interface is a thin admin view. Then the engineering effort belongs on contract design, throughput and idempotency — see API development — and the UI is a much smaller piece of work.
- You want a visual redesign only, with no change to behaviour, data or performance. That is a design engagement first; bringing engineers in before the design direction exists means paying us to hold opinions about typography.
Related work
Projects in this space
Headless commerce storefront
A storefront rebuilt against a headless backend — category and product rendering, cart state and checkout flow separated from the commerce platform.
Read the case study: Headless commerce storefrontAdaptive learning platform
A learner-facing web application with progress tracking, assessment flows and an instructor-side console over the same data.
Read the case study: Adaptive learning platformPatient records interface
Clinical record screens built over integrated data sources, where dense tabular views and careful form validation carry most of the work.
Read the case study: Patient records interface
Questions
Common questions
How small a piece of web work will you actually take?
Small work starting around Rs 25,000 is genuinely welcome — one screen, one form, a slow table, an accessibility pass before a customer review. We scope it as a written list of what is included and what is not, and deliver it on a preview URL you can click before it merges. A well-defined small task is often the cleanest way for both sides to find out whether a longer engagement makes sense.
Can you work inside our existing React codebase rather than rewriting it?
Usually yes, and usually that is the right call. We start by reading the code and running a build analysis to see where the weight and the coupling are, then propose targeted changes: route-level code splitting, moving server data out of the global store, replacing a hand-rolled dropdown with an accessible primitive. A rewrite is recommended only when the framework version or the state model blocks the outcome you are paying for, and we will name the specific thing that blocks it.
Do you default to Next.js?
For anything with public pages, or where first paint depends on server data, Next.js with the App Router is the usual choice. For an application entirely behind a login — an internal tool, an admin console — a React SPA built with Vite is often better: fewer moving parts, a simpler mental model for your team, and a static build to host. The decision comes from your routes and your team, not from a house preference.
Do you handle design as well as front-end engineering?
We handle interface engineering, design systems and the UX of technical surfaces — tables, forms, filters, dashboards, empty and error states. If you already have designs in Figma we build to them and flag anything that will not survive real data, long strings or a 320px viewport. If there is no design direction at all, that is worth resolving before engineering starts, and we will tell you rather than improvise a brand.
How do you approach accessibility?
We target WCAG 2.2 AA and build it in rather than remediating later. In practice that means accessible primitives for interactive widgets, deliberate focus management on route changes and dialogs, contrast handled at the design-token level, and a keyboard and screen-reader pass on each core flow. Automated scanners run in CI but catch only part of the picture, so primary paths get tested with a real screen reader. You get a written list of what was fixed and what is outstanding.
Our API is slow. Does that become your problem?
Partly. We can remove browser-side waterfalls, parallelise and deduplicate requests, cache sensibly and prefetch on intent, which often accounts for more of the felt slowness than people expect. What we will not do is hide a genuinely slow or badly shaped endpoint behind a longer skeleton. We will show you the timing breakdown and either work with your backend team or take that work on as API development.
What stops the front end getting slow again after you leave?
The budgets stay in CI. Per-route JavaScript limits and Core Web Vitals thresholds fail a pull request that regresses them, with the bundle diff attached so the cause is obvious. Combined with the architecture decision records and the component library, a new developer inherits the constraints along with the code instead of discovering them six months later.
Keep reading
Related work and reading
Related services
- API development
A web app is only as responsive as the endpoints behind it. This covers contract design, pagination style, error shapes and idempotency — the things that decide how many round trips a screen needs.
- Mobile app development
When the requirement involves device sensors, background work, offline-first sync or store distribution, that belongs in a native or React Native app rather than a browser.
- MVP development
If the web app is the first version of a new product and the scope is still moving, the MVP page covers how a first release gets cut down to what can actually be validated.
- Software testing
Browser-side test strategy — component tests, Playwright journeys, visual regression and accessibility checks in CI — is covered in detail on the testing page.
- Cloud and DevOps
Preview environments per pull request, edge caching, CDN configuration and deployment pipelines for the front end are set up on the cloud and DevOps side.
Send us the screen that is causing trouble
A URL, a Figma link, or a screenshot of the table that freezes is enough to start. We will tell you whether it is a one-week fix, a rebuild of one route, or a bigger front-end problem — and if it is smaller than you feared, we will say that too.