← All projects

Electric Submersible Pump design for oil & gas

ESP Design Platform

A web platform for designing Electric Submersible Pump (ESP) installations in oil wells: from a 3D wellbore trajectory to pump, motor, and cable selection, in one seven-step engineering workflow with layered validation and print-ready reports.

Role
Frontend Engineer · Freelance
Year
2026
Type
Client project
Note

Team project. I worked on the Next.js frontend; the FastAPI backend was built by another team. The 3D viewer was co-built with a teammate. All data shown in the covers is demo data.

ESP Design Platform
Three well profiles (J-type, S-type, horizontal) drilled from one pad, with a top view and per-well survey stats
J, S, and horizontal profiles from one pad, rendered from survey stations with the same average-tangential math the backend uses for MD → TVD.
Workflow window with the directional survey grid and 3D trajectory, an equipment selection result card, and a pump curve
From survey to selection: the directional survey grid and 3D viewer, the equipment selection result, and the pump curve at the design rate. Demo data.
Next.jsReactTypeScriptThree.jsTanStack QueryZustandTailwindPWA

Client work under NDA: source code and live app are not publicly available.

7

Workflow steps

5

Validation layers

9

Equipment catalogs

~1,100

TypeScript files

~104k

Lines of code

780+

Commits

Problem

Choosing the right ESP means a long chain of dependent calculations: well geometry → fluid properties → reservoir inflow → required head → only then pump, motor, cable, and surface equipment. It usually lives in spreadsheets or expensive desktop software, scattered and hard to audit, and one wrong number early on ripples all the way to the equipment that gets ordered.

Solution

A frontend that guides engineers through the chain step by step, validates every input against physical and engineering rules, visualizes the well in 3D, warns when an upstream change invalidates selected equipment, and turns the result into equipment recommendations and print-ready reports.

Problems Solved

ESP design done in separate spreadsheets or licensed desktop tools, with no single flow or version trail

→One guided web workflow where each step unlocks only after the previous one is valid

Input errors surface at the end of the process, far from where they were made

→Per-field save on blur with server errors mapped back to the exact field

Changing one input (e.g. water cut) can silently invalidate a pump that was already selected

→A cross-step guard that checks impact after saves and cascades the release of invalid equipment

Pumps set in a dogleg fail early, and pressure math uses TVD, not MD

→An interactive 3D trajectory viewer showing exactly where the pump sits on the well path

Results must reach clients and field teams as formal documents

→Compact, Detail, and Attachment reports on A4 pages with engineer-chosen chart and 3D snapshots

Field sites with unstable connectivity

→PWA with workflow queries persisted to IndexedDB and an offline mutation queue

Key Features

Seven-Step Workflow

  • Well Description → Wellbore Data → Fluid Properties → Data Inflow → Design Criteria → Equipment Selection → Report
  • Directional survey step appears only for non-vertical wells
  • Next is enabled only when the backend reports the step as complete (can_continue)
  • Role-based landing: engineers go straight to the workflow, admins to the dashboard

Wellbore & Trajectory

  • Spreadsheet-style survey grid: keyboard navigation, paste from Excel from the active cell, copy back, unit conversion
  • Four 2D views (MD vs TVD, MD vs Angle, top view, isometric) plus one 3D view
  • KPI strip with MD, TVD, N/S, E/W, angle, and azimuth at pump depth

Engineering Calculations

  • PVT curves, viscosity, and water cut from fluid properties
  • IPR curves with PI, Vogel, and Composite models; productivity index and Qmax
  • Total Dynamic Head, GVF, and performance curves from design criteria
  • Unit switching (ft/m, psi/bar, bbl/d/m³/d) with canonical values stored on the server

Equipment Selection

  • Pump → intake → protector → motor → cable & MLE → wellhead → controller → transformer
  • Tiered status per item: recommended, preferred, acceptable, warning, fail
  • Summary panel with design recommendation vs actual selection and specific checks (thrust, casing clearance, shaft HP, ampacity)
  • Surface cable shown per segment: wellhead → junction box → switchboard/VSD → transformer

