3D Chart Mode
Engineering Case Study · Solo Build

From Zero to 8 — Architecting a Solo, AI-Integrated Enterprise Platform in 5 Months

No off-the-shelf product fit. So I designed and shipped one from scratch: eight integrated modules spanning architecture, permission systems, an AI analysis layer, UI/UX, infrastructure, and production deployment. Then the company built the rest of itself on top of it.

staffreached across the org
projectslive in the system
taskstracked to date
modulesone source of truth
Role
Solo Architect & Engineer
Full-stack + IT Admin
Timeline
5 months · zero to production
Status
Live in production
actively expanding
Core Stack
React PWA · TypeScript · Firebase · Vercel · Google Cloud · LLM APIs

Plus an AI analysis layer that sits across these modules, consuming their data rather than forming a ninth (see 04 — AI Layer).

01 — Overview

One system, eight domains, one source of truth

I designed and shipped a fully integrated internal operations platform covering eight functional domains — built independently, from system architecture and data modeling through UI/UX, infrastructure, and deployment.

The guiding philosophy was Operational Truth + Immutable Logs: business data can be updated, but work records and activity logs are written once and never edited. Nothing is deleted, only archived. This single principle shaped every architectural decision downstream.

The system was used to manage its own development — work logs, task tracking, and changelogs all lived inside the platform from day one.
02 — The Problem

Every ERP covered the generic 80%.
None covered the 20% that mattered.

The organization ran eight operational domains across disconnected tools — spreadsheets, chat threads, standalone apps — with no single source of truth, no audit trail, and no way to correlate data across functions like project hours, leave requests, and resource bookings.

The specific workflows were the problem: attendance records that feed back into project work logs, a rental system covering rooms, vehicles, and equipment in one interface, and a permission model where two managers with the same role see completely different data depending on their assignments.

So I built it from scratch.

03 — Scope

Eight functional modules

01
Employee Management
Staff profiles, department structure, role assignments
02
CRM
Client records, contacts, interaction history
03
Project Management
Project → Item → Task hierarchy, work logs, time tracking
04
Attendance & Leave
Leave, overtime, business trips — all linked to work logs & Calendar
05
Announcements
Company-wide and department-targeted notices
06
Rental System
Rooms, studio, vehicles, equipment — synced to Calendar; meeting-room bookings extend into Meeting Records with AI transcription & dispatch
07
Quotation System
Built in response to a need raised by the management team mid-project, demonstrating the architecture's flexibility to absorb new domains. Covers manpower and cost estimates, with planned integration into the project system.
08
Software Subscriptions
Internal SaaS license tracking and ownership

How the eight modules connect

Everything resolves to a single source of truth. Identity flows down from Employee Management; operational data flows up into Project Management; events and records flow out to the calendar, logs, and AI analysis. Hover or tap any module to trace its links; toggle the legend to see which modules push notifications out.

Meeting Record is not a ninth module — it extends the Rental system's Meeting Room subtype, linking meeting decisions into task assignment and writing them back into Project Management.

Identity flows down from Employee Management into eight core modules centred on Project Management, then events and records flow out to a notification, calendar, logging, AI analysis and dashboard layer. GChat and LINE notifications are marked on the modules that emit them. Google OAuth · sign-in Employee Management identity · org · roles — global reference account lifecycle CRM clients · contacts Attendance leave · overtime Rental rooms · vehicles Project Management project → item → task → work log Meeting Record → task review/assign Announcements standalone Subscriptions standalone Quotation manpower · cost Aggregation & output notifications · calendar · logging · AI analysis · dashboard / snapshot
Eight core modules tap a chip to highlight · tap a box to trace links
04 — AI Layer

An AI analysis layer,
grounded in the company's own data

Project management is where the platform's data density pays off. I built a configurable LLM layer that reads live project and work-log data straight from Firestore and turns it into structured, executive-ready analysis, grounded in the organization's real context rather than generic model output.

Provider-agnostic by design

The layer connects to multiple major LLM providers through one interface, with model selection exposed per run. Switching models for cost, capability, or availability is a configuration choice, not a rewrite.

Grounded generation — RAG + context engineering

Output is driven by the organization's own knowledge: client brand profiles, the project database, and reusable capability templates feed a retrieval layer, so analysis reflects real entities and prior work rather than invented specifics. Context is assembled deliberately: the right records, in the right order, within the model's window.

Token economy

Source material is summarized and compressed before it reaches the model, preserving signal while cutting token consumption across high-volume calls.

Tuned, not raw

Prompts are calibrated for a consistent output style and format, so analysis lands in a shape the team can act on immediately.

