PayChecker
Architecture

PayChecker Architecture

This document provides detailed architectural information about the PayChecker system, including component design, data flow, and technical implementation details.

System Architecture

High-Level Architecture

┌─────────────────────────────────────────────────────────┐
│                     Frontend Layer                       │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐  │
│  │ Vue.js SPA   │  │   Axios      │  │  Components  │  │
│  │ (Vite Build) │  │  API Client  │  │   (27 Vue)   │  │
│  └──────────────┘  └──────────────┘  └──────────────┘  │
│         Deployed on Vercel (Global CDN)                 │
└───────────────────────┬─────────────────────────────────┘
                        │ HTTPS/REST

┌─────────────────────────────────────────────────────────┐
│                     Backend Layer                        │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐  │
│  │   FastAPI    │  │   Pydantic   │  │  pdfplumber  │  │
│  │   Routes     │  │  Validation  │  │  PDF Parser  │  │
│  └──────────────┘  └──────────────┘  └──────────────┘  │
│         Deployed on Railway (Docker Container)          │
└───────────────────────┬─────────────────────────────────┘
                        │ PostgreSQL (SSL)

┌─────────────────────────────────────────────────────────┐
│                      Data Layer                          │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐  │
│  │  PostgreSQL  │  │     RLS      │  │    Auth0     │  │
│  │  (Supabase)  │  │   Policies   │  │     JWT      │  │
│  └──────────────┘  └──────────────┘  └──────────────┘  │
│         Hosted on Supabase Cloud (US East)              │
└─────────────────────────────────────────────────────────┘

Component Architecture

Frontend Components

The Vue.js frontend is built using the Composition API with a component-based architecture:

ComponentPurposeSize
ShiftEntryGrid.vueMain shift entry interface26KB
ShiftDetailModal.vueDetailed shift editing72KB
LESPdfUpload.vueLES file upload interface-
PayPeriodTabs.vuePay period navigation-
SummaryCards.vueAggregated data display-
(22 additional components)Various UI elements-

Component Structure:

frontend/src/
├── components/
│   ├── ShiftEntryGrid.vue      # Main grid for shift entry
│   ├── ShiftDetailModal.vue    # Detailed shift form
│   ├── LESPdfUpload.vue        # PDF upload interface
│   ├── PayPeriodTabs.vue       # Period navigation
│   ├── SummaryCards.vue        # Data summaries
│   └── ... (22 more)
├── services/
│   └── api.ts                  # Axios API client
└── utils/
    └── helpers.ts              # Utility functions

Backend Components

The FastAPI backend is organized into functional modules:

ModulePurposeFiles
api/REST API endpoints12 modules
pdf_parser/LES PDF parsing12 modules
verification/Pay verification5 modules
payperiod/Pay period logic4 modules
scripts/Utility scriptsVarious

Backend Structure:

pay_checker/
├── api/
│   ├── main.py                 # FastAPI app
│   ├── auth/                   # Auth endpoints
│   ├── shifts/                 # Shift CRUD
│   ├── premium_time/           # OJTI/CIC/TOS/TNW
│   ├── pay_periods/            # Pay period endpoints
│   └── les_parser/             # Parser endpoints
├── pdf_parser/
│   ├── parser.py               # Main parser
│   ├── extractors/
│   │   ├── pay_period.py       # Metadata extraction
│   │   ├── earnings.py         # Earnings table
│   │   ├── deductions.py       # Deductions table
│   │   ├── paid_by_govt.py     # Govt-paid benefits
│   │   └── leave.py            # Leave balances
│   └── models.py               # Pydantic models
├── verification/
│   └── calculator.py           # Pay verification engine
└── payperiod/
    └── calculations.py         # Pay period math

Data Flow Architecture

LES PDF Parsing Flow

┌──────────────┐
│  User        │
│  Uploads     │
│  LES PDF     │
└──────┬───────┘


┌──────────────────────────────────────────┐
│  POST /api/parse                         │
│  ┌────────────────────────────────────┐  │
│  │ 1. Validate PDF format             │  │
│  │ 2. Extract text with coordinates   │  │
│  │ 3. Parse sections via extractors   │  │
│  │ 4. Validate with Pydantic models   │  │
│  │ 5. Store in pay.les_pay_periods    │  │
│  └────────────────────────────────────┘  │
└──────────────────────────────────────────┘