Master Data

  • Catalogs for pumps, motors, protectors, intakes, cables, MLE, wellheads, controllers, transformers, casing and tubing
  • Paginated tables with search, status toggle, and Excel import with templates
  • Pump curve form from tabulated points: paste from Excel, monotonic rate validation, up to 30 points

Reports

  • Compact, Detail, and Attachment reports on responsive A4 pages with @page portrait/landscape
  • Well schematic and equipment callouts drawn as SVG
  • Snapshots taken from the exact same chart components as the workflow

System Design & Architecture

Next.js 16 App Router with a strict three-layer data flow: services → hooks → components. Components never call the API directly; every endpoint has its own service file wrapped by a TanStack Query hook. A central fetchData() attaches the token and device ID and handles 401 → refresh → retry once → logout behind a single-flight guard. Server state lives in TanStack Query (workflow queries persisted to IndexedDB), design context and per-step errors in Zustand with versioned migrations, and offline sessions and the mutation queue in Dexie. The UI follows Atomic Design, with React Compiler enabled.

UI (Atomic Design)

  • atoms → molecules → organisms → pages
  • Workflow canvas with step routing from the pathname

Data layer

  • Services per endpoint + TanStack Query hooks
  • fetchData(): token, X-Device-Id, single-flight refresh

Client state

  • Zustand: active well, espDesignId, per-step errors
  • IndexedDB: persisted queries, Dexie offline queue

External

  • FastAPI backend (built by another team)
  • Serwist service worker

Tech Stack

Framework
Next.js 16 (App Router, standalone output), React 19, React Compiler, TypeScript
3D & Charts
Three.js via React Three Fiber + drei, Recharts, Chart.js, custom SVG isometric projection
State & Data
TanStack Query (IndexedDB persister), Zustand, Dexie
UI
Tailwind CSS v4, Leaflet maps, CSS print (@page) reports
Offline
Serwist PWA with a custom service worker

Technical Deep Dive

013D wellbore trajectory viewerModern wells are rarely vertical. Engineers need to see where the pump sits on the well path, using numbers identical to the hydraulic calculations.+
Survey grid (MD · Inc · Azi) ─► backend (average tangential)
  ─► trajectory (TVD · North · East)
       ├─► 2D charts: MD vs TVD · MD vs Angle · top view · isometric
       └─► WellboreThree (React Three Fiber) ◄─ pump setting depth
              └─► toDataURL ─► A4 report attachment
  • Coordinates: East → +X, TVD → −Y, North → +Z, at 1 unit = 1,000 ft
  • Well path built as a CatmullRomCurve3 → TubeGeometry with an emissive material
  • Bounding box computed from the data: 6 translucent planes, 12 edges, and a grid on each plane, memoized
  • Billboard labels for surface, max TVD, compass points, and PSD, outlined to stay readable over the grid
  • Tooltips rendered through drei <Html> into the regular DOM so they can use theme CSS variables
  • PSD marker with its own index so its selection never collides with survey stations
  • OrbitControls with damping and zoom limits, a GizmoHelper, and fog for depth
02Level of detail from camera distanceWells with hundreds of stations hide the path under dots, so station visibility depends on how close the camera is.+
  • In useFrame, the distance from the camera to the bounding-box center drives station scale
  • Far away only the tube shows; zooming in grows the stations smoothly until each one is clickable
03Light/dark themes inside WebGLThree.js materials take JavaScript values, not CSS variables, so the scene doesn't follow a theme switch on its own.+
  • Twin palettes (PALETTES.light / PALETTES.dark) with 26 tokens: colors, opacities, and light intensities
  • Selected from a Zustand theme store: a module-level store is still readable inside <Canvas>, which runs in a separate React reconciler where regular Context doesn't reach
04One capture pipeline for three chart technologiesReports capture the exact same chart components used in the workflow instead of redrawing them.+
  • Chart.js and Three.js canvases captured with toDataURL()
  • Recharts SVG serialized and rasterized at 3× for 300 dpi print, with computed styles inlined first because an SVG inside <img> can't read page CSS variables
  • preserveDrawingBuffer is optional: off in the workflow, on only in the report snapshot modal, so the workflow keeps driver optimizations
  • Snapshot canvas ratio derived from A4 page geometry (card size after padding, header, and gap), so the PNG fills the report card with no empty bands
