HaulPass is a farmer-first grain hauling efficiency application designed to reduce wait times at grain elevators by collecting farmer-side data on routes, geolocations, observations, and time tracking. Unlike traditional elevator-side scheduling software, HaulPass starts with farmer data collection to build trust and demonstrate value before enabling elevator scheduling capabilities.
Core Value Proposition: Help farmers make data-driven decisions about when to haul grain, reducing wait times and improving operational efficiency through real-time queue intelligence and predictive analytics.
Grain elevators are hesitant to deploy scheduling software because if a farmer doesn’t show up on time, it could ruin the entire day’s schedule. Therefore, HaulPass approaches the problem from the farmer’s side:
The farmer opens HaulPass and sees:
Required on signup:
Complete grain hauling cycle tracking with:
Screen 1: Name
Screen 2: Email + Password
Screen 3: Farm Name
Screen 4: Binyard Name
Screen 5: Grain Truck Name/Number
Screen 6: Grain Capacity (kg/lbs) - with "Skip" option
Screen 7: Search and Select Favorite Elevator
User Story: Steve opens HaulPass while drinking coffee at 6am to decide if he should start hauling.
Action: Steve taps “Haul Grain”
Display: Grain Type Selector
Options: Dropdown menu OR Icon grid
Grains: Wheat, Canola, Barley, Oats, Soybeans, etc.
Action: Select grain type → Tap “Begin Loading”
Timer starts (shows minutes only)
Display: "Loading [Grain Type]"
Background: GPS tracking position
Color: Green if < average, Red if > average
When auger shuts off:
Action: Tap “Weight Loaded”
Display: Weight input screen
Field: [ ] kg [Toggle kg/lbs]
Buttons: [Submit] [Skip]
On Submit/Skip:
Action: Automatically shows “Begin Haul to [Elevator Name]”
Button: "Begin Haul to Prairie Grain Co-op"
Tap: Start drive timer
Display:
- Timer (minutes only)
- Average time to elevator: 14m
- Current time: 12m (GREEN)
- Distance tracking in background
Trigger: GPS detects proximity to elevator
Action: Prompt appears “In Elevator Queue”
Display: Queue Entry Screen
Question: "How many trucks ahead of you?"
Note: "(Not including currently unloading)"
Input: [0] [1] [2] [3] [4] [5] [6+]
On Submit:
Trigger: GPS detects movement to pit/unload location (last long stop before leaving)
Action: Prompt “Begin Unloading”
Tap: Start unload timer
Display: "Unloading [Grain Type]"
Timer: Shows minutes only
Action: Tap “Finished Unloading”
Timer stops
Display: Unload Summary
- Your time: 8m 47s
- Your average: 9m 12s (YOU WERE 4% FASTER!)
Data Entry Screen:
Weight (kg/lbs): [ ] (required)
Dockage %: [ ] (optional)
Grain Grade: [ #1 / #2 / #3 / Other ] (optional)
Price: $[ ]/tonne (optional)
Notes: [ ] (optional)
Action: Automatically starts return timer
Display: "Return Trip"
Timer: Shows minutes
Options:
[Begin Load] - Starting another trip today
[Finished for Day] - Done hauling
If “Begin Load”:
If “Finished for Day”:
Display: Daily Summary
╔══════════════════════════════════════════════════╗
║ Thanks for using HaulPass, have a great rest ║
║ of your day! ║
╠══════════════════════════════════════════════════╣
║ ║
║ You hauled 73,321kg of Canola today in 3 trips! ║
║ ║
║ 🚛 Average full trip: 1hr 52min ║
║ ⏱️ Load time: 21 min (6% faster than usual) ║
║ ⏳ Wait time: 37 min (11% longer, elevator 15% ║
║ busier than usual) ║
║ 📦 Unload time: 8min 11sec average ║
║ 🛣️ Round trip: 31.5km @ 89km/hr average ║
║ ⚖️ Scale accuracy: 98% match ║
║ 📊 Dockage: 2.45% (0.78% higher than usual) ║
║ ║
╚══════════════════════════════════════════════════╝
Example Scenario: Understanding real-time queue intelligence at Prairie Grain Co-op
Steve (SuperB truck, avg unload 11m 09s):
Frank (Tandem truck, avg unload 7m 30s):
Ben (Single truck, avg unload 5m 45s):
Ted (at his farm considering hauling):
While Ted is deciding:
Notification:
🔔 Prairie Grain Co-op Queue Update
Now: 4 trucks in queue
Est. wait: 1hr 06min
(Updated 30 seconds ago)
Ted’s Decision:
When a user enters queue position:
If data doesn’t match:
Who gets notified:
What triggers notifications:
Notification content:
🔔 [Elevator Name] Update
Queue: [X] trucks ([+/-Y] from before)
Wait: [XX]min ([+/-YY]min from before)
Updated: [time] ago
CREATE TABLE user_profiles (
id UUID PRIMARY KEY REFERENCES auth.users(id),
email TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
farm_name TEXT NOT NULL,
binyard_name TEXT NOT NULL,
grain_truck_name TEXT NOT NULL,
grain_capacity_kg DECIMAL(10,2), -- Optional, learned over time
preferred_unit TEXT DEFAULT 'kg' CHECK (preferred_unit IN ('kg', 'lbs')),
favorite_elevator_id UUID REFERENCES elevators(id),
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE elevators (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
company TEXT NOT NULL,
location GEOGRAPHY(POINT, 4326) NOT NULL,
address TEXT,
accepted_grains TEXT[],
phone_number TEXT,
email TEXT,
operating_hours JSONB,
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_elevators_location ON elevators USING GIST(location);
CREATE TABLE haul_sessions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES user_profiles(id) NOT NULL,
elevator_id UUID REFERENCES elevators(id) NOT NULL,
grain_type TEXT NOT NULL,
session_date DATE NOT NULL,
status TEXT CHECK (status IN ('loading', 'driving', 'queued', 'unloading', 'returning', 'paused', 'completed')),
-- Loading phase
loading_start TIMESTAMP,
loading_end TIMESTAMP,
loading_weight_kg DECIMAL(10,2),
loading_duration_seconds INTEGER,
-- Drive to elevator phase
drive_start TIMESTAMP,
drive_end TIMESTAMP,
drive_duration_seconds INTEGER,
drive_distance_km DECIMAL(10,2),
-- Queue phase
queue_start TIMESTAMP,
queue_end TIMESTAMP,
queue_duration_seconds INTEGER,
trucks_ahead_count INTEGER,
-- Unloading phase
unload_start TIMESTAMP,
unload_end TIMESTAMP,
unload_duration_seconds INTEGER,
unload_weight_kg DECIMAL(10,2),
dockage_percent DECIMAL(5,2),
grain_grade TEXT,
price_per_tonne DECIMAL(10,2),
notes TEXT,
-- Return phase
return_start TIMESTAMP,
return_end TIMESTAMP,
return_duration_seconds INTEGER,
-- Metadata
is_paused BOOLEAN DEFAULT false,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_haul_sessions_user ON haul_sessions(user_id);
CREATE INDEX idx_haul_sessions_elevator ON haul_sessions(elevator_id);
CREATE INDEX idx_haul_sessions_date ON haul_sessions(session_date);
CREATE TABLE queue_snapshots (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
elevator_id UUID REFERENCES elevators(id) NOT NULL,
user_id UUID REFERENCES user_profiles(id) NOT NULL,
haul_session_id UUID REFERENCES haul_sessions(id),
queue_position INTEGER NOT NULL, -- 0 = currently unloading
trucks_ahead INTEGER NOT NULL,
estimated_wait_minutes INTEGER,
user_location GEOGRAPHY(POINT, 4326),
snapshot_time TIMESTAMP DEFAULT NOW(),
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_queue_elevator_time ON queue_snapshots(elevator_id, snapshot_time DESC);
CREATE TABLE elevator_stats (
elevator_id UUID PRIMARY KEY REFERENCES elevators(id),
current_queue_length INTEGER DEFAULT 0,
current_wait_estimate_minutes INTEGER DEFAULT 0,
average_unload_time_minutes DECIMAL(5,2),
total_loads_today INTEGER DEFAULT 0,
busy_score DECIMAL(3,2), -- 0.0 to 1.0, indicates how busy vs normal
last_activity TIMESTAMP,
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE user_elevator_stats (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES user_profiles(id) NOT NULL,
elevator_id UUID REFERENCES elevators(id) NOT NULL,
total_loads INTEGER DEFAULT 0,
average_wait_minutes DECIMAL(7,2),
average_unload_minutes DECIMAL(7,2),
average_drive_minutes DECIMAL(7,2),
average_load_minutes DECIMAL(7,2),
total_weight_kg DECIMAL(12,2),
average_dockage_percent DECIMAL(5,2),
last_haul_date DATE,
updated_at TIMESTAMP DEFAULT NOW(),
UNIQUE(user_id, elevator_id)
);
function calculateEstimatedWait(elevator_id: UUID, current_position: int): int {
// Get all users currently in queue
const queueUsers = getQueueUsers(elevator_id);
// Get their average unload times
let totalEstimatedMinutes = 0;
for (let i = 0; i < current_position; i++) {
const user = queueUsers[i];
const avgUnloadTime = getUserAvgUnloadTime(user.id, elevator_id);
const cushion = 2; // minutes for truck movement
totalEstimatedMinutes += avgUnloadTime + cushion;
}
return totalEstimatedMinutes;
}
-- User's average unload time at specific elevator
SELECT
AVG(unload_duration_seconds) / 60.0 as avg_unload_minutes
FROM haul_sessions
WHERE user_id = $1
AND elevator_id = $2
AND status = 'completed'
AND unload_duration_seconds IS NOT NULL
AND session_date > NOW() - INTERVAL '90 days';
-- Elevator's current busy score (compared to typical activity)
SELECT
CASE
WHEN typical_loads = 0 THEN 0
ELSE (current_loads::DECIMAL / typical_loads::DECIMAL)
END as busy_score
FROM (
SELECT
COUNT(*) FILTER (WHERE session_date = CURRENT_DATE) as current_loads,
AVG(COUNT(*)) FILTER (WHERE
EXTRACT(DOW FROM session_date) = EXTRACT(DOW FROM CURRENT_DATE)
) as typical_loads
FROM haul_sessions
WHERE elevator_id = $1
GROUP BY session_date
) as stats;
Goal: Single farmer can track a complete haul workflow
Goal: Multiple farmers collaborate on queue data
Goal: Provide valuable insights and predictions
Goal: Production-ready application
IDLE → GRAIN_SELECTION
GRAIN_SELECTION → LOADING
LOADING → LOADED (with weight) / LOADED (skipped weight)
LOADED → DRIVING_TO_ELEVATOR
DRIVING_TO_ELEVATOR → IN_QUEUE
IN_QUEUE → UNLOADING
UNLOADING → UNLOADED (data entry)
UNLOADED → RETURNING
RETURNING → LOADING (next trip) / PAUSED (load for later) / COMPLETED (done for day)
PAUSED → DRIVING_TO_ELEVATOR (resume)
COMPLETED → IDLE (daily summary shown)
This document serves as the complete technical and functional specification for HaulPass. All future development, documentation updates, and feature decisions should reference this document to maintain alignment with the core vision.
Next Steps:
Last Updated: [Date] Version: 1.0 Status: Living Document - Updates as needed