┌──────────────────────────────────────────┐
│  Supabase Database (PostgreSQL)          │
│  ┌────────────────────────────────────┐  │
│  │ pay.les_pay_periods                │  │
│  │ pay.les_earnings                   │  │
│  │ pay.les_deductions                 │  │
│  │ pay.les_paid_by_govt               │  │
│  │ pay.les_leave_balances             │  │
│  │ pay.les_remarks                    │  │
│  └────────────────────────────────────┘  │
└──────────────────────────────────────────┘


┌──────────────┐
│  User sees   │
│  parsed data │
│  in UI       │
└──────────────┘

Shift Tracking Flow

┌──────────────┐
│  User        │
│  Creates     │
│  Shift       │
└──────┬───────┘


┌──────────────────────────────────────────┐
│  POST /api/v1/shifts                     │
│  ┌────────────────────────────────────┐  │
│  │ 1. Validate shift data             │  │
│  │ 2. Check authorization (RLS)       │  │
│  │ 3. Insert into pay.shifts          │  │
│  │ 4. Auto-create TOS/TNW entries     │  │
│  │    (if shift_type requires)        │  │
│  └────────────────────────────────────┘  │
└──────────────────────────────────────────┘


┌──────────────────────────────────────────┐
│  Database with Cascade Rules             │
│  ┌────────────────────────────────────┐  │
│  │ pay.shifts (parent)                │  │
│  │   ├── pay.ojti_time (child)        │  │
│  │   ├── pay.cic_time (child)         │  │
│  │   └── pay.tos_tnw (child)          │  │
│  │                                    │  │
│  │ Cascade on DELETE/UPDATE           │  │
│  └────────────────────────────────────┘  │
└──────────────────────────────────────────┘


┌──────────────────────────────────────────┐
│  GET /api/v1/shifts/aggregated           │
│  ┌────────────────────────────────────┐  │
│  │ Returns shifts with nested:        │  │
│  │   - ojti_time[]                    │  │
│  │   - cic_time[]                     │  │
│  │   - tos_tnw[]                      │  │
│  └────────────────────────────────────┘  │
└──────────────────────────────────────────┘


┌──────────────┐
│  Vue.js Grid │
│  Displays    │
│  Data        │
└──────────────┘

Pay Verification Flow

┌──────────────┐
│  User        │
│  Requests    │
│  Verify      │
└──────┬───────┘


┌──────────────────────────────────────────┐
│  POST /api/verify                        │
│  ┌────────────────────────────────────┐  │
│  │ 1. Load LES data for period        │  │
│  │ 2. Load shift data for period      │  │
│  │ 3. Calculate expected pay          │  │
│  │    - Base: 80 hrs × hourly_rate    │  │
│  │    - Premiums: OJTI, CIC, OT       │  │
│  │ 4. Compare to actual pay           │  │
│  │ 5. Identify discrepancies          │  │
│  └────────────────────────────────────┘  │
└──────────────────────────────────────────┘


┌──────────────┐
│  User sees   │
│  verification│
│  results     │
└──────────────┘

PDF Parser Architecture

Coordinate-Based Extraction

The LES parser uses a coordinate-based approach rather than regex on raw text:

Parser Architecture:

┌─────────────────────────────────────────┐
│  pdfplumber                             │
│  Extract words with (x, y) coordinates  │
└──────────────────┬──────────────────────┘


┌─────────────────────────────────────────┐
│  Define column boundaries for sections  │
│  (based on LES template layout)         │
└──────────────────┬──────────────────────┘


┌─────────────────────────────────────────┐
│  Group words into rows by y-coordinate  │
└──────────────────┬──────────────────────┘


┌─────────────────────────────────────────┐
│  Extract values from specific columns   │
└──────────────────┬──────────────────────┘


┌─────────────────────────────────────────┐
│  Validate with Pydantic models          │
└─────────────────────────────────────────┘

Extractor Pattern:

Each LES section has a dedicated extractor class:

ExtractorResponsible For
PayPeriodExtractorMetadata (dates, member info, rates)
EarningsExtractorEarnings table (description, hours, amount)
DeductionsExtractorDeductions table (description, amount)
PaidByGovtExtractorGovernment-paid benefits
LeaveExtractorLeave balances (annual, sick, etc.)

Benefits:

  • More accurate than regex on raw text
  • Handles multi-line values correctly
  • Resilient to minor layout changes
  • Precise column extraction

Database Architecture

Two-Schema Design

