CleanNami Field Ops - Gig Economy Workforce App

January 6, 2026

React (Vite)SupabaseStripe ConnectGeolocationOptimistic LockingPWA

Executive Summary

While the CleanNami Platform handles customer bookings, the Field Ops App is the mission-critical tool used daily by the workforce. It transforms a complex logistical challenge—managing distributed independent contractors—into an automated, self-regulating system.

Core Achievement: The application successfully decentralizes quality control. By enforcing a strict "Evidence-Based" workflow (GPS + Photos + Checklists), the app ensures standards are met without on-site managers, enabling the platform to scale indefinitely while maintaining high reliability.


Technical Architecture

Tech Stack

  • Frontend: React 18 with react-router-dom (SPA Architecture)
  • Build Tool: Vite
  • UI System: TailwindCSS, Shadcn UI, Lucide React
  • Backend/Data: Supabase (PostgreSQL, Realtime, Edge Functions)
  • State Management: React Context + Custom Hooks
  • Native Features: Capacitor (Camera, Geolocation, Push Notifications)
  • Payments: Stripe Connect (Express Accounts)

Architecture Highlights

  • Offline-First Capabilities: JobWorkflow state restoration ensures data isn't lost if the browser refreshes or loses signal during a clean.
  • Real-time Synchronization: Supabase Realtime channels listen for job swaps and availability updates instantly.
  • RPC-Heavy Logic: Complex business rules (like "swap eligibility" and "reliability scoring") are offloaded to Postgres RPCs to keep the client light and secure.
  • Optimistic Locking: Prevents race conditions in the job swap market without complex server-side mutexes.

The "Proof-of-Work" Execution Engine

Feature Overview

The core of the application is a rigid, 4-stage state machine that prevents a cleaner from getting paid until the job is proven complete. This replaces the need for a human supervisor.

File: JobWorkflow.tsx

The Workflow State Machine:

  1. Instruction Phase: Cleaner reviews notes, access codes, and property specifics.
  2. Evidence Phase (Arrival):
  • GPS Lock: useLocationTracking logs coordinates against property geofence.
  • Before Photos: Camera capture required for pre-existing damage.
  1. In-Progress: Timer runs; cleaner executes task list.
  2. Completion Phase:
  • After Photos: Mandatory upload of specific zones (Kitchen, Bath, etc.).
  • Checklist Validation: All required boolean flags must be true.
  • Capture: Triggers completeJobAndCapture API.

Code Highlight: Evidence Validation

The app uses a dynamic validation logic that scales requirements based on property size (e.g., larger homes require more photos).

// JobWorkflow.tsx
const isEvidenceComplete = () => {
  const requiredChecklist = checklist.filter((item) => item.required);
  
  // Dynamic requirements based on property size 
  // e.g. 2 photos per bath * 3 baths = 6 photos required
  const beforePhotosMet = beforePhotos.length >= totalRequiredBefore;
  const afterPhotosMet = afterPhotos.length >= totalRequiredAfter;
  const checklistMet = requiredChecklist.every((item) => item.completed);

  return beforePhotosMet && afterPhotosMet && checklistMet;
};

Technical Challenge: Taming Timezones

The Problem: "It’s Always 6:00 PM in Florida"

To ensure adequate workforce coverage, cleaners must submit their availability for the upcoming two weeks by Sunday at 6:00 PM ET. If they miss this hard deadline, they enter a "Grace Period" until 7:00 PM ET.

Relying on the user's device time (new Date()) was insecure; a cleaner could change their phone's clock or use a VPN to bypass the deadline.

The Solution: Server-Authoritative Logic

We standardized all business logic to ignore the device's local time in favor of a calculated "Target Time." We utilize a utility function, getFloridaDateTime(), which forces the application to evaluate time based on the market's timezone.

File: Availability.tsx