Versioned, never overwritten

Each run is saved as its own tabbed version; re-analyzing produces a new version instead of replacing the last: the same write-once principle that governs the rest of the platform.

A real run, end to end

Sample run · Week 26 (Jun 22–28, 2026)
Analyzed    2026-06-27 00:50 · live from Firestore
Tokens      7,783   (in 6,961 / out 822)
Model       GPT-4o (OpenAI)
Est. cost   ~NT$0.82  (US$0.0256 @ NT$32)
Raw system output — as captured 分析範圍:2026 第26週(06-22~06-28)· 分析日 26-06-27(六) 00:50 · 本次分析使用 7,783 tokens(輸入 6,961/輸出 822)· 模型 GPT-4o(OpenAI)· 估算花費約 NT$0.82(US$0.0256,匯率 32)

Per-run token usage and cost are surfaced directly in the output, a designed outcome of the compression and context strategy above. Management sees exactly what each analysis consumes and what it returns, instead of an opaque AI bill.

05 — Architecture

A five-layer system

Frontend Layer        →  React PWA (TypeScript, Tailwind CSS) · Vercel
Application Logic     →  Permission Engine · Audit Logger · Snapshot Builder
Firebase Platform     →  Firebase Auth + Firestore + Functions
Data Layer            →  Operational · Immutable Logs · Snapshots · Archives
Intelligence & Output →  LLM Analysis Layer · PDF / Excel Export

The first four layers govern truth: what's recorded and how it's protected. The fifth layer consumes that truth and produces derived artifacts on top of it: AI analysis, reports, and document exports. Those artifacts follow the same write-once discipline (an analysis run is versioned, not overwritten), so the intelligence layer extends the system's philosophy rather than bypassing it.

Why Firestore over a relational DB?

The data access patterns were document-centric and user-scoped. Firestore's real-time listeners reduced round-trips for dashboard views, and its security rules let me enforce access control at the database layer as a second line of defense. I configured composite indexes manually to handle complex multi-field queries across modules without performance degradation.

Transactions for consistency

Multi-step operations run through Firestore transactions: a leave approval that updates both the request and the linked work log, a rental booking that checks availability and writes the calendar event atomically. Either everything succeeds, or nothing does.

06 — Access Control

RBAC + ABAC: the most complex part

I designed a hybrid model combining Role-Based and Attribute-Based Access Control.

CHAIRMAN → CEO → DIRECTOR → MANAGER → GENERAL
                           → HR
                           → FIN
                           → ADMIN / OPS_ADMIN
+ Project Manager Permissions

On top of roles, permissions are constrained by three attributes:

Two users with the same role can have entirely different data visibility. A PROJECT MANAGER sees only projects they're assigned to; a GENERAL user sees only their own records. The permission engine is validated independently of Firestore security rules — a defense-in-depth design where even a misconfigured query can't leak data.

07 — ID Strategy

Human-readable, immutable identifiers

Every entity carries a structured ID that encodes its context directly — no joins required to read a log.

EntityFormatExample
ProjectYYYYMMDD-PNN20260307-P01
ItemYYYYMMDD-PNN-INN20260307-P01-I01
Task…-PNN-INN-TNN…-P01-I01-T01
Work LogYYMMDD-WLNNNN260328-WL0001
Leave RequestYYYYMMDD-TYPE-NNN20260310-OFF-001

The embedded date refers to the parent entity's first actual start date, not the record's creation date. Once written, IDs never change.

08 — Integrations

Meeting users where they already are

Meeting outcomes → tracked work

Meeting notes are usually where decisions go to die. Here, a meeting record links straight into the work pipeline: captured decisions become assigned tasks and push real updates to the associated tickets, so what was agreed and what's tracked never drift apart, and every decision stays connected to the work it produced in one trail.

In-app recording → transcription → tracked work

Meetings are recorded directly from the meeting-record page in one tap, uploaded to Firebase Storage, and transcribed by Gemini 2.5 Flash on Vertex AI. The transcript is prompt-tuned for Traditional Chinese, multi-speaker prefixes, and explicit unclear-audio markers, then feeds the same AI analysis layer that surfaces decisions and action items, turning a spoken conversation into structured, assignable tasks with no minutes typed by hand.

Why Vertex AI, specifically: meeting content is confidential business data, so I deliberately routed transcription through Vertex AI rather than a public API endpoint — enterprise data governance guarantees the audio and transcripts are never used to train models. Access to transcription follows the same permission model as the rest of the system (record owner, attendees, or senior roles only). Recordings over 30 minutes are truncated on capture, a deliberate boundary around serverless execution limits.

