Database Schema - esketchandu/TrailAndOutdoorRecreationHub GitHub Wiki
Trail & Outdoor Recreation Hub - Database Schema
Overview
The Trail Hub database is designed to power a community-driven platform where outdoor enthusiasts can discover, share, and explore hiking trails together. Think of it as a social network meets GPS tracker meets trail guidebook - all rolled into one.
Database Schema Diagram
Entity Relationship Diagram showing all tables and their relationships
What This Database Does
The database structure supports six core features that make the Trail Hub special:
- Trail Management - Store detailed trail information including GPS routes, difficulty levels, and elevation data
- User Reviews - Let hikers share their experiences with ratings, trail conditions, and helpful tips
- Points of Interest - Mark important spots like viewpoints, water sources, and campsites along trails
- Photo Sharing - Upload and geotag photos to create visual trail guides
- Hazard Reporting - Keep the community safe with real-time alerts about trail conditions
- Trip Planning - Organize group hikes and connect with other outdoor enthusiasts
Technology Stack
I am using PostgreSQL with the PostGIS extension for spatial data. This means I can store actual GPS trails and perform geographic queries like "find all trails within 10 miles of me" or "which viewpoints are along this trail?"
Design Philosophy
The schema follows a few key principles:
- Keep it simple - I am focusing on essential features for the MVP
- Community first - Users can contribute trails, reviews, photos, and updates
- Safety matters - Hazard reporting and trail conditions are prominently featured
- Real-world ready - All spatial data uses standard GPS coordinates (WGS84/SRID 4326)
Database Tables
Core Tables
users
The heart of our community - everyone from casual day hikers to hardcore mountaineers
Column Name | Data Type | Details |
---|---|---|
id | integer | Primary key - unique identifier for each user |
username | varchar(80) | Unique username for login (required, indexed for fast lookups) |
varchar(120) | Email address (required, unique, indexed) | |
password_hash | varchar(255) | Securely hashed password (never store plain text!) |
first_name | varchar(50) | User's first name (optional) |
last_name | varchar(50) | User's last name (optional) |
bio | text | Tell us about your hiking experience (optional) |
avatar_url | varchar(500) | Profile picture stored in AWS S3 |
hiking_level | varchar(20) | Self-reported skill: beginner, intermediate, advanced, or expert (defaults to beginner) |
is_active | boolean | Is the account active? (defaults to true) |
is_admin | boolean | Admin privileges for content moderation (defaults to false) |
created_at | timestamp | When they joined our community |
updated_at | timestamp | Last profile update |
trails
The stars of the show - from easy nature walks to challenging summit attempts
Column Name | Data Type | Details |
---|---|---|
id | integer | Primary key - unique trail identifier |
name | varchar(200) | Trail name like "Eagle Peak Trail" (required, indexed for search) |
description | text | What makes this trail special? Wildlife, views, history, etc. |
difficulty | varchar(20) | How tough is it? easy, moderate, hard, or expert (required) |
length_km | float | Total distance in kilometers (required) |
elevation_gain_m | float | Total climb in meters - helps gauge difficulty |
geometry | geometry | The actual GPS trail route (PostGIS LINESTRING, required) |
region | varchar(100) | General area like "Yosemite" or "Rocky Mountains" |
parking_info | text | Where to park, fees, and tips |
created_by | integer | User who added this trail (foreign key to users) |
avg_rating | float | Average star rating (calculated from reviews, starts at 0) |
total_reviews | integer | Number of reviews |
created_at | timestamp | When the trail was added (indexed for "newest trails") |
updated_at | timestamp | Last modification date |
reviews
Real experiences from real hikers - the good, the muddy, and the breathtaking
Column Name | Data Type | Details |
---|---|---|
id | integer | Primary key |
trail_id | integer | Which trail is being reviewed (foreign key, indexed) |
user_id | integer | Who wrote this review (foreign key, indexed) |
rating | integer | Star rating from 1 (awful) to 5 (amazing) - required |
title | varchar(200) | Review headline like "Perfect Fall Hike!" |
content | text | The full review story (required) |
hiked_date | date | When they actually hiked it |
weather_condition | varchar(50) | What was the weather? sunny, cloudy, rainy, or snowy |
trail_condition | varchar(50) | Trail status: excellent, good, muddy, icy, or poor |
crowd_level | varchar(20) | How busy was it? empty, light, moderate, or crowded |
helpful_count | integer | How many people found this helpful (starts at 0) |
is_verified_hike | boolean | GPS verified they were actually there (defaults to false) |
created_at | timestamp | When review was written (indexed for sorting) |
updated_at | timestamp | Last edit timestamp |
points_of_interest
The highlights along the way - because the journey matters as much as the destination
Column Name | Data Type | Details |
---|---|---|
id | integer | Primary key |
trail_id | integer | Which trail this POI belongs to (foreign key, indexed) |
name | varchar(200) | POI name like "Eagle's Nest Viewpoint" (required) |
description | text | What's special about this spot? |
type | varchar(50) | What kind of POI: viewpoint, waterfall, campsite, water_source, parking, warning, or junction (required) |
location | geometry | Exact GPS coordinates (PostGIS POINT, required) |
latitude | float | Latitude for quick access (required) |
longitude | float | Longitude for quick access (required) |
elevation_m | float | How high up is it? |
distance_from_start_km | float | How far from the trailhead? |
amenities | json | For campsites: {"toilets": true, "fire_rings": false, "bear_boxes": true} |
seasonal_info | text | Notes like "Water source dries up in late summer" |
is_verified | boolean | Has this been confirmed? (defaults to false) |
created_by | integer | User who added this POI (foreign key) |
created_at | timestamp | When it was added |
updated_at | timestamp | Last update |
photos
A picture's worth a thousand elevation gains - visual inspiration and trail documentation
Column Name | Data Type | Details |
---|---|---|
id | integer | Primary key |
uuid | uuid | Unique identifier for S3 storage (auto-generated) |
trail_id | integer | Which trail this photo belongs to (foreign key, indexed) |
user_id | integer | Who took this amazing shot (foreign key) |
poi_id | integer | Linked to a specific POI (optional foreign key) |
original_url | varchar(500) | Full-resolution image in S3 (required) |
thumbnail_url | varchar(500) | Small preview version |
medium_url | varchar(500) | Web-optimized version |
caption | text | Photo description or story |
latitude | float | GPS latitude from EXIF or manual tagging |
longitude | float | GPS longitude from EXIF or manual tagging |
location_on_trail | geometry | Exact spot on trail (PostGIS POINT, optional) |
taken_at | timestamp | When the photo was taken (from EXIF) |
camera_make | varchar(100) | Camera brand (from EXIF) |
camera_model | varchar(100) | Camera model (from EXIF) |
likes_count | integer | How many likes (starts at 0) |
is_featured | boolean | Admin-selected showcase photos (defaults to false) |
uploaded_at | timestamp | When uploaded to Trail Hub (indexed for recent photos) |
hazards
Keeping everyone safe - because conditions change and the community looks out for each other
Column Name | Data Type | Details |
---|---|---|
id | integer | Primary key |
trail_id | integer | Which trail has the hazard (foreign key, indexed) |
reported_by | integer | User who reported it (foreign key, required) |
type | varchar(50) | Type of hazard: fallen_tree, flooding, wildlife, ice, rockfall, trail_damage, or closure (required) |
severity | varchar(20) | How serious: low, medium, high, or extreme (required) |
title | varchar(200) | Quick summary like "Large tree across trail at mile 2" (required) |
description | text | Full details and suggested detours (required) |
location | geometry | Exact hazard location (PostGIS POINT, required) |
latitude | float | Latitude for quick display (required) |
longitude | float | Longitude for quick display (required) |
status | varchar(20) | Current status: active, monitoring, or resolved (defaults to active) |
resolved_at | timestamp | When the hazard was cleared |
resolved_by | integer | User who marked it resolved (foreign key) |
resolution_notes | text | How it was resolved |
expires_at | timestamp | Auto-expire date for temporary hazards |
verified_by_admin | boolean | Admin confirmation for serious hazards (defaults to false) |
reported_at | timestamp | When first reported (indexed for recent hazards) |
updated_at | timestamp | Last status update |
trip_plans
Bringing hikers together - because trails are better with friends
Column Name | Data Type | Details |
---|---|---|
id | integer | Primary key |
trail_id | integer | Which trail we're hiking (foreign key) |
organizer_id | integer | Who's organizing this adventure (foreign key) |
title | varchar(200) | Trip name like "Sunrise Hike to Eagle Peak" (required) |
description | text | What to expect, skill level needed, etc. |
planned_date | date | When we're going (required, indexed for upcoming trips) |
start_time | time | Meeting time (required) |
estimated_end_time | time | When we expect to finish |
meeting_point | varchar(200) | Where to meet - might differ from trailhead |
meeting_point_location | geometry | GPS coordinates of meeting spot (PostGIS POINT) |
meeting_lat | float | Meeting point latitude |
meeting_lon | float | Meeting point longitude |
max_participants | integer | Group size limit (defaults to 10) |
difficulty_level | varchar(20) | Expected pace/difficulty |
pace | varchar(20) | How fast we'll go: leisurely, moderate, or fast |
requirements | json | What to bring: ["water", "lunch", "hiking boots", "headlamp"] |
notes | text | Additional info like "Dog-friendly" or "Photography focus" |
status | varchar(20) | Trip status: active, cancelled, or completed (defaults to active) |
is_public | boolean | Open to everyone or private group? (defaults to true) |
created_at | timestamp | When trip was posted |
updated_at | timestamp | Last modification |
Relationship Tables
trip_participants
Who's coming on the adventure?
Column Name | Data Type | Details |
---|---|---|
trip_plan_id | integer | Which trip (composite primary key with user_id) |
user_id | integer | Which hiker (composite primary key with trip_plan_id) |
status | varchar(20) | Their RSVP: pending, confirmed, or declined (defaults to pending) |
joined_at | timestamp | When they signed up |
favorite_trails
Your personal trail wishlist and hall of fame
Column Name | Data Type | Details |
---|---|---|
user_id | integer | Who favorited it (composite primary key) |
trail_id | integer | Which trail they love (composite primary key) |
favorited_at | timestamp | When they added it to favorites |
hazard_photos
Visual documentation of trail hazards
Column Name | Data Type | Details |
---|---|---|
hazard_id | integer | Which hazard (composite primary key) |
photo_id | integer | Which photo shows it (composite primary key) |
Key Design Decisions
Why These Relationships?
- One user can create many trails - We're building a community-contributed database
- One trail has many reviews - Multiple perspectives make better trail guides
- One review per user per trail - Prevents spam and ensures authentic experiences
- Photos can link to POIs - "Here's what the viewpoint actually looks like!"
- Hazards have reporters and resolvers - Track who's helping keep trails safe
- Trip plans use many-to-many - Flexible group organization
Performance Considerations
I have added indexes on:
- Search fields (usernames, trail names) for quick lookups
- Foreign keys for efficient joins
- Timestamps for sorting by recent activity
- Spatial data automatically indexed by PostGIS for "near me" queries
Data Integrity
- Unique constraints prevent duplicate usernames and emails
- Foreign keys ensure data consistency (can't review a non-existent trail)
- Not null constraints on critical fields (every trail needs a name and route)
- Application-level validation for ratings (1-5) and status values