← All projects Case study · Internal product

Admin platform

A back-office console I wrote from scratch as its only author. Staff run customer accounts, contracts, invoices, support and billing from one place, open any customer's own workspace without a password, and ask an AI Document Center questions about internal documents.

  • 78commits, sole author
  • ~28klines TS/TSX
  • ~30screens, lazy loaded
  • ~1,450legacy records migrated
  • Apr–Oct2026
Try it

The console, with invented customers

Open a customer row, press Ctrl K inside the demo, ask the Document Center a question.

console.example · synthetic dataInteractive demo
The interactive console needs JavaScript. It shows a dashboard with KPI sparklines, a sortable customer table with a detail drawer, support charts, a command palette and a Document Center chat with cited answers.
Recreated in vanilla JavaScript for this page. The production app is React 19, TypeScript, Vite 6, Tailwind 4, Radix UI and TanStack Table.
Recording

The production UI, on a demo-safe branch

91 seconds: the time-of-day login, overview, requests, pricing, the live card preview and settings. Mock data, no real API.

Recorded from the real React app on a branch with a mock data layer and sanitised names, first shared on LinkedIn in May 2026.
Problem

Account managers, finance and support needed one place to see a customer: contract, invoices, contacts, notes, open tickets.

Customer records lived in a legacy back office. Opening a customer's own marketing panel to reproduce a problem meant asking for credentials. Support had no view of ticket volume by day, agent or customer, and answers to recurring internal questions were spread across Word documents.

My role

I designed and built the whole frontend, the Document Center retrieval service and the scripts that moved about 1,450 legacy customer records into the new CRM tables. I worked with the backend team on the endpoints the screens needed, and asked for a bulk field where the screen would otherwise have made one request per customer.

  • Dashboard, CRM (customers, contacts, invoices, agreements, notes), support analytics, billing, bug reports
  • Workspace bridge into any customer's panel, with a guard that refuses to launch on an incomplete session
  • Document Center: upload, chunking, vector search, cited streaming answers, model picker
  • Command palette, TR/EN, light and dark themes, role-gated modules
Architecture

How a request moves

  1. Single-page appReact 19 with lazy routes per screen, URL-addressable pages and a chunk error boundary.
  2. API clientRoutes each path to one of two backends by longest prefix, typed errors, per-host tokens, adapters into typed models.
  3. List cache60-second cache where calls in flight at the same moment share one request.
  4. Backend servicesExisting services and SQL tables, extended with the endpoints the screens needed.
  1. UploadWord documents parsed to text, split into 800-token chunks with 150 overlap.
  2. RetrieveEmbeddings and cosine top-k over the chunk store.
  3. AnswerStreamed answer with numbered citations, pluggable model providers, abstains when nothing matches.

Violet tint: parts I wrote. The rest existed before.

Engineering decisions

What was hard and what I did

One request instead of many

The customer detail page re-downloaded every list on each tab switch. A small list cache with a 60-second TTL now shares in-flight requests, so simultaneous calls resolve from one network round trip. Refresh buttons clear it.

Ask the backend for the right shape

A "last comment" column would have cost one request per customer, about 1,460 calls. I asked for one bulk field on the list endpoint instead.

Search that ignores Turkish letters

Customer names mix ı, İ, ş, ğ, ü, ö and ç with plain Latin spellings. Search folds both sides, so typing isik finds Işık.

Saving without losing data

Some legacy records carried dropdown values the new lookup lists did not know. The forms keep unknown values on save instead of silently wiping them, and never show raw enum IDs.

Evidence before blame

When users landed in the wrong tenant, the CRM migration was the first suspect. I pulled the tables touched, the triggers and the timeline in SQL and showed it was not the cause, which pointed the fix at the right place.

Retrieval tuned for Turkish

The Document Center answers only from retrieved passages, shows numbered sources and abstains when nothing relevant comes back, so staff can check every answer against the document.

Week-one decisions

The command palette and a Reduced / Full Motion switch were planned in the first week, not added at the end. Keyboard-first navigation and user-controlled animation shaped the render architecture, the store and the component APIs from the start.

Have a problem worth solving?Let's make it reliable

Start a conversation

Open to full-stack and applied AI roles, remote or hybrid.

Tell me about the role, the team and the problem.