Architecture

System Overview

Evergrn is a three-tier application:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  React Web (Vite)    β”‚   β”‚  Azure Static Web App  β”‚   β”‚  React Native (Expo)β”‚
β”‚  port 5173 (dev)     β”‚   β”‚  web.staging.evergrn.coβ”‚   β”‚  iOS only           β”‚
β”‚  Vite proxy β†’ :3000  β”‚   β”‚  Entra MFA required    β”‚   β”‚  staging.evergrn.co β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚ localhost                   β”‚ VITE_API_BASE              β”‚ JWT auth
         β”‚                            β”‚ fetch interceptor           β”‚
         β–Ό                            β–Ό                             β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    Node.js / Express API  (port 3000)                        β”‚
β”‚           src/app.js β†’ src/routes/* β†’ src/middleware/*                       β”‚
β”‚                                                                              β”‚
β”‚  Dev:     evergrn-api-dev-c7dxhkf3ctcgdqby.canadacentral-01.azurewebsites.netβ”‚
β”‚  Staging: staging.evergrn.co (evergrn-api-stage App Service B1)              β”‚
β”‚  Prod:    evergrn.co (not yet provisioned)                                   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                    β”‚ Prisma ORM
                    β–Ό
         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
         β”‚  PostgreSQL          β”‚     β”‚   Stripe    β”‚
         β”‚  dev:   evergrn-db   β”‚     β”‚   (payments)β”‚
         β”‚  stage: evergrn-db-  β”‚     β”‚             β”‚
         β”‚         stage        β”‚     β”‚             β”‚
         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Environments

Dev Staging Production
API URL evergrn-api-dev-*.azurewebsites.net staging.evergrn.co evergrn.co (TBD)
Web URL localhost:5173 (Vite) web.staging.evergrn.co (Azure SWA) evergrn.co (TBD)
Web hosting Vite dev server Azure Static Web App Standard TBD
Database evergrn-db (shared dev data) evergrn-db-stage (isolated) evergrn-db-prod (TBD)
GodMode βœ… Enabled ❌ Disabled ❌ Disabled
Access control Open Entra CA policy (MFA) on web; API open Public
Deploy trigger .\deploy.ps1 API: ADO pipeline ID 2 (manual); Web SWA: ADO pipeline ID 4 (manual) TBD
iOS API target Dev App Service (via .env) staging.evergrn.co (via eas.json env) TBD

Tech Stack

Layer Technology Version / Notes
API runtime Node.js + Express CommonJS modules
ORM Prisma PostgreSQL driver
Database PostgreSQL Local in dev
Auth jsonwebtoken + bcryptjs HS256, 12 salt rounds
Payments Stripe PaymentIntents with manual capture
File uploads Multer (memoryStorage) + @azure/storage-blob Azure Blob Storage (evergrnuploads)
Input validation zod Schema validation middleware for all body-receiving routes
Rate limiting express-rate-limit Auth login + forgot-password endpoints
Web framework React 19 + Vite TailwindCSS v4
Mobile framework React Native 0.81.5 + Expo SDK 54 Hermes engine; iOS primary; Android support in progress (eas.json Android profiles added 2026-06-28, Google Play Developer account pending approval)
Mobile auth storage expo-secure-store Encrypted on device
Push notifications expo-notifications + expo-server-sdk Expo push token saved on User/Provider; sent from backend
Maps (web) Leaflet MapPicker component
Navigation (mobile) React Navigation v6 Stack + Bottom Tabs
Weather Open-Meteo + zippopotam.us Free, no API key; 1h in-memory cache per ZIP; zippopotam.us for ZIP→lat/lng geocoding
Observability Azure Application Insights SDK in server.js; auto-instruments Express, Prisma, Stripe
Document Intelligence @azure-rest/ai-document-intelligence prebuilt-idDocument (driver's license OCR) + prebuilt-read (insurance certificate OCR); src/config/docIntelligence.js

Database Schema

Models

User (customer accounts)

Field Type Notes
id String (cuid) PK
email String unique
password String bcrypt hash
name String
address String home address for job ZIP inference
zip String
lat / lng Float? optional map pin
stripeCustomerId String? set on first card save
stripePaymentMethodId String? saved card reference
stripeSubscriptionId String? set when customer starts premium subscription
isPremium Boolean default false; unlocks multi-address, favorites, pro notes, spend summary, seasonal alerts
premiumSince DateTime? set when subscription first activates
seasonalAlertsEnabled Boolean default false; premium-only opt-in for seasonal push alerts
pushToken String? Expo push token; registered on mobile login
createdAt / updatedAt DateTime

Relations: jobs[], payments[], reviews[], addresses UserAddress[], reports JobReport[], favoriteProviders FavoriteProvider[], proNotes ProNote[]

Provider (professional accounts)

Field Type Notes
id String (cuid) PK
email String unique
password String bcrypt hash
name String
phone String?
companyName String?
companyUrl String?
insuranceInfo String? Auto-populated by OCR from insCheck* fields; not editable from mobile (image-upload only)
insDocUrl String? Azure blob URL of uploaded insurance certificate image
insCheckStatus String? pending / active / expired / review / error
insCheckCarrier String? OCR-extracted carrier name
insCheckPolicy String? OCR-extracted policy number
insCheckEffective String? OCR-extracted effective date (ISO format)
insCheckExpiry String? OCR-extracted expiry date (ISO format)
insCheckCoverage String? OCR-extracted coverage amount
insCheckFlags String[] e.g. ['expired', 'no_policy', 'no_carrier', 'near_expiry']
insCheckAt DateTime? Timestamp of last OCR run
logoUrl String? Azure blob URL (https://evergrnuploads.blob.core.windows.net/logos/...)
headshots String[] Staff headshot blob URLs; required before bidding
idDocFront String? Driver's license front blob URL; required before bidding
idDocBack String? Driver's license back blob URL; required before bidding
idCheckStatus String? pending / passed / review / failed / error
idCheckName String? OCR-extracted full name
idCheckDOB String? OCR-extracted date of birth (ISO format)
idCheckExpiry String? OCR-extracted expiry date (ISO format)
idCheckState String? OCR-extracted issuing state/region
idCheckFlags String[] e.g. ['expired', 'near_expiry', 'no_dob', 'not_a_license']
idCheckAt DateTime? Timestamp of last OCR run
services String[] e.g. ['lawncare','snowplowing']
serviceZips String[] ZIP codes they serve
availability Json? Weekly schedule: { mon: { start, end, on }, ... }
isPremium Boolean default false β€” Preferred Pro member
premiumSince DateTime? when Preferred Pro was activated
premiumBilledMonth String? last month billed ("YYYY-MM") β€” prevents double-charging
preferredProAppliedAt DateTime? set when provider applies to Preferred Pro
pushToken String? Expo push token; registered on mobile login
createdAt / updatedAt DateTime

Relations: quotes[], jobs[], reviews[]

Job

Field Type Notes
id String (cuid) PK
customerId String FK β†’ User
providerId String? FK β†’ Provider; set on quote accept
addressId String? FK β†’ UserAddress; set if customer selects a saved address at booking
service String lawncare / snowplowing / handyman / home_cleaning
address String copied from customer address at creation
zip String used for provider matching
description String? customer's free-text description
preferredAt DateTime customer's requested time
isAsap Boolean default false; urgent jobs sort to top of provider feed + trigger URGENT push notification
recurringGroupId String? shared ID linking jobs in the same recurring series
lat / lng Float? optional
details Json? structured service-specific details
images String[] general job photos (blob URLs)
beforeImages String[] set when provider starts job (blob URLs)
afterImages String[] set when provider completes job (blob URLs)
status JobStatus see enum below
createdAt / updatedAt DateTime

Relations: customer, provider?, userAddress?, quotes[], messages[], payment?, review?, report JobReport?

Quote

Field Type Notes
id String (cuid) PK
jobId String FK β†’ Job
providerId String FK β†’ Provider
amount Float provider's net price
platformFeeRate Float 0.18 (18%)
platformFee Float amount Γ— 0.18
totalAmount Float amount + platformFee
scheduledAt DateTime? proposed service date/time
note String? optional message to customer
status QuoteStatus PENDING / ACCEPTED / REJECTED
createdAt DateTime

Message

Field Type Notes
id String (cuid) PK
jobId String FK β†’ Job
senderId String User.id or Provider.id
senderRole String "user" or "provider"
content String
createdAt DateTime

Review

Field Type Notes
id String (cuid) PK
jobId String unique β€” one review per job
customerId String FK β†’ User
providerId String FK β†’ Provider
rating Int 1–5
comment String?
createdAt DateTime

Payment

Field Type Notes
id String (cuid) PK
jobId String unique β€” one payment per job
customerId String FK β†’ User
amount Float quote totalAmount (platform take + provider net)
stripePaymentIntentId String? set on authorization at quote acceptance
tipAmount Float? set if customer adds a tip after job completion
stripeTipPaymentIntentId String? separate PaymentIntent for the tip charge
status PaymentStatus PENDING / PAID / FAILED
createdAt DateTime

Current behavior (pre-production): Stripe capture fires immediately on job completion. The Payment Release Flow (HELD / DISPUTED states) is designed but not yet implemented β€” see the "Payment Release Flow" section below.

UserAddress (saved addresses per customer)

Field Type Notes
id String (cuid) PK
userId String FK β†’ User
label String e.g. "Home", "Cabin" β€” default "Home"
address String street address
zip String
isPrimary Boolean only one per user; used as default for booking
lat / lng Float? optional map pin
streetViewPhoto String? blob URL β€” uploaded by customer for provider reference
serviceAreaPhotos String[] blob URLs β€” additional property photos (driveway, lawn, etc.)
drivewayLength Float? in feet β€” used by snowplow providers
lawnAcres Float? lawn size β€” used by lawncare providers
createdAt DateTime

JobReport (customer dispute, within 72h of completion)

Field Type Notes
id String (cuid) PK
jobId String unique β€” one report per job
customerId String FK β†’ User
description String customer's description of the problem
createdAt DateTime

72-hour window is enforced server-side using job.updatedAt at the time status was set to COMPLETED. One report per job (DB unique on jobId).

ProviderPayout [NOT YET IMPLEMENTED β€” required before go-live]

Internal ledger of what is owed to each provider per job after a successful capture. Created by the daily payout cron when a payment is released.

Field Type Notes
id String (cuid) PK
providerId String FK β†’ Provider
jobId String FK β†’ Job
paymentId String FK β†’ Payment
amount Float provider's net share = quote.amount (totalAmount minus 18% platform fee)
status ProviderPayoutStatus PENDING / PAID
batchDate DateTime the 8 AM batch run that released this payout
paidAt DateTime? set when manual ACH/Stripe transfer is confirmed
createdAt DateTime

Admin [GODMODE β€” remove before production]

Field Type Notes
id String (cuid) PK
email String unique
password String bcrypt hash
createdAt DateTime

FavoriteProvider (customer's saved providers β€” premium only)

Field Type Notes
id String (cuid) PK
userId String FK β†’ User
providerId String FK β†’ Provider
createdAt DateTime
Unique constraint: (userId, providerId)

ProNote (private customer note on a provider β€” premium only)

Field Type Notes
id String (cuid) PK
userId String FK β†’ User
providerId String FK β†’ Provider (not a DB relation β€” just stores the ID)
note String customer's private note text
updatedAt DateTime @updatedAt
Unique constraint: (userId, providerId) β€” one note per provider per customer

Notification (persistent in-app notification history)

Field Type Notes
id String (cuid) PK
recipientId String User.id or Provider.id
recipientType String "user" or "provider"
title String
body String
jobId String? optional link to a job
read Boolean default false
createdAt DateTime
Index: (recipientId, recipientType, read)

WaitlistEmail (coming-soon page signups)

Field Type Notes
id String (cuid) PK
email String unique
name String?
zip String?
role String? "customer" or "provider"
createdAt DateTime

Staff (internal support accounts)

Field Type Notes
id String (cuid) PK
email String unique
password String bcrypt hash
name String
role StaffRole see enum below
createdAt DateTime
Relations: elevationRequests[], assignedTickets[]

ElevationRequest (staff request to impersonate a user/provider account)

Field Type Notes
id String (cuid) PK
staffId String FK β†’ Staff
targetId String User.id or Provider.id being impersonated
targetRole String "user" or "provider"
targetName / targetEmail String snapshot at request time
reason String required justification
status ElevationStatus PENDING β†’ APPROVED β†’ ACTIVE β†’ DEACTIVATED; or REJECTED
requestedAt DateTime
approvedAt / approvedBy DateTime? / String? set when admin clicks email approve link
activatedAt DateTime? set when staff triggers impersonation token
deactivatedAt / deactivatedBy DateTime? / String? auto-set after 15 min of ACTIVE state
notes String? internal admin notes

SupportTicket

Field Type Notes
id String (cuid) PK
submitterId String User.id or Provider.id
submitterRole String "user" or "provider"
submitterName String snapshot at submission
subject String
body String
status TicketStatus OPEN β†’ IN_PROGRESS β†’ RESOLVED β†’ CLOSED
assignedToId String? FK β†’ Staff
createdAt / updatedAt DateTime
resolvedAt DateTime?
notes String? internal staff notes

AuditEvent (actions taken during an active elevation/impersonation session)

Field Type Notes
id String (cuid) PK
elevationId String FK β†’ ElevationRequest
method String HTTP method of the action
path String API path accessed
body String? request body (sanitized)
createdAt DateTime
Index: (elevationId) β€” every request made during an active elevation is logged by auditLog middleware

Enums

JobStatus:    PENDING β†’ QUOTED β†’ ACCEPTED β†’ ON_THE_WAY β†’ IN_PROGRESS β†’ COMPLETED
                                                                      β†’ CANCELLED
QuoteStatus:  PENDING β†’ ACCEPTED | REJECTED
PaymentStatus: PENDING β†’ PAID | FAILED
   (HELD and DISPUTED are designed for the Payment Release Flow β€” not yet in schema)

StaffRole:    customer_support | professional_support | technical_support | internal_audit

ElevationStatus: PENDING β†’ APPROVED β†’ ACTIVE β†’ DEACTIVATED
                         β†’ REJECTED

TicketStatus: OPEN β†’ IN_PROGRESS β†’ RESOLVED β†’ CLOSED

ProviderPayoutStatus [NOT YET IMPLEMENTED]:
              PENDING β†’ PAID

File Structure

evergrn/
β”œβ”€β”€ server.js                        Entry point β€” loads .env, starts Express on :3000
β”œβ”€β”€ package.json                     Backend dependencies
β”œβ”€β”€ .env                             Secrets (DATABASE_URL, JWT_SECRET, STRIPE_*)
β”œβ”€β”€ .env.example                     Template for new environments
β”‚
β”œβ”€β”€ prisma/
β”‚   └── schema.prisma                All Prisma models, enums, relations
β”‚
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ app.js                       Express setup: middleware, routes, static files
β”‚   β”œβ”€β”€ config/
β”‚   β”‚   β”œβ”€β”€ db.js                    PrismaClient singleton
β”‚   β”‚   β”œβ”€β”€ stripe.js                Stripe client singleton
β”‚   β”‚   β”œβ”€β”€ push.js                  Expo push notification sender (expo-server-sdk)
β”‚   β”‚   β”œβ”€β”€ blob.js                  Azure Blob Storage helpers (uploadBuffer, uploadFiles, makeUpload); createIfNotExists() called before every upload
β”‚   β”‚   β”œβ”€β”€ docIntelligence.js       Azure AI Document Intelligence: analyzeIdDocument() (prebuilt-idDocument) + analyzeInsuranceDocument() (prebuilt-read)
β”‚   β”‚   β”œβ”€β”€ notificationQueue.js     In-memory Map queue for admin-to-user banners (replace with Redis pre-launch)
β”‚   β”‚   β”œβ”€β”€ email.js                 ACS email templates + sendEmail() β€” quoteReceived, quoteAccepted, jobCompleted, waitlistConfirmation
β”‚   β”‚   └── mailer.js                ACS-backed sendPasswordReset() β€” used by /auth/forgot-password
β”‚   β”œβ”€β”€ middleware/
β”‚   β”‚   β”œβ”€β”€ auth.js                  authenticate() β€” JWT verification, sets req.user
β”‚   β”‚   β”œβ”€β”€ validate.js              validate(schema) β€” Zod middleware; returns 400 on invalid body
β”‚   β”‚   β”œβ”€β”€ auditLog.js              auditLog β€” globally mounted; logs all requests during active elevation sessions to AuditEvent
β”‚   β”‚   └── error.js                 Global error handler
β”‚   β”œβ”€β”€ schemas/
β”‚   β”‚   └── index.js                 18 Zod schemas for all body-receiving endpoints
β”‚   └── routes/
β”‚       β”œβ”€β”€ auth.js                  /auth/* β€” register + login for both roles + password reset
β”‚       β”œβ”€β”€ jobs.js                  /jobs/* β€” full job lifecycle, messages, images, report, tip, recurring
β”‚       β”œβ”€β”€ quotes.js                /quotes/* β€” quoting, acceptance, market rate
β”‚       β”œβ”€β”€ providers.js             /providers/me/* β€” profile, headshots, ID docs (OCR), insurance doc (OCR), logo, schedule, insights, smart bundles, availability, premium-insights, notifications, preferred-pro, support
β”‚       β”œβ”€β”€ payments.js              /payments/* β€” Stripe card save, setup intent, premium subscription
β”‚       β”œβ”€β”€ reviews.js               /reviews β€” post review after completion
β”‚       β”œβ”€β”€ users.js                 /users/me/* β€” profile, location, addresses, address photos, favorites, pro notes, spend summary, seasonal alerts, push token, notifications, support
β”‚       β”œβ”€β”€ services.js              /services/available β€” service lookup by ZIP
β”‚       β”œβ”€β”€ weather.js               /weather?zip= β€” 14-day forecast (Open-Meteo + zippopotam.us, 1h cache)
β”‚       β”œβ”€β”€ waitlist.js              /waitlist β€” coming-soon page signups, sends ACS confirmation email
β”‚       β”œβ”€β”€ webhooks.js              /webhooks β€” Stripe event handling (PaymentIntents + subscription lifecycle)
β”‚       β”œβ”€β”€ staff.js                 /staff/* β€” internal support portal (login, elevation, impersonation, tickets, audit)
β”‚       β”œβ”€β”€ driveTime.js             /drive-time β€” ETA + distance via OSRM (lat/lng or address string inputs)
β”‚       └── admin.js                 /admin/* β€” [GODMODE] dev tooling + notify endpoint
β”‚
β”œβ”€β”€ uploads/                         Legacy local disk β€” no longer used for new uploads
β”‚                                    (Azure Blob Storage: evergrnuploads.blob.core.windows.net)
β”‚
β”œβ”€β”€ client/                          React web frontend
β”‚   β”œβ”€β”€ vite.config.js               Vite + proxy config
β”‚   β”œβ”€β”€ package.json
β”‚   └── src/
β”‚       β”œβ”€β”€ main.jsx                 React 19 entry point
β”‚       β”œβ”€β”€ App.jsx                  Router + layout + ImpersonationBanner [GODMODE]
β”‚       β”œβ”€β”€ context/
β”‚       β”‚   └── AuthContext.jsx      Auth state, localStorage, JWT decode
β”‚       β”œβ”€β”€ components/
β”‚       β”‚   β”œβ”€β”€ Navbar.jsx           Top nav (hidden on landing + dashboards)
β”‚       β”‚   β”œβ”€β”€ EvergrnLogo.jsx      SVG logo
β”‚       β”‚   β”œβ”€β”€ MapPicker.jsx        Leaflet map for location selection
β”‚       β”‚   └── MessageThread.jsx    Job messaging UI
β”‚       └── pages/
β”‚           β”œβ”€β”€ Landing.jsx          Marketing homepage
β”‚           β”œβ”€β”€ Login.jsx            Customer login
β”‚           β”œβ”€β”€ Register.jsx         Customer registration
β”‚           β”œβ”€β”€ Dashboard.jsx        Customer main UI β€” jobs, quotes, details
β”‚           β”œβ”€β”€ ProviderPortal.jsx   Provider landing page
β”‚           β”œβ”€β”€ ProviderLogin.jsx    Provider login
β”‚           β”œβ”€β”€ ProviderRegister.jsx Provider registration
β”‚           β”œβ”€β”€ ProviderDashboard.jsx Provider UI β€” browse, quote, schedule, insights
β”‚           └── GodMode.jsx          [GODMODE] Admin impersonation dashboard
β”‚
β”œβ”€β”€ mobile/                          React Native mobile app
β”‚   β”œβ”€β”€ App.js                       SafeAreaProvider + AuthContext + RootNavigator
β”‚   β”œβ”€β”€ app.json                     Expo config (bundle ID, theme color, push notification plugin)
β”‚   β”œβ”€β”€ eas.json                     EAS Build profiles (development/simulator, device, preview, production)
β”‚   β”œβ”€β”€ package.json
β”‚   └── src/
β”‚       β”œβ”€β”€ api.js                   apiFetch() wrapper + BASE_URL (hardcoded to Azure App Service URL)
β”‚       β”œβ”€β”€ context/
β”‚       β”‚   └── AuthContext.js       SecureStore token storage + JWT parse + push token registration on login
β”‚       β”œβ”€β”€ components/
β”‚       β”‚   β”œβ”€β”€ MessageThread.js     Messaging UI (shared with web pattern)
β”‚       β”‚   β”œβ”€β”€ StarRating.js        5-star rating input/display
β”‚       β”‚   └── NotificationBanner.js Animated slide-in banner; persistent until user taps X
β”‚       β”œβ”€β”€ hooks/
β”‚       β”‚   └── useNotificationPoller.js 30s polling hook β€” checks server queue then job state changes
β”‚       β”œβ”€β”€ utils/
β”‚       β”‚   └── pushNotifications.js Requests permission, gets Expo push token, saves to backend
β”‚       β”œβ”€β”€ navigation/
β”‚       β”‚   β”œβ”€β”€ RootNavigator.js     Auth gate β†’ role-based navigator; mounts NotificationBanner overlay
β”‚       β”‚   β”œβ”€β”€ AuthNavigator.js     Stack: Landing β†’ Login/Register flows
β”‚       β”‚   β”œβ”€β”€ CustomerNavigator.js Bottom tabs for customer role
β”‚       β”‚   └── ProviderNavigator.js Bottom tabs for provider role
β”‚       └── screens/
β”‚           β”œβ”€β”€ auth/
β”‚           β”‚   β”œβ”€β”€ LandingScreen.js
β”‚           β”‚   β”œβ”€β”€ LoginScreen.js
β”‚           β”‚   β”œβ”€β”€ RegisterScreen.js
β”‚           β”‚   β”œβ”€β”€ ProviderLoginScreen.js
β”‚           β”‚   └── ProviderRegisterScreen.js
β”‚           β”œβ”€β”€ customer/
β”‚           β”‚   β”œβ”€β”€ HomeScreen.js       Browse + search
β”‚           β”‚   β”œβ”€β”€ BookScreen.js       Post a new job
β”‚           β”‚   β”œβ”€β”€ JobsScreen.js       My jobs list
β”‚           β”‚   β”œβ”€β”€ JobDetailScreen.js  Detail view + messaging
β”‚           β”‚   └── ProfileScreen.js    Account settings + Report a Problem (72h window)
β”‚           └── provider/
β”‚               β”œβ”€β”€ AvailableScreen.js          Job board + conflict filter
β”‚               β”œβ”€β”€ ActiveScreen.js             Assigned/active jobs
β”‚               β”œβ”€β”€ ScheduleScreen.js           Upcoming schedule view
β”‚               β”œβ”€β”€ ProviderJobDetailScreen.js  Quote submission + messaging
β”‚               └── ProviderProfileScreen.js    Profile editing
β”‚
β”œβ”€β”€ scripts/                          Dev/seed utilities (not for production)
β”‚   β”œβ”€β”€ seedAdmin.js                  Creates godmode@evergrn.com [GODMODE]
β”‚   β”œβ”€β”€ seedAvailableJobs.js          Populates test PENDING jobs
β”‚   β”œβ”€β”€ seedSchedule.js               Adds ACCEPTED jobs to a provider
β”‚   β”œβ”€β”€ seedConflicts.js              Seeds conflicting jobs for UI testing
β”‚   β”œβ”€β”€ seedMultiAddress.js           Creates multiaddress@test.com with 3 saved addresses
β”‚   β”œβ”€β”€ seedAlexJobs.js               Creates completed jobs for Alex (dispute workflow testing)
β”‚   └── reassignSchedule.js           Moves jobs between provider accounts
β”‚
└── current-documentation/            This folder

Naming Conventions

Database / Prisma

Backend (Node.js)

Web Frontend (React)

Mobile (React Native)

API Routes

File Uploads

All uploads go to Azure Blob Storage (evergrnuploads, Cool tier, Canada Central). Files are held in memory by multer and streamed directly to blob β€” nothing touches the container filesystem.

Container Path pattern Used by
jobs {jobId}-{timestamp}-{random}.{ext} Job photos, before/after workflow photos
logos {providerId}.{ext} Provider company logos (overwrites on update)
headshots {providerId}-{timestamp}.{ext} Provider staff headshots
id-docs {providerId}-front/back-{timestamp}.{ext} Provider ID verification docs; OCR runs in background after upload
insurance-docs {providerId}-{timestamp}.{ext} Provider insurance certificate images; OCR runs in background after upload
messages {jobId}-{timestamp}-{random}.{ext} Photo attachments sent in job message threads
addresses {addressId}-street/area-{timestamp}.{ext} Customer property photos

All returned URLs are full blob URLs: https://evergrnuploads.blob.core.windows.net/{container}/{blobName}

[GODMODE] Marker

All dev-only admin tooling is tagged with // [GODMODE] comments. To find every marked line:

grep -r "\[GODMODE\]" .

Files to delete entirely before production: src/routes/admin.js, client/src/pages/GodMode.jsx, scripts/seedAdmin.js

Platform Fee

Platform fee is 18% calculated at quote time:

platformFee   = quote.amount Γ— 0.18
totalAmount   = quote.amount + platformFee

The platformFeeRate is stored on each Quote record so historical rates are preserved if the rate ever changes.


Payment Release Flow [NOT YET IMPLEMENTED β€” required before go-live]

This section documents the intended production payment lifecycle. The current codebase captures Stripe payments immediately on job completion, which must be changed before launch.

Design goals

Payment lifecycle (state machine)

Quote accepted
    β”‚
    β–Ό
Stripe PaymentIntent created (capture_method: 'manual')
Payment.status = PENDING
    β”‚
Job marked COMPLETED by provider
    β”‚
    β–Ό
Payment.status = HELD
Payment.releaseAt = completedAt + 72 hours
    β”‚
    β”œβ”€β”€β”€ Customer files JobReport within 72h ──────────────────────────────┐
    β”‚                                                                       β”‚
    β”‚         8 AM daily cron runs                                          β”‚
    β”‚              β”‚                                                        β”‚
    β”‚    releaseAt <= today 8 AM?                                           β”‚
    β”‚         NO β†’ skip (will be picked up at tomorrow's 8 AM batch)       β”‚
    β”‚         YES β†’ JobReport exists?                                       β”‚
    β”‚                   YES ──────────────────────────────────────────────►│
    β”‚                   NO                                                  β”‚
    β”‚                    β”‚                                                  β–Ό
    β”‚           stripe.paymentIntents.capture()              Payment.status = DISPUTED
    β”‚           Payment.status = PAID                        ProviderPayout NOT created
    β”‚           ProviderPayout created (status=PENDING)      Flagged in GodMode for admin
    β”‚           batchDate = today 8 AM                       Admin resolves manually:
    β”‚                                                          - capture + create payout, OR
    β–Ό                                                          - refund customer
End

Batch payout timing rules

The cron runs once daily at 8:00 AM local server time.

A payment is included in a given day's batch if and only if:

Payment.releaseAt <= today at 08:00:00

If a job is completed at 10:00 AM on Monday, releaseAt = Thursday 10:00 AM. The Thursday 8 AM batch does not include it (10 AM > 8 AM). It is picked up by the Friday 8 AM batch. This means the effective hold is between 72 and ~96 hours depending on time of completion β€” always at least 72h, never more than 96h.

Completed at releaseAt Paid out at
Monday 6:00 AM Thursday 6:00 AM Thursday 8 AM βœ“ (6 AM < 8 AM)
Monday 10:00 AM Thursday 10:00 AM Friday 8 AM (10 AM > 8 AM)
Monday 11:59 PM Thursday 11:59 PM Friday 8 AM

Provider payout split

The Stripe PaymentIntent is for totalAmount (provider net + 18% platform fee). When the cron captures it:

totalAmount   = quote.totalAmount        β†’ charged to customer's card β†’ Evergrn bank account
providerShare = quote.amount             β†’ owed to provider (recorded in ProviderPayout)
platformShare = quote.platformFee        β†’ retained by Evergrn

The ProviderPayout record is created with status = PENDING. A human reviews the daily batch in GodMode and initiates the ACH transfer or Stripe Dashboard transfer to the provider. Once confirmed, ProviderPayout.status is set to PAID and paidAt is recorded.

What needs to be built (implementation checklist)

Files to create/modify

src/jobs/
└── payoutCron.js          node-cron job β€” runs at 8 AM daily

src/routes/
β”œβ”€β”€ jobs.js                remove capture calls; set HELD + releaseAt on completion
└── admin.js               add payout dashboard endpoints [GODMODE]

prisma/
└── schema.prisma          Payment (add releaseAt, new statuses); ProviderPayout model