Implementation Plan · Engineering

Survey Operations Platform
Comprehensive Build Plan

A staged engineering plan to build a professional-grade HR + Operations platform for a land surveying company — capable of competing with Jobber, ServiceTitan, BambooHR, and Gusto, built specifically for survey workflows. Includes a concrete sprint-by-sprint breakdown of Phase 1.

Phases
5
Staged rollout
MVP Timeline
6–8
Weeks to Phase 1 launch
User Roles
6
Crew → Owner
Full Build
~30
Weeks to competitive
01

Principles & the "Professional" Bar


Before a line of code, we agree on what "professional and competitive" means in practice. These become the non-negotiable acceptance bar for every screen — the difference between an internal tool and a product that rivals ServiceTitan.

Product

Mobile-first field, desktop-rich office

Crews work on phones in trucks with poor signal; the office works on large screens. The same app must excel at both, and tolerate going offline.

Data

One source of truth

Every piece of data lives in exactly one place and is referenced everywhere else. No duplicated job info, ever.

Security

Role-aware everywhere

A user only ever sees what their role permits — enforced on the server, reflected in the UI. Pay and financials are gated.

Craft

Four states on every screen

Loading (skeletons), empty (helpful guidance), error (recoverable), populated. Plus WCAG 2.1 AA accessibility and sub-second navigation.

Definition of Done (every feature): server enforces permissions · all four UI states · responsive mobile + desktop · unit + integration tests, E2E on critical paths · accessible (keyboard + contrast) · telemetry in place · documented in the API schema.
02

Technical Architecture


A typed, containerized, Python-native stack — chosen for speed of development today and the Phase 5 AI work tomorrow.

Next.js (App Router) · React · Tailwind · shadcn/ui TanStack Query · react-hook-form + zod · PWA FastAPI (Python) · REST + OpenAPI 3 SQLAlchemy 2.0 · Pydantic v2 · Alembic · Arq workers PostgreSQL system of record Redis queue + cache Supabase Auth JWT issuer Supabase Storage / S3 PDFs · photos · CAD HTTPS · typed client generated from OpenAPI

Typed boundary: the frontend client is generated from FastAPI's OpenAPI schema, so the two halves never drift.

Key decisions

ConcernDecisionWhy
API styleREST + OpenAPIAuto-generated schema & typed client; simpler than GraphQL for this team
Type safetyopenapi-typescript / orvalFrontend and backend can't drift; compile-time safety across the wire
ORM + migrationsSQLAlchemy 2.0 + AlembicExplicit, version-controlled schema changes
Server state (FE)TanStack QueryCaching, optimistic updates, background refetch
Formsreact-hook-form + zodPerformant, type-safe, schemas shared with validation
AuthSupabase issues JWT; FastAPI verifiesOffload password/session security; keep authorization in our API
AuthorizationCentral RBAC dependency in FastAPIOne enforcement point, fully testable
Background jobsArq (async) + RedisNotifications, payroll exports, AI processing, scheduled tasks
FilesSigned URLs to Storage/S3Never proxy large files through the API — scalable & cheap
SearchPostgres FTS → Meilisearch laterDon't over-build; upgrade when Phase 5 "intelligent search" demands it
03

Core Data Model


Jobs are the central entity; everything else hangs off identity, operations, and the ledger of time/assets/money.

Identity & HR

People & access

  • users, roles, user_roles
  • employees, pay_records (restricted)
  • certifications → expiry alerts
  • pto_balances, pto_requests
Operations

The work itself

  • clients
  • jobs (central), job_assignments
  • documents, job_notes
  • field_reports, checklists
Time · Assets · Money

The ledger

  • timecards, equipment, vehicles
  • invoices, job_costs
  • audit_log (immutable)
  • notifications, announcements
Modeling rules: soft-delete business entities (never hard-delete job/financial records) · every sensitive mutation writes to audit_log · money in integer cents · all timestamps UTC, displayed in the user's timezone.
04

Phased Roadmap


Each phase ships real, deployable value before the next begins. We start with the highest-pain problems.

Weeks 1–2

Sprint 0 — Foundations

Repo, environments, CI/CD, design tokens, auth/RBAC skeleton, observability. No user features — the spine everything hangs on.

monorepodockergitlab-cistorybook
Weeks 3–8

Phase 1 — MVP: Stop the Bleeding

Replace PDF email distribution; one home for job info. Auth, jobs, documents, notes, search. Detailed below in §05.

jobs CRUDdocumentsnotesRBAC
+4–6 weeks

Phase 2 — Field Operations

Mobile PWA, offline-tolerant photo & report capture, GPS, custom safety checklists, push notifications.

PWAoffline syncGPSchecklists
+5–7 weeks

Phase 3 — Admin Automation + HR Core

Clock in/out, timecards, PTO, My Pay portal, equipment/vehicle inventory, certification expiry alerts, payroll export.

