bharath/docs/architecture/DATABASE_SCHEMA.md
Vivek 394117dd74 BharathTrucks MVP - 6 sprints complete
- Govt-app styled freight marketplace
- Role-based auth (driver/shipper/broker/admin)
- Load board with bidding system
- Trip tracking with status flow
- In-app messaging
- Admin panel
- Mobile bottom nav + PWA
- Docker + Coolify ready
2026-05-31 06:21:13 +00:00

12 KiB

BharathTrucks — Database Schema Design

Version: 1.0
Date: 2026-05-31
Database: Supabase PostgreSQL


1. Schema Overview

┌──────────┐     ┌──────────┐     ┌──────────┐
│  Users   │────▶│ Profiles │────▶│  Loads   │
│(Supabase)│     │          │     │          │
└──────────┘     └──────────┘     └────┬─────┘
                      │                 │
                      │            ┌────▼─────┐
                      │            │   Bids   │
                      │            └────┬─────┘
                      │                 │
                 ┌────▼─────┐     ┌────▼─────┐
                 │  Trucks  │     │  Trips   │
                 └──────────┘     └────┬─────┘
                                       │
                                  ┌────▼─────┐
                                  │ Payments │
                                  └──────────┘

2. Tables

2.1 profiles

Extends Supabase auth.users with app-specific data.