// Availability.tsx
const calculateSubmissionState = (bypass: boolean = false) => {
  // 1. Get standardized time, ignoring device locale and VPNs
  const now = getFloridaDateTime(); 
  const currentHour = now.getHours();
  
  // 2. Enforce strict business hours (Florida Time)
  // Check if today is Sunday and if it matches the bi-weekly epoch cycle
  if (now.getDay() === 0 && isSubmissionSunday(now)) {
    if (currentHour < 18) return { state: 'open' }; // Before 6:00 PM ET
    if (currentHour < 19) return { state: 'grace' }; // 6:00 PM - 7:00 PM ET
  }
  
  // 3. Fallback to finding the next valid submission window
  const nextSub = getNextSubmissionSunday(addDays(today, 1));
  return { 
      state: 'closed', 
      nextSubmission: nextSub 
  };
};

Decentralized Job Swapping

Feature Overview

Cleaners often have emergencies. Instead of calling support, they use the Swap Market to trade shifts. This system must handle high concurrency—if two cleaners try to accept the same job simultaneously, only one can succeed.

Files: JobDetails.tsx, useAvailableSwaps.ts

Solution: Optimistic Locking

We implemented a robust optimistic locking pattern at the database level using Supabase. We do not use a "Check then Update" pattern, which is prone to race conditions. Instead, the UPDATE query itself enforces exclusivity.

// useAvailableSwaps.ts
const acceptSwap = async (swapId: string) => {
  const { data, error } = await supabase
    .from('swap_requests')
    .update({
      replacement_cleaner_id: cleanerId,
      status: 'accepted'
    })
    .eq('id', swapId)
    .eq('status', 'pending') // <--- THE OPTIMISTIC LOCK
    .select()
    .single(); // <--- Expects exactly 1 row to change
    
  if (!data) throw new Error("Swap was already taken or expired.");
  toast({ title: "Swap Accepted!", description: "Job added to your schedule." });
}

Why this works:

  1. Atomic Operation: The database executes the UPDATE statement atomically.
  2. Zero Rows Affected: If a second user tries to accept, the status is no longer 'pending', so the query updates 0 rows.
  3. Graceful Failure: The app catches the null return and informs the user the job is gone.

Compliance-First Onboarding

The app creates a legal firewall for the platform. A cleaner cannot access the dashboard or see jobs until the multi-stage onboarding is complete.

File: Onboarding.tsx

  1. Profile Setup: Basic demographics and experience validation.
  2. Stripe Connect: Direct integration with Stripe's onboarding flow to establish a connected Express account for payouts.
  3. Legal Signing: Digital signing of Contractor Agreements, W9s, and Liability Waivers. Documents are stored in Supabase Storage with strict RLS policies.
  4. Capabilities: Self-certification for premium skills (e.g., Hot Tub Maintenance) which unlocks higher-paying job tiers.

Payment Flow Sequence

The interaction below illustrates how the frontend triggers the payment capture only after evidence is secured.

sequenceDiagram
    participant C as Cleaner App (JobWorkflow.tsx)
    participant S as Supabase (DB & Storage)
    participant E as Edge Function (Payment)
    participant ST as Stripe API

    Note over C: User taps "Submit Evidence & Complete"
    
    C->>S: Upload Photos (Before/After)
    S-->>C: Return Public URLs
    
    C->>S: UPDATE evidence_packets (status: 'complete')
    
    C->>E: POST /complete-job-and-capture (jobId)
    
    rect rgb(240, 240, 240)
        Note right of E: Server-Side Validation
        E->>S: Verify evidence completeness
        E->>ST: Stripe.PaymentIntents.capture(paymentIntentId)
        ST-->>E: Capture Success
        
        E->>S: INSERT into payouts (status: 'pending')
        E-->>C: Return { success: true }
    end
    
    C->>S: DELETE from jobs (clean local view)
    C->>C: Toast: "Payment Captured!"

Conclusion

The CleanNami Field Ops app is an automated manager. By codifying business rules—like the "Submission Sunday" deadline and the "Evidence Packet" requirement—directly into the React logic, the platform achieves operational consistency that usually requires a large human operations team.

It demonstrates a mastery of Supabase-centric architecture, where the frontend acts as a high-fidelity interface for complex backend logic (RPCs) and real-time data streams.