timecardsPTOpayroll exportlicenses
+3–4 weeks

Phase 4 — Business Intelligence

Executive dashboard, visual dispatch board, job costing, utilization analytics, historical search, client portal.

exec dashboarddispatch boardjob costingclient portal
Ongoing

Phase 5 — AI & Automation

AI PDF extraction, semantic search, voice-to-report, document summaries, scheduling assist — built on the latest Claude models via the Python backend.

PDF extractionvoice-to-reportsemantic search
05

Phase 1 — Sprint-by-Sprint Breakdown


Four two-week sprints take us from an empty repo to a usable MVP in users' hands. Each sprint is independently deployable to staging and ends in a demoable milestone. Tasks are grouped by track so backend, frontend, and platform work can run in parallel.

Sprint 0

Foundations & Infrastructure

Weeks 1–2

Goal: a deployable skeleton — repo, environments, CI, design tokens — so every later sprint is fast.

Backend & Database
  • Scaffold monorepo: apps/web, apps/api, packages/
  • FastAPI skeleton: health endpoint, settings via pydantic-settings, structured logging
  • Docker Compose local stack: Postgres + Redis + API + Web with hot reload
  • SQLAlchemy 2.0 base + Alembic init; first migration (users, roles, user_roles)
  • Ruff + Black + mypy; pre-commit hooks
Frontend & Design
  • Next.js App Router scaffold; Tailwind + shadcn/ui initialized
  • Encode design tokens (color, type, spacing, radius) in tailwind.config.ts
  • Storybook stood up; base components: Button, Input, Card, Badge, Skeleton, EmptyState
  • Responsive app-shell skeleton: sidebar (desktop) / bottom-nav (mobile), top bar
Platform & DevOps
  • GitLab CI pipeline: lint → type-check → test → build → Docker image
  • Dockerfiles; staging deploy via Dokploy on the VPS, auto-TLS reverse proxy
  • Sentry error tracking + uptime check wired into both apps
  • Testing harness: pytest + factories, Vitest + Testing Library, Playwright skeleton
Demo / Done: docker compose up runs the full stack locally; a stub login page is deployed to staging; CI is green on every MR.
Sprint 1

Identity, Auth & RBAC

Weeks 3–4

Goal: secure login, roles, and a role-guarded shell — the security spine before any business data exists.

Backend & Database
  • Supabase Auth integration; verify JWT on every request via a FastAPI dependency
  • Resolve user → roles → permissions; seed the six roles
  • Central require_permission() dependency guarding endpoints
  • employees model + read endpoints; audit_log write infrastructure
apps/api/auth/rbac.py — one enforcement point for the whole API
def require_permission(permission: Permission):
    # FastAPI dependency: resolves the JWT, loads the user's
    # roles, and rejects the request if the grant is missing.
    async def _guard(user: User = Depends(current_user)) -> User:
        if permission not in user.permissions:
            raise HTTPException(status_code=403,
                                detail="forbidden")
        return user
    return _guard

@router.get("/jobs", dependencies=[Depends(
    require_permission(Permission.JOBS_READ))])
async def list_jobs(): ...
Frontend & Design
  • Auth flow: login, logout, password reset, silent session refresh
  • Route guards + a <Can permission="…"> gate to hide unauthorized UI
  • User menu, notification bell stub, ⌘K command palette shell
  • Employee directory: list + read-only profile (first real data screen)
Demo / Done: a user logs in and sees a role-appropriate empty shell; hitting an unauthorized route or API endpoint is blocked at both layers; the RBAC test matrix (role × resource) passes.
Sprint 2

Jobs Core — the Central Entity

Weeks 5–6

Goal: create, assign, and track jobs through their status lifecycle — the heart of the platform.

Backend & Database
  • clients, jobs, job_assignments models + migration
  • Jobs CRUD API with pagination, filtering, sorting; RBAC-scoped (crew sees only their jobs)
  • Status workflow with enforced transitions; every change written to audit_log
  • Geocode the site address on create for later map use
apps/api/jobs/status.py — enforce the lifecycle, don't trust the client
# Assigned → In Progress → Waiting → Completed
ALLOWED: dict[Status, set[Status]] = {
    Status.ASSIGNED:    {Status.IN_PROGRESS},
    Status.IN_PROGRESS: {Status.WAITING, Status.COMPLETED},
    Status.WAITING:     {Status.IN_PROGRESS},
    Status.COMPLETED:   set(),
}

def transition(job: Job, target: Status) -> Job:
    if target not in ALLOWED[job.status]:
        raise InvalidTransition(job.status, target)
    job.status = target
    return job
Frontend & Design
  • Job dashboard: filterable/sortable data table with status badges; per-role default views
  • Job create/edit forms (react-hook-form + zod), client picker, crew assignment
  • Job detail page: the canonical record everything else attaches to
  • All four UI states (loading skeletons, empty, error, populated) on every screen
