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 customer workspaces for support, and ask an AI document assistant questions about internal documents.
- ~80commits, sole author
- ~28klines TS/TSX
- ~30screens, lazy loaded
- 1,000+legacy records migrated
- Apr–Oct2026
The console, with invented customers
Open a customer row, press Ctrl K inside the demo, ask the document assistant a question.
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.
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 assistant retrieval service and the scripts that moved over a thousand 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
- Support access to customer workspaces, with a guard that refuses to launch on an incomplete session
- AI document assistant: upload, chunking, vector search, cited streaming answers, model picker
- Command palette, TR/EN, light and dark themes, role-gated modules
How a request moves
- Single-page appReact 19 with lazy routes per screen, URL-addressable pages and a chunk error boundary.
- API clientRoutes each path to one of two backends by longest prefix, typed errors, per-host tokens, adapters into typed models.
- List cache60-second cache where calls in flight at the same moment share one request.
- Backend servicesExisting services and SQL tables, extended with the endpoints the screens needed.
- UploadWord documents parsed to text, split into 800-token chunks with 150 overlap.
- RetrieveEmbeddings and cosine top-k over the chunk store.
- AnswerStreamed answer with numbered citations, pluggable model providers, abstains when nothing matches.
Violet tint: parts I wrote. The rest existed before.
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 assistant 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.
- The console, with invented customers
- The production UI, on a demo-safe branch
- Problem
- My role
- How a request moves
- What was hard and what I did
- One request instead of many
- Ask the backend for the right shape
- Search that ignores Turkish letters
- Saving without losing data
- Evidence before blame
- Retrieval tuned for Turkish
- Week-one decisions


Notes on this post
Join in
Leave a comment.
Sign in to add your note to this post.
Only your name and avatar are used.