Meeting AI, one release later

A follow-up release chained the whole flow behind one button: transcript, then AI summary, then suggested dispatch. The dispatch step pulls decisions and to-dos out of the transcript, matches them against existing project tasks, proposes an owner for each, and skips resolutions it already generated; items worth keeping but not assignable land under "other discussions". A pre-meeting keyword field (names, products, companies) cuts recognition errors, long recordings are transcribed in segments with visible progress, and every record shows its accumulated AI cost at the bottom. Original audio is always preserved, and a failed transcription says exactly why.

Google Calendar — full lifecycle

Attendance records and room bookings are managed via the Calendar API across the full event lifecycle: not just creation, but update and deletion with dynamic attendee management as assignments change. Keeping external calendar state and internal system state consistent, handling partial failures and avoiding drift, was the real engineering work beyond simply calling the API.

GChat Bot + LINE Bot

Real-time notifications through two channels: Google Chat for office staff, LINE for field staff. They cover approvals, task assignments, and announcements. The system comes to the user, not the other way around.

Absence-aware approval routing

The approval engine reads live data from the Attendance module: when any approver, from a department head up to the CEO, is on leave or traveling, requests escalate to the next level automatically, and pending-approval reminders to that person pause. Someone back from a trip isn't greeted by a pile of expired notifications. The modules feed each other, and the workflow adapts to organizational reality on its own, one more persona-aware behavior falling out of the single-source-of-truth design.

Webhooks, exports & email

External attendance data is reconciled into the immutable work log via webhooks. Key modules export to PDF and Excel, the output half of the Intelligence & Output layer. A server-side layer sends email notifications as a reliable fallback to the bot channels.

09 — Craft

A PWA, designed — not decorated

The UI/UX was designed independently, informed by a background in graphic design. Visual consistency across eight modules — spacing, hierarchy, color, interaction — was treated as a first-class requirement, not an afterthought.

10 — Performance

Fast where it counts,
untouched where it doesn't

Six decisions govern how the platform loads, renders, and reads data. Each one started with a number from the actual build, and the two tagged as core design are the ones I'd walk through first in a design review.

Bundle size: chunks split by hand

Instead of trusting automatic vendor splitting, manualChunks sorts libraries by how often pages use them. Code every page touches lands in shared vendor chunks (vendor-react, vendor-firebase, vendor-lucide): downloaded once, cached, reused on every navigation. Heavy libraries that serve a single page, like recharts and d3, stay inside the dashboard's lazy chunk and load only when someone opens it.

Two packages needed force. A build test showed that leaving xlsx unsplit put ~880 KB into the index bundle, a cost every page paid on every visit. Split out, it loads only on the export page; the whole ~1.1 MB package refuses to tree-shake, and that's an accepted trade. mock-data, at ~2.3 MB the heaviest single item in the index, got its own chunk as well.

mock-data~2.3 MB
xlsx~1.1 MB
index, before+880 KB

Measured from the production build. The first two now load on demand; the striped bar is what every page carried while xlsx sat in the index.

Libraries every page touches earn a shared chunk. Libraries one page touches wait their turn.

Route-level lazy loading, 75 times

Nearly every page mounts through React.lazy(() => import(...)): 75 call sites across the app. Named exports pass through .then(m => ({ default: m.Xxx })) because React.lazy only accepts default exports, and a single <Suspense> boundary with a spinner covers every loading state. Landing on the home page downloads the index, the shared vendors, and that one page's chunk. Everything else waits for a click.

fsCache: a hand-built cache in front of Firestore

No react-query, no SWR. fsCache exists for one reason: Firestore bills per read, and most reads here would return unchanged data. It caches on two levels, in memory and in localStorage, so a page refresh keeps the cache. getAll(keys) pulls several collections in one call and re-fetches only the stale ones, while an inflight Map dedupes concurrent requests so a race can't hit the same collection twice.

The TTL is 180 days. That number only makes sense as a fallback for when the onSnapshot connection drops; the real expiry logic comes next. A separate getEmployeeCache() keeps a 4-hour snapshot of employee data and serves stale data on a miss instead of blocking the page.

Core design

Cache invalidation, event-driven

The conflict: a 180-day TTL is what actually saves money, but every client still has to notice a change within seconds. Long TTL and fresh data pull against each other, and cache invalidation is famously one of the two hard problems in computer science. The resolution is a version broadcast:

01 · write
A write lands in any collection
The write path calls cacheVersionSync, which bumps that collection's version number in a single doc: system_meta/cache_versions.
02 · broadcast
One document fans out
Every client holds exactly one onSnapshot listener, on that doc. The version change reaches all of them at once.
03 · invalidate
One key expires
Each client marks only the changed collection stale. The rest of the cache stays warm.
04 · refetch
The next read pays, once
The next getAll() sees the expired fetchedAt and re-reads just that collection.
05 · remember
Known versions persist locally
cn-cache-versions-v1 in localStorage records versions already seen, so a reload doesn't re-invalidate data that's still fresh.
Reads sit near zero on a normal day. When someone edits data, exactly one collection refreshes, on every client.

Rendering: knowing when not to optimize

A count of what the codebase actually contains:

useMemoexpensive derived values
useCallbackonly where a reference matters
React.memodeliberately absent
virtualizationdeferred until ~10k rows

useMemo does the real guarding: filtering, sorting, assembling project chains, computing statistics. Those are recalculations worth avoiding. React.memo and list virtualization guard re-renders themselves, and at a 30-person scale there's nothing there worth guarding yet; wrapping every child component would be optimizing for a size the system doesn't have. useCallback follows the same logic: with no memoized children, a stable callback reference buys little, which is why it appears 69 times against useMemo's 587.

The next bottleneck already has a name. When work_logs passes ten thousand entries and filtering or print views start to drag, list virtualization goes in first. What each tool solves, and whether today's data volume has that problem, matters more than how many tools show up in a grep.

Core design

API strategy: two paths under a 12-function ceiling

Data access splits into two paths, both built on native fetch and the cache layer above.

Path A · bulk data
Straight to Firestore
  • Reads go through fsCache; writes go through updateDoc, writeBatch, runTransaction
  • No backend hop. firestore.rules stands as the second permission gate behind the app's own engine
  • Page entry fires one getAll() across up to five collections, in parallel, deduped in flight
Path B · secrets & server work
29 calls into 7 functions
  • fetch('/api/...') appears 29 times across the app
  • Vercel's Hobby plan allows 12 serverless functions, so endpoints multiplex with ?action=
  • One function carries four AI features: worklog-ai, meeting-stt, meeting-dispatch, meeting-summary; another carries calendar create, update, and delete
  • The 60-second timeout once killed meeting transcription mid-file; audio is now chunked on the client and transcribed in segments
7 / 12 functions used

The usual answer to a function ceiling is a paid plan. Keeping bulk data off the API entirely, and multiplexing what remains behind ?action=, decouples how many features ship from how many functions the platform allows.

11 — Infrastructure

The layer most frontend engineers never touch

Delivering this to a live organization meant owning the Google infrastructure end to end:

Version control uses structured atomic commits — each message documents intent and scope, serving as the sole maintainer's PR description and changelog. A version changelog is maintained inside the system itself.

12 — The Hardest Part

Designing for every role at once

The technical architecture was the straightforward part. The real challenge was continuous requirement discovery across departments, each with different workflows, mental models, and sometimes conflicting priorities, and translating all of it into one system that serves everyone without compromising data integrity.

A general staffer needs frictionless work-log entry. A manager needs a cross-project workload view. A director needs clean KPIs. Finance needs an audit trail. HR needs attendance tied to project records. These aren't UI differences — they demand different access patterns, permission boundaries, and definitions of "done" for the same record.

The RBAC+ABAC model and the immutable log separation were shaped by these conversations, not by upfront design alone.

I ran UAT cycles with staff before each module launched, and onboarded both general users and managers, adapting the explanation to each person's technical comfort.

"I can't believe an architecture this large was actually finished." — the CEO, at the first full demo

After the full demo, leadership greenlit continued development on the spot. The leave module rolled out company-wide within a week, and a scheduling tool another team had been prototyping in parallel was consolidated into this platform's roadmap, a merge the prototyping team themselves had been hoping for as their design kept converging on what the platform already did. It passed the only validation that matters: the people closest to the alternative wanted to build on this one.

13 — Full Stack

Everything under the hood

TypeScriptReactVitemanualChunksReact.lazy × 75Tailwind CSSNode.js PWA / Service WorkerFramer Motiondnd-kitDesign Tokens ExpressFirebase AuthFirestore Firestore TransactionsComposite IndexesEvent-Driven Cache Invalidation Firebase FunctionsVercelServerless FunctionsGoogle OAuth Google Calendar APIGoogle Cloud ConsoleWorkspace AdminMediaRecorder APIFirebase StorageSpeech-to-TextVertex AIGemini 2.5 Flash GChat BotLINE BotWebhooks PDF / Excel ExportGA4RBAC + ABAC LLM APIMulti-ProviderRAG Context EngineeringPrompt EngineeringToken Optimization