Demo / Done: a PM creates a job, assigns a crew, and moves it through its statuses; a field-crew login sees only their assigned jobs and cannot edit others'.
Sprint 3

Documents, Notes, Search & MVP Polish

Weeks 7–8

Goal: kill PDF-by-email, capture field knowledge, and harden the MVP for a real pilot.

Backend & Database
  • File pipeline: signed-URL upload to Supabase Storage, versioning, thumbnails for images
  • documents model tied to jobs (PDF / CAD / photo / permit) with version history
  • job_notes (immutable, timestamped, attachments) + announcements
  • Postgres full-text search across jobs, notes, and documents
Frontend & Design
  • Per-job document repository: upload, version list, download; drag-and-drop
  • Job notes thread with attachments; company announcements feed on the dashboard
  • Global search bar (⌘K) wired to the FTS endpoint
  • Accessibility pass (keyboard + contrast) and Lighthouse mobile budget ≥ 90
Platform & Launch Prep
  • Playwright E2E for critical paths: login, create job, upload doc, search
  • Seed/import existing jobs + clients; validate counts and spot-check
  • UAT on staging with one user per role; triage and fix
  • One-page role guides + pilot rollout plan
Demo / Done: a crew member opens the app, finds the current job packet, reads the latest notes, and searches a past job — with zero phone calls to the office. MVP is pilot-ready.
Parallelization note: with two engineers, one owns the backend/data track and one owns the frontend/design track each sprint, syncing on the typed API client. Sprint 0 is the only sprint where both work the same setup tasks together.
06

SOLID Principles Compliance


How the architecture is structured to stay maintainable as it grows from MVP to a five-module platform.

SSingle Responsibility

Each layer has one reason to change: SQLAlchemy models own persistence, Pydantic schemas own the API contract, service modules own business rules (e.g. jobs/status.py only governs transitions), and React components own presentation. A pay-rate change never touches job code.

OOpen / Closed

New features extend rather than modify. Adding Phase 3's PTO module means new routers, models, and permissions — the auth and RBAC core is untouched. New roles are added to a registry, not by editing every endpoint.

LLiskov Substitution

Storage is accessed through one interface, so Supabase Storage and Amazon S3 are interchangeable behind a common abstraction — the VPS-to-AWS migration swaps an implementation without changing callers. The same holds for the notification channel (email / push).

IInterface Segregation

The OpenAPI surface is split per domain (jobs, documents, HR, payroll) and the generated TS client mirrors it, so the field-crew app imports only the job/document endpoints it uses — never the payroll surface. Role-scoped schemas keep clients lean.

DDependency Inversion

FastAPI's dependency injection means high-level handlers depend on abstractions (a Repository, an AuthProvider, a FileStore), not concretes. Tests inject fakes; production injects Postgres/Supabase/S3 — the business logic doesn't know the difference.

07

Security & Cross-Cutting Concerns


You'll hold pay rates, possibly SSNs/EINs for tax forms, and PLS license data. Treat it accordingly — built in continuously, not as a phase.

Two-layer authorization

The server is the source of truth; UI gating is convenience only. Consider Postgres Row-Level Security for defense in depth on the most sensitive tables.

Encryption & secrets

TLS in transit, encryption at rest; tokenize SSN/EIN. Secrets are env-based, rotated, scoped per environment, never committed.

Audit & backups

Immutable audit log on sensitive access; automated nightly offsite backups with tested restores. A backup you've never restored is a guess.

Accessibility & performance

WCAG 2.1 AA with axe checks in CI; code-splitting, query caching, DB indexing, paginate everything. Lighthouse mobile ≥ 90.

Tax-form handling

Strongly prefer linking out to a payroll provider over storing W-2s/1099s yourself; if you must store them, isolate and encrypt.

Supply chain

Automated CVE scanning in CI; Renovate/Dependabot for updates; an internal security review of auth + RBAC + file access before go-live.

08

Risks & Mitigations


Risk
Sev
Mitigation
Scope creep beyond the MVP delays launch and dilutes value.
HIGH
Frozen Phase-1 scope; everything else deferred to a named later phase. The §05 sprint plan is the contract.
Low field adoption — the platform goes unused and ROI never lands.
HIGH
Mobile-first, offline-tolerant (Phase 2), a pilot crew, one-page role guides, and a champion inside the company.
Breach of sensitive pay/tax data.
HIGH
Two-layer RBAC, encryption, audit logs, pre-launch pen-test; consider linking out tax forms entirely.
Poor field connectivity loses reports and frustrates crews.
MED
Offline queue + sync from Phase 2; optimistic UI that reconciles on reconnect.
Migration data quality — garbage-in erodes trust on day one.
MED
Validate counts, spot-check, and run new + old in parallel before a hard cutover (Sprint 3).
Solo-developer bus factor stalls the project.
LOW
Docs, conventions, tests, and CI from Sprint 0 so anyone can pick it up.