05Five layers of validationEngineering inputs have physical rules (flowing pressure below reservoir pressure, PSD above the perforations, liner below production casing), and results depend on each other across steps.+
Input ─blur─► 1 Client     number format, empty fields
             2 Server     domain rules via per-field upsert
                ├─ error {key, detail} ─► mapped to dotted field path
                └─ success ─► 3 Step status   can_continue → Next button
                               4 Calculations  results marked stale on change
                               5 Cross-step    /impact → equipment cascade
                                  └─► Equipment selection summary
  • Every field saves on blur or Enter through an upsert endpoint; no giant Save button validating 30 fields at once
  • Errors extracted from several response shapes and mapped to dotted paths like perforation.top, so the message appears right under the wrong input
  • Per-step error stores in Zustand; editing a field clears its own error, a successful save clears the step
  • Technical backend messages normalized into actionable sentences
  • A pending-save counter blocks leaving a step before the last blur has saved
  • Two-stage confirmation for risky changes, such as a PSD that may cause pump-off; declining restores the value and flags the field
06Calculations that know when they're staleOn Data Inflow, only the input group that changed invalidates its result.+
  • Static (Pr / static fluid level) and flowing (Pwf / flowing fluid level / test rate) groups compared separately
  • Changed group marked stale and its result hidden until the engineer recalculates
  • Switching the IPR model deliberately doesn't invalidate results, since it doesn't change test data
  • Composite model selectable only when bubble-point conditions recommend it
  • Uncalculated drafts kept in sessionStorage so they survive navigation
07Cross-step impact guardUpstream changes can push the selected pump to fail, and every downstream item is sized from that pump.+
  • EquipmentSelectionGuard mounted once on the workflow canvas, active only on the four upstream routes
  • Triggered when all mutations on the page settle (useIsMutating), not by hooks in each form, so a new save path can't slip through
  • Calls /impact, then fires the cascade endpoint from the right point (pump → intake → protector → motor → cable → MLE) and shows what was released
08Equipment selection summaryOne panel that brings together dozens of results from many endpoints, consistent with the selection tables.+
  • Design criteria (rate, TDH, target Q, fluid SG, GVF) grouped with status colors; GVF has its own explained scale
  • Shares query keys with the selection forms, so recommendations stay filled after the form unmounts, with no extra requests
  • One motor catalog endpoint stores max_motor_load as a side effect; the summary calls it without that parameter so reading never writes

Trade-offs

Assess impact after values are saved

over Predict impact before saving

Predicting would mean running a second PVT → IPR → TDH chain outside the main path; judging what's already stored keeps the message always accurate

One guard triggered by mutations settling

over Impact checks inside each form

The four upstream pages save through different paths (blur, unit dropdown, Calculate); hooking each path risks missing a new one

Backend decides step completeness (can_continue)

over Duplicate completeness rules in the frontend

One source of truth; rules change on the server without the frontend drifting

Trajectory computed by the backend

over Compute MD → TVD in the browser

Numbers on screen are identical to the ones used in hydraulic calculations and reports

Custom SVG isometric projection for the top view

over A second WebGL scene

Lightweight, sharp in print, and easy to annotate

Challenges & Lessons

Three.js scene ignored theme changes

Twin token palettes read from a module-level Zustand store inside <Canvas>

WebGL lives outside CSS; design tokens need a JavaScript twin

Report snapshots of 3D came out black

preserveDrawingBuffer enabled only in the snapshot modal

Turn on expensive rendering flags only where they're needed

Server errors landed far from the field that caused them

Normalize error shapes and map keys to dotted field paths

Validation is only useful when the message appears where the mistake was made

Upstream edits silently invalidated selected equipment

A single settle-triggered guard with a cascading release

Cross-cutting checks belong in one place, triggered by state, not by every code path

Ongoing Improvements

  • Extend the offline mutation queue beyond Well Description to the other forms

Next project

Asset Management System

→