┌─────────────────────────────────────────────────────┐
│                  PostgreSQL Database                │
│                                                     │
│  ┌───────────────────────────────────────────────┐ │
│  │  public schema (Platform - READ-ONLY)         │ │
│  │  ┌──────────────────────────────────────────┐ │ │
│  │  │ public.members                           │ │ │
│  │  │ public.facilities                        │ │ │
│  │  │ public.positions                         │ │ │
│  │  │ public.regions                           │ │ │
│  │  │ public.grants                            │ │ │
│  │  └──────────────────────────────────────────┘ │ │
│  └───────────────────────────────────────────────┘ │
│                          │                          │
│                          │ FK References            │
│                          ↓                          │
│  ┌───────────────────────────────────────────────┐ │
│  │  pay schema (PayChecker - READ-WRITE)         │ │
│  │  ┌──────────────────────────────────────────┐ │ │
│  │  │ pay.pay_periods                          │ │ │
│  │  │ pay.les_pay_periods → public.members     │ │ │
│  │  │ pay.shifts → public.members              │ │ │
│  │  │ pay.ojti_time → pay.shifts               │ │ │
│  │  │ pay.cic_time → pay.shifts                │ │ │
│  │  │ pay.tos_tnw → pay.shifts                 │ │ │
│  │  └──────────────────────────────────────────┘ │ │
│  └───────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘

Row-Level Security (RLS)

-- Example RLS Policy: Users can only view their own shifts
CREATE POLICY "users_view_own_shifts"
ON pay.shifts FOR SELECT
USING (
    membernumber = (
        SELECT membernumber FROM public.members
        WHERE auth0_user_id = auth.jwt() ->> 'sub'
    )
);
 
-- Example RLS Policy: FacReps can view facility members
CREATE POLICY "facreps_view_facility_members"
ON public.members FOR SELECT
USING (
    EXISTS (
        SELECT 1 FROM public.positions p
        JOIN public.members m ON m.membernumber = p.membernumber
        WHERE m.auth0_user_id = auth.jwt() ->> 'sub'
          AND p.positioncode IN ('VP', 'FacRep')
          AND p.facilitycode = members.facilitycode
    )
);

Database Migrations

Migration System: Supabase migrations (SQL-based)

supabase/migrations/
├── 20240101000000_initial_schema.sql
├── 20240102000000_add_shifts_table.sql
├── 20240103000000_add_premium_time_tables.sql
├── ...
└── 20251108000002_update_tos_subtypes.sql (latest)

Migration Process:

  1. Write SQL migration file
  2. Test locally with supabase db reset
  3. Apply to production with supabase db push
  4. Automatic rollback on failure

Authentication & Authorization

Authentication Flow (Auth0)

┌──────────────┐
│  User        │
│  Logs In     │
└──────┬───────┘


┌──────────────────────────────────────────┐
│  Auth0 (OAuth 2.0 / OpenID Connect)      │
│  ┌────────────────────────────────────┐  │
│  │ 1. Verify credentials              │  │
│  │ 2. Issue JWT token                 │  │
│  │ 3. Return token to client          │  │
│  └────────────────────────────────────┘  │
└──────────────────────────────────────────┘


┌──────────────────────────────────────────┐
│  Vue.js App stores JWT                   │
│  (memory or localStorage)                │
└──────────────────────────────────────────┘


┌──────────────────────────────────────────┐
│  API Request with Authorization header   │
│  Authorization: Bearer <jwt-token>       │
└──────────────────────────────────────────┘


┌──────────────────────────────────────────┐
│  FastAPI validates JWT                   │
│  ┌────────────────────────────────────┐  │
│  │ 1. Verify signature with Auth0     │  │
│  │ 2. Extract user claims (sub)       │  │
│  │ 3. Look up member number           │  │
│  │ 4. Set context for RLS             │  │
│  └────────────────────────────────────┘  │
└──────────────────────────────────────────┘


┌──────────────┐
│  Database    │
│  enforces    │
│  RLS         │
└──────────────┘

Development Mode

For local development, authentication can be bypassed:

# Development mode: Use X-Dev-Member-Number header
if ENVIRONMENT == "development":
    member_number = request.headers.get("X-Dev-Member-Number", "35973")
else:
    # Production: Validate JWT token
    member_number = get_member_from_jwt(token)

Shift Type Handling

State Machine for Shift Types

