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:
| Component | Purpose | Size |
|---|---|---|
| ShiftEntryGrid.vue | Main shift entry interface | 26KB |
| ShiftDetailModal.vue | Detailed shift editing | 72KB |
| LESPdfUpload.vue | LES file upload interface | - |
| PayPeriodTabs.vue | Pay period navigation | - |
| SummaryCards.vue | Aggregated 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 functionsBackend Components
The FastAPI backend is organized into functional modules:
| Module | Purpose | Files |
|---|---|---|
| api/ | REST API endpoints | 12 modules |
| pdf_parser/ | LES PDF parsing | 12 modules |
| verification/ | Pay verification | 5 modules |
| payperiod/ | Pay period logic | 4 modules |
| scripts/ | Utility scripts | Various |
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 mathData 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:
| Extractor | Responsible For |
|---|---|
| PayPeriodExtractor | Metadata (dates, member info, rates) |
| EarningsExtractor | Earnings table (description, hours, amount) |
| DeductionsExtractor | Deductions table (description, amount) |
| PaidByGovtExtractor | Government-paid benefits |
| LeaveExtractor | Leave 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:
- Write SQL migration file
- Test locally with
supabase db reset - Apply to production with
supabase db push - 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.tomlautomatically - Uses
uvfor 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 = 10Vercel 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.