Restructured the codebase, built three new verticals, and closed every security vulnerability.
A Python script migrated 25 files into a feature-based layout. Travel Tickets, Courier & Moving, and Kampala Real Estate Broker were built from scratch. The monorepo came out of a security audit clean.
The codebase was flat. Everything lived in pages/, components/, and store/. Before building three new verticals, I wrote a Python script to reorganise it. It either worked or broke everything.
The constraint
A flat structure meant every new vertical scattered files across unrelated folders. Adding Courier & Moving would mean a page in pages/, a component in components/, a store in store/ — no grouping, no ownership. The fix was a feature-based layout under src/features/. The risk was rewriting every import path across the entire codebase in one move.
The proof
The migration script
restructure.py moved 25 files and rewrote every import reference across every .ts and .tsx file it touched:
import os, shutil, re
base_dir = r"d:\Destination\bexze-monorepo\artifacts\fikalo-web"
src_dir = os.path.join(base_dir, "src")
moves = [
("pages/LoginPage.tsx", "features/auth/LoginPage.tsx"),
("store/useAuth.ts", "features/auth/useAuth.ts"),
("pages/TravelTicketsPage.tsx", "features/tickets/TravelTicketsPage.tsx"),
("components/TicketBookingSheet.tsx", "features/tickets/TicketBookingSheet.tsx"),
("pages/MyOrdersPage.tsx", "features/orders/MyOrdersPage.tsx"),
("store/useCart.ts", "features/orders/useCart.ts"),
("pages/LandingPage.tsx", "features/core/LandingPage.tsx"),
("pages/HubPage.tsx", "features/core/HubPage.tsx"),
# ...25 files total
]
replacements = {
"@/store/useAuth": "@/features/auth/useAuth",
"@/store/useCart": "@/features/orders/useCart",
"@/pages/LoginPage": "@/features/auth/LoginPage",
"@/pages/TravelTicketsPage": "@/features/tickets/TravelTicketsPage",
"@/pages/MyOrdersPage": "@/features/orders/MyOrdersPage",
"@/pages/LandingPage": "@/features/core/LandingPage",
# ...full replacement map
}
for src_rel, dest_rel in moves:
src_path = os.path.join(src_dir, src_rel)
dest_path = os.path.join(src_dir, dest_rel)
if os.path.exists(src_path):
os.makedirs(os.path.dirname(dest_path), exist_ok=True)
shutil.move(src_path, dest_path)
for root, dirs, files in os.walk(src_dir):
for file in files:
if file.endswith((".ts", ".tsx")):
file_path = os.path.join(root, file)
with open(file_path, "r", encoding="utf-8") as f:
content = f.read()
for old_import, new_import in replacements.items():
pattern = re.compile(rf'([\'"]){re.escape(old_import)}([\'"])')
content = pattern.sub(rf'\1{new_import}\2', content)
with open(file_path, "w", encoding="utf-8") as f:
f.write(content)
The new structure:
src/features/
auth/ — LoginPage, ProfilePage, ProfileDropdown, useAuth
cars/ — RentalCarsPage, CarBookingSheet, rental-cars
core/ — LandingPage, HubPage, AdminPage
courier/ — CourierPage
orders/ — MyOrdersPage, OrderDetailsPage, CheckoutPage, useCart
properties/ — PropertyPage, KampalaRealEstatePage
tickets/ — TravelTicketsPage, TicketBookingSheet
The build passed clean after the script ran.
Travel Tickets — built from scratch
Travel Tickets existed as a card on the landing page with nowhere to go. It became a working vertical.
mockSupabase.ts got a travel_tickets table with a simulator that auto-confirms bookings after 15 seconds:
setTimeout(() => {
ticket.status = "confirmed";
dispatchMockEvent("ticket_updated", ticket);
}, 15000);
TravelTicketsPage.tsx — a search dashboard with bus and flight filters for Kampala and Mogadishu routes.
TicketBookingSheet.tsx — a multi-step slide-out drawer: traveler details, seat selection, payment, confirmation.
MyOrdersPage.tsx — a Travel Tickets tab that renders confirmed bookings as boarding passes with QR codes.
Courier & Moving — built from scratch
Four vehicle classes, dynamic UGX pricing, a dispatch simulator with four state transitions:
// mockSupabase.ts — courier dispatch loop
const states = ["pending", "assigned", "picked_up", "delivered"];
let step = 0;
const interval = setInterval(() => {
if (step >= states.length) {
clearInterval(interval);
return;
}
order.status = states[step];
dispatchMockEvent("courier_updated", order);
step++;
}, 8000);
| Vehicle | Use case | Base price |
|---|---|---|
| Boda | Small packages | 5,000 UGX |
| Cab | Fragile or medium loads | 12,000 UGX |
| Pickup | Heavy items, appliances | 35,000 UGX |
| Box Truck | Full house moves | 80,000 UGX |
CourierPage.tsx — district selector for pickup and delivery, vehicle class picker, contact entry, mobile money payment sheet. MyOrdersPage.tsx — status polling wired to dispatch events, showing live state transitions.
Kampala Real Estate Broker — built from scratch
Mogadishu already had property listings. Kampala had nothing. KampalaRealEstatePage.tsx added Kampala-specific rentals with dual UGX and USD pricing, a broker match form with a budget slider from 500,000 to 10,000,000 UGX, and a live-matching simulator:
// mockSupabase.ts — broker matching stages
const stages = [
{
status: "SEARCHING_BROKER",
delay: 0,
message: "Finding a broker near you...",
},
{
status: "BROKER_ASSIGNED",
delay: 6000,
message:
"Broker Assigned! Andrew Mukasa (+256 782 111 222) is on your request.",
},
{
status: "PROPERTIES_FOUND",
delay: 18000,
message: "Andrew Mukasa found 3 properties matching your criteria.",
},
];
stages.forEach(({ status, delay, message }) => {
setTimeout(() => {
inquiry.status = status;
dispatchMockEvent("fikalo:inquiry_update", { inquiry, message });
}, delay);
});
When a broker is assigned, a WhatsApp button appears linking to https://wa.me/256782111222 with a pre-filled message. When PROPERTIES_FOUND fires, a banner renders linking to /courier — if you just found a place, you might need a truck.
PropertyPage.tsx required a type fix to support the new Kampala listings — price_ugx was missing from the mock property schema:
// PropertyPage.tsx — before
type MockProperty = {
price: number;
// ...
};
// After
type MockProperty = {
price: number;
price_ugx?: number;
// ...
};
The back button now routes dynamically: /kampala-real-estate for IDs prefixed kp, /mogadishu otherwise.
Mombasa paused
Removed from geofencing.ts, useCity.ts, HubSwitcher.tsx, LandingPage.tsx, and BottomNav.tsx. The City type now accepts only kampala and mogadishu. Not deleted — paused.
Hub integration — 9 services, one grid
All three new verticals were registered in KAMPALA_SERVICES and the hub grid replaced its hardcoded row layout with a single wrapping grid:
// HubSwitcher.tsx — KAMPALA_SERVICES
const KAMPALA_SERVICES: ServiceConfig[] = [
{
key: "FOOD",
label: "Order Food",
subtitle: "Restaurants & cafés",
icon: UtensilsCrossed,
isTab: true,
sectionLabel: "Food Stores",
},
{
key: "PHARMACY",
label: "Pharmacy",
subtitle: "Medicines & wellness",
icon: Pill,
isTab: true,
sectionLabel: "Pharmacies",
},
{
key: "GROCERY",
label: "Groceries",
subtitle: "Fresh produce & more",
icon: ShoppingBasket,
isTab: true,
sectionLabel: "Grocery Stores",
},
{
key: "RIDER",
label: "Order Rider",
subtitle: "Quick errands",
icon: Bike,
isTab: true,
sectionLabel: "Riders",
},
{
key: "CAR",
label: "Order Car",
subtitle: "Rides across Kampala",
icon: Car,
isTab: true,
sectionLabel: "Cars",
},
{
key: "RENTALS",
label: "Rent a Car",
subtitle: "Self-drive & chauffeured",
icon: Car,
isTab: false,
href: "/rental-cars",
},
{
key: "TICKETS",
label: "Travel Tickets",
subtitle: "Bus & flights",
icon: Ticket,
isTab: false,
href: "/travel-tickets",
},
{
key: "COURIER",
label: "Courier & Moving",
subtitle: "Packages & relocation",
icon: Truck,
isTab: false,
href: "/courier",
},
{
key: "REAL_ESTATE",
label: "Real Estate Broker",
subtitle: "Find homes & offices",
icon: Home,
isTab: false,
href: "/kampala-real-estate",
},
];
Nine services. Three rows. Any new vertical added to the config renders automatically.
Security audit — clean
$ pnpm --filter @workspace/fikalo-web typecheck
> tsc -p tsconfig.json --noEmit
Exit Code: 0
$ pnpm audit
No known vulnerabilities found
Vulnerabilities were found in an earlier pass. Fixed with pnpm.overrides in the root package.json:
"pnpm": {
"overrides": {
"picomatch": "^4.0.4",
"path-to-regexp": "^8.4.0",
"fast-uri": "^3.1.2",
"ws": "^8.21.0",
"vite": "^7.3.5",
"yaml": "^2.8.3",
"postcss": "^8.5.10",
"qs": "^6.15.2",
"brace-expansion@^2": "2.0.3",
"esbuild": "^0.28.1",
"@babel/core": "^7.29.6"
}
}
Where this sits
| Item | Status |
|---|---|
| Feature-based codebase restructure | Done |
| Python migration script — 25 files moved | Done |
| Travel Tickets vertical | Done |
| Courier & Moving vertical — 4 vehicle classes | Done |
| Kampala Real Estate Broker — broker simulator, WhatsApp, dual pricing | Done |
| Mombasa paused | Done |
| Kampala hub — 9 services, unified grid | Done |
| TypeScript typecheck — zero errors | Clean |
| Security audit — zero vulnerabilities | Clean |
| Supabase production wiring | Not started |
Three verticals, one migration script, zero vulnerabilities. The workspace is clean.