CREATE TABLE profiles (
  id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
  role TEXT NOT NULL CHECK (role IN ('driver', 'shipper', 'broker', 'admin')),
  full_name TEXT NOT NULL,
  phone TEXT UNIQUE NOT NULL,
  email TEXT,
  avatar_url TEXT,
  language TEXT DEFAULT 'hi' CHECK (language IN ('en', 'hi')),
  city TEXT,
  state TEXT,
  is_verified BOOLEAN DEFAULT FALSE,
  is_premium BOOLEAN DEFAULT FALSE,
  is_active BOOLEAN DEFAULT TRUE,
  onboarding_complete BOOLEAN DEFAULT FALSE,
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_profiles_role ON profiles(role);
CREATE INDEX idx_profiles_city ON profiles(city);

2.2 driver_profiles

Additional driver-specific info.

CREATE TABLE driver_profiles (
  id UUID PRIMARY KEY REFERENCES profiles(id) ON DELETE CASCADE,
  license_number TEXT,
  license_expiry DATE,
  experience_years INTEGER DEFAULT 0,
  routes_preferred TEXT[], -- ['Mumbai-Delhi', 'Chennai-Bangalore']
  truck_id UUID REFERENCES trucks(id),
  availability_status TEXT DEFAULT 'available' CHECK (availability_status IN ('available', 'on_trip', 'offline')),
  total_trips INTEGER DEFAULT 0,
  rating NUMERIC(2,1) DEFAULT 0.0,
  bids_today INTEGER DEFAULT 0,
  bids_today_date DATE DEFAULT CURRENT_DATE
);

2.3 shipper_profiles

CREATE TABLE shipper_profiles (
  id UUID PRIMARY KEY REFERENCES profiles(id) ON DELETE CASCADE,
  business_name TEXT,
  gst_number TEXT,
  business_type TEXT,
  shipping_frequency TEXT CHECK (shipping_frequency IN ('daily', 'weekly', 'monthly', 'occasional')),
  total_loads_posted INTEGER DEFAULT 0,
  rating NUMERIC(2,1) DEFAULT 0.0
);

2.4 broker_profiles

CREATE TABLE broker_profiles (
  id UUID PRIMARY KEY REFERENCES profiles(id) ON DELETE CASCADE,
  agency_name TEXT,
  experience_years INTEGER DEFAULT 0,
  network_size INTEGER DEFAULT 0,
  operating_regions TEXT[],
  total_deals INTEGER DEFAULT 0,
  total_commission NUMERIC(12,2) DEFAULT 0,
  rating NUMERIC(2,1) DEFAULT 0.0
);

2.5 trucks

CREATE TABLE trucks (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  owner_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
  registration_number TEXT UNIQUE NOT NULL,
  truck_type TEXT NOT NULL CHECK (truck_type IN ('open', 'closed', 'container', 'flatbed', 'tanker', 'refrigerated', 'mini')),
  capacity_tons NUMERIC(5,1) NOT NULL,
  make TEXT, -- Tata, Ashok Leyland, etc.
  model TEXT,
  year INTEGER,
  is_active BOOLEAN DEFAULT TRUE,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_trucks_owner ON trucks(owner_id);
CREATE INDEX idx_trucks_type ON trucks(truck_type);

2.6 loads

CREATE TABLE loads (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  posted_by UUID NOT NULL REFERENCES profiles(id),
  shipper_id UUID REFERENCES profiles(id), -- if broker posts on behalf
  origin_city TEXT NOT NULL,
  origin_state TEXT NOT NULL,
  destination_city TEXT NOT NULL,
  destination_state TEXT NOT NULL,
  weight_tons NUMERIC(5,1) NOT NULL,
  truck_type_required TEXT NOT NULL,
  material_type TEXT,
  budget NUMERIC(10,2),
  pickup_date DATE NOT NULL,
  description TEXT,
  is_urgent BOOLEAN DEFAULT FALSE,
  status TEXT DEFAULT 'open' CHECK (status IN ('open', 'booked', 'in_transit', 'delivered', 'cancelled')),
  bid_count INTEGER DEFAULT 0,
  accepted_bid_id UUID,
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_loads_status ON loads(status);
CREATE INDEX idx_loads_origin ON loads(origin_city);
CREATE INDEX idx_loads_destination ON loads(destination_city);
CREATE INDEX idx_loads_posted_by ON loads(posted_by);
CREATE INDEX idx_loads_pickup_date ON loads(pickup_date);
CREATE INDEX idx_loads_truck_type ON loads(truck_type_required);

2.7 bids

CREATE TABLE bids (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  load_id UUID NOT NULL REFERENCES loads(id) ON DELETE CASCADE,
  driver_id UUID NOT NULL REFERENCES profiles(id),
  amount NUMERIC(10,2) NOT NULL,
  estimated_delivery DATE,
  note TEXT,
  status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'accepted', 'rejected', 'withdrawn')),
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW(),
  UNIQUE(load_id, driver_id) -- one bid per driver per load
);

CREATE INDEX idx_bids_load ON bids(load_id);
CREATE INDEX idx_bids_driver ON bids(driver_id);
CREATE INDEX idx_bids_status ON bids(status);

2.8 trips

CREATE TABLE trips (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  load_id UUID NOT NULL REFERENCES loads(id),
  driver_id UUID NOT NULL REFERENCES profiles(id),
  shipper_id UUID NOT NULL REFERENCES profiles(id),
  bid_id UUID NOT NULL REFERENCES bids(id),
  status TEXT DEFAULT 'confirmed' CHECK (status IN ('confirmed', 'picked_up', 'in_transit', 'delivered', 'cancelled')),
  picked_up_at TIMESTAMPTZ,
  delivered_at TIMESTAMPTZ,
  driver_rating INTEGER CHECK (driver_rating BETWEEN 1 AND 5),
  shipper_rating INTEGER CHECK (shipper_rating BETWEEN 1 AND 5),
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_trips_driver ON trips(driver_id);
CREATE INDEX idx_trips_shipper ON trips(shipper_id);
CREATE INDEX idx_trips_status ON trips(status);

2.9 messages

CREATE TABLE messages (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  load_id UUID REFERENCES loads(id),
  sender_id UUID NOT NULL REFERENCES profiles(id),
  receiver_id UUID NOT NULL REFERENCES profiles(id),
  content TEXT NOT NULL,
  is_read BOOLEAN DEFAULT FALSE,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_messages_receiver ON messages(receiver_id, is_read);
CREATE INDEX idx_messages_load ON messages(load_id);

2.10 payments

CREATE TABLE payments (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  trip_id UUID NOT NULL REFERENCES trips(id),
  payer_id UUID NOT NULL REFERENCES profiles(id),
  payee_id UUID NOT NULL REFERENCES profiles(id),
  amount NUMERIC(10,2) NOT NULL,
  method TEXT DEFAULT 'upi' CHECK (method IN ('upi', 'cash', 'bank_transfer')),
  status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'completed', 'disputed')),
  upi_reference TEXT,
  notes TEXT,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_payments_trip ON payments(trip_id);
CREATE INDEX idx_payments_payee ON payments(payee_id);

2.11 broker_commissions

CREATE TABLE broker_commissions (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  broker_id UUID NOT NULL REFERENCES profiles(id),
  trip_id UUID NOT NULL REFERENCES trips(id),
  load_id UUID NOT NULL REFERENCES loads(id),
  amount NUMERIC(10,2) NOT NULL,
  status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'received')),
  created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_commissions_broker ON broker_commissions(broker_id);

2.12 notifications

CREATE TABLE notifications (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID NOT NULL REFERENCES profiles(id),
  type TEXT NOT NULL CHECK (type IN ('bid_received', 'bid_accepted', 'bid_rejected', 'trip_update', 'payment', 'system')),
  title TEXT NOT NULL,
  body TEXT,
  reference_id UUID, -- load_id, bid_id, trip_id
  is_read BOOLEAN DEFAULT FALSE,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_notifications_user ON notifications(user_id, is_read);

3. Row Level Security (RLS) Policies

-- Profiles: users can read all, update own
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Public profiles readable" ON profiles FOR SELECT USING (true);
CREATE POLICY "Users update own profile" ON profiles FOR UPDATE USING (auth.uid() = id);

-- Loads: all can read open loads, owners can update
ALTER TABLE loads ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Open loads readable" ON loads FOR SELECT USING (true);
CREATE POLICY "Owners manage loads" ON loads FOR ALL USING (auth.uid() = posted_by);

-- Bids: load owner + bidder can see
ALTER TABLE bids ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Bid participants can view" ON bids FOR SELECT
  USING (auth.uid() = driver_id OR auth.uid() IN (SELECT posted_by FROM loads WHERE id = load_id));
CREATE POLICY "Drivers create bids" ON bids FOR INSERT WITH CHECK (auth.uid() = driver_id);

-- Messages: sender and receiver only
ALTER TABLE messages ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Message participants" ON messages FOR SELECT
  USING (auth.uid() = sender_id OR auth.uid() = receiver_id);
CREATE POLICY "Users send messages" ON messages FOR INSERT WITH CHECK (auth.uid() = sender_id);

4. Database Functions

-- Auto-update updated_at timestamp
CREATE OR REPLACE FUNCTION update_updated_at()
RETURNS TRIGGER AS $$
BEGIN
  NEW.updated_at = NOW();
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER profiles_updated_at BEFORE UPDATE ON profiles
  FOR EACH ROW EXECUTE FUNCTION update_updated_at();
CREATE TRIGGER loads_updated_at BEFORE UPDATE ON loads
  FOR EACH ROW EXECUTE FUNCTION update_updated_at();
CREATE TRIGGER bids_updated_at BEFORE UPDATE ON bids
  FOR EACH ROW EXECUTE FUNCTION update_updated_at();
CREATE TRIGGER trips_updated_at BEFORE UPDATE ON trips
  FOR EACH ROW EXECUTE FUNCTION update_updated_at();

-- Increment bid count on load when new bid placed
CREATE OR REPLACE FUNCTION increment_bid_count()
RETURNS TRIGGER AS $$
BEGIN
  UPDATE loads SET bid_count = bid_count + 1 WHERE id = NEW.load_id;
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER bid_count_trigger AFTER INSERT ON bids
  FOR EACH ROW EXECUTE FUNCTION increment_bid_count();

-- Reset daily bid count
CREATE OR REPLACE FUNCTION reset_daily_bids()
RETURNS void AS $$
BEGIN
  UPDATE driver_profiles SET bids_today = 0, bids_today_date = CURRENT_DATE
  WHERE bids_today_date < CURRENT_DATE;
END;
$$ LANGUAGE plpgsql;

5. Seed Data (Development)

-- Truck types reference
INSERT INTO trucks (owner_id, registration_number, truck_type, capacity_tons, make)
VALUES
  -- Will be populated during testing
;

-- Sample cities for load testing
-- Mumbai, Delhi, Bangalore, Chennai, Hyderabad, Ahmedabad, Pune, Kolkata, Jaipur, Nagpur

Schema designed for simplicity in Phase 1. Normalized where needed, denormalized (bid_count, total_trips) for read performance.