┌─────────────────────────────────────────────────────┐
│                  Shift Type States                  │
├─────────────────────────────────────────────────────┤
│                                                     │
│  REGULAR                                            │
│  ├─ Can have: OJTI, CIC, TOS, TNW entries          │
│  └─ No automatic entries created                   │
│                                                     │
│  TOS (Time Off Station)                             │
│  ├─ Auto-creates: Full-shift TOS entry             │
│  ├─ Can have: OJTI, CIC entries                    │
│  └─ Cannot have: Additional TOS, TNW entries       │
│                                                     │
│  TNW (Time Not Worked)                              │
│  ├─ Auto-creates: Full-shift TNW entry             │
│  ├─ Cannot have: OJTI, CIC, TOS entries            │
│  └─ Premium time disabled                          │
│                                                     │
│  RDO (Regular Day Off)                              │
│  └─ Represented by absence of shift record         │
│                                                     │
└─────────────────────────────────────────────────────┘

Cascade Delete Requirements

-- When shift deleted → cascade to all premium time
ALTER TABLE pay.ojti_time
  ADD CONSTRAINT fk_shift
  FOREIGN KEY (shift_id)
  REFERENCES pay.shifts(id)
  ON DELETE CASCADE;
 
-- When shift type changes to TNW → delete all premium time
-- Handled in application logic (FastAPI)

Deployment Architecture

CI/CD Pipeline

┌──────────────┐
│  Developer   │
│  git push    │
└──────┬───────┘

       ├──────────────────────────────────┐
       │                                  │
       ↓                                  ↓
┌──────────────────┐            ┌──────────────────┐
│  Vercel          │            │  Railway         │
│  ┌────────────┐  │            │  ┌────────────┐  │
│  │ 1. Clone   │  │            │  │ 1. Clone   │  │
│  │ 2. Build   │  │            │  │ 2. Build   │  │
│  │ 3. Deploy  │  │            │  │ 3. Deploy  │  │
│  └────────────┘  │            │  └────────────┘  │
│  Frontend SPA    │            │  Backend API     │
└──────────────────┘            └──────────────────┘
       │                                  │
       └──────────────┬───────────────────┘

             ┌──────────────────┐
             │  Supabase Cloud  │
             │  PostgreSQL      │
             └──────────────────┘

Railway Container Build

Railpack Builder:

  • Detects pyproject.toml automatically
  • Uses uv for fast dependency installation
  • Caches dependencies aggressively
  • Smaller container images
  • Faster builds

railway.toml:

[build]
builder = "RAILPACK"
 
[deploy]
startCommand = "uvicorn pay_checker.api.main:app --host 0.0.0.0 --port $PORT"
healthcheckPath = "/health"
healthcheckTimeout = 30
restartPolicyType = "ON_FAILURE"
restartPolicyMaxRetries = 10

Vercel SPA Deployment

vercel.json:

{
  "rewrites": [
    { "source": "/(.*)", "destination": "/index.html" }
  ],
  "headers": [
    {
      "source": "/assets/(.*)",
      "headers": [
        {
          "key": "Cache-Control",
          "value": "public, max-age=31536000, immutable"
        }
      ]
    }
  ]
}

Technology Decisions

Why FastAPI?

  • Modern async Python framework
  • Auto-generated OpenAPI docs (Swagger)
  • Pydantic validation built-in
  • Type hints throughout
  • Fast performance (ASGI server)

Why Vue.js 3?

  • Composition API for better code organization
  • Excellent TypeScript support
  • Smaller bundle size than React
  • Reactive state management
  • Strong ecosystem (Vite, Pinia)

Why Supabase?

  • PostgreSQL with built-in Auth (Auth0 compatible)
  • Row-Level Security (RLS) policies
  • Real-time subscriptions (future)
  • Free tier suitable for development
  • Easy migration path to self-hosted

Why pdfplumber?

  • Coordinate-based extraction (more accurate)
  • Handles multi-line values correctly
  • Resilient to minor layout changes
  • Pure Python (no external dependencies)
  • Active maintenance

Why Railway?

  • Simpler than AWS/GCP for small apps
  • Automatic HTTPS
  • Good Python support (Railpack)
  • $5/month free credit
  • Easy environment variable management

Performance Considerations

Backend Optimizations

  • Database connection pooling
  • Async/await for I/O operations
  • Pydantic model caching
  • PDF parsing in background tasks (future)

Frontend Optimizations

  • Vite for fast builds
  • Code splitting (dynamic imports)
  • Asset optimization (images, fonts)
  • CDN delivery via Vercel
  • Lazy loading components

Database Optimizations

  • Indexes on frequently queried columns
  • Materialized views for aggregations (future)
  • Partitioning for large tables (future)
  • Connection pooling (pgBouncer)

Related Documentation


This architecture supports ~100 concurrent users with room to scale. For deployment instructions, see the Setup & Configuration guide.