Vertical Farm Database Schema
Introduction
This document outlines the database schema for the Vertical Farm project. The schema is designed for use with Supabase (PostgreSQL) and supports all core functionalities including farm layout management, device assignment, grow recipe management, scheduling, automation, and role-based access control.
Entity Relationship Diagram (ERD)
erDiagram
user_profiles {
UUID id PK
user_role role
TEXT name
TIMESTAMPTZ created_at
TIMESTAMPTZ updated_at
}
farms {
UUID id PK
TEXT name
TEXT location
UUID manager_id FK
NUMERIC width
NUMERIC depth
TIMESTAMPTZ created_at
TIMESTAMPTZ updated_at
}
rows {
UUID id PK
UUID farm_id FK
TEXT name
INTEGER position
NUMERIC position_x
NUMERIC position_y
NUMERIC length
NUMERIC depth
TEXT orientation
TIMESTAMPTZ created_at
TIMESTAMPTZ updated_at
}
racks {
UUID id PK
UUID row_id FK
TEXT name
INTEGER position_in_row
NUMERIC width
NUMERIC depth
NUMERIC height
INTEGER max_shelves
TIMESTAMPTZ created_at
TIMESTAMPTZ updated_at
}
shelves {
UUID id PK
UUID rack_id FK
TEXT name
INTEGER position_in_rack
NUMERIC width
NUMERIC depth
NUMERIC max_weight
TIMESTAMPTZ created_at
TIMESTAMPTZ updated_at
}
device_assignments {
UUID id PK
UUID shelf_id FK "nullable"
UUID rack_id FK "nullable"
UUID row_id FK "nullable"
UUID farm_id FK "nullable"
TEXT entity_id
TEXT entity_type
TEXT friendly_name
UUID assigned_by FK
TIMESTAMPTZ created_at
TIMESTAMPTZ updated_at
}
species {
UUID id PK
TEXT name
TEXT description
TIMESTAMPTZ created_at
TIMESTAMPTZ updated_at
}
grow_recipes {
UUID id PK
UUID species_id FK
TEXT name
INTEGER grow_days
NUMERIC light_hours_per_day
NUMERIC watering_frequency_hours
NUMERIC target_temperature_min
NUMERIC target_temperature_max
NUMERIC target_humidity_min
NUMERIC target_humidity_max
NUMERIC target_ph_min
NUMERIC target_ph_max
NUMERIC target_ec_min
NUMERIC target_ec_max
NUMERIC average_yield
NUMERIC sowing_rate
JSONB custom_parameters
TIMESTAMPTZ created_at
TIMESTAMPTZ updated_at
}
schedules {
UUID id PK
UUID shelf_id FK
UUID grow_recipe_id FK
TIMESTAMPTZ start_date
TIMESTAMPTZ estimated_end_date
TIMESTAMPTZ actual_end_date
schedule_status status
TEXT notes
TIMESTAMPTZ created_at
TIMESTAMPTZ updated_at
}
scheduled_actions {
UUID id PK
UUID schedule_id FK "nullable"
UUID device_assignment_id FK
action_type action_type
JSONB parameters
TIMESTAMPTZ execution_time
action_status status
TIMESTAMPTZ executed_at
TEXT error_message
TIMESTAMPTZ created_at
TIMESTAMPTZ updated_at
}
sensor_readings {
BIGSERIAL id PK
UUID device_assignment_id FK
TEXT reading_type
NUMERIC value
TEXT unit
TIMESTAMPTZ timestamp
}
harvests {
UUID id PK
UUID schedule_id FK
UUID shelf_id FK
NUMERIC yield_amount
TEXT yield_unit
TIMESTAMPTZ harvest_date
TEXT notes
TIMESTAMPTZ created_at
TIMESTAMPTZ updated_at
}
automation_rules {
UUID id PK
TEXT name
TEXT description
UUID farm_id FK "nullable"
UUID trigger_source_device_id FK
TEXT trigger_reading_type
TEXT trigger_condition
NUMERIC trigger_value
UUID action_target_device_id FK
action_type action_type
JSONB action_parameters
BOOLEAN is_active
TIMESTAMPTZ created_at
TIMESTAMPTZ updated_at
}
user_profiles ||--o{ farms : "manages (optional)"
farms ||--o{ rows : "contains"
rows ||--o{ racks : "contains"
racks ||--o{ shelves : "contains"
shelves }o--|| device_assignments : "assigned to (optional)"
racks }o--|| device_assignments : "assigned to (optional)"
rows }o--|| device_assignments : "assigned to (optional)"
farms }o--|| device_assignments : "assigned to (optional)"
user_profiles ||--o{ device_assignments : "assigned by (optional)"
species ||--o{ grow_recipes : "has (many)"
shelves ||--o{ schedules : "scheduled on (many)"
grow_recipes ||--o{ schedules : "uses"
schedules }o--|| scheduled_actions : "triggers (optional)"
device_assignments ||--o{ scheduled_actions : "targets"
device_assignments ||--o{ sensor_readings : "generates"
schedules ||--o{ harvests : "results in"
shelves ||--o{ harvests : "from (redundant with schedule)"
farms }o--|| automation_rules : "applies to (optional)"
device_assignments ||--o{ automation_rules : "trigger source"
device_assignments ||--o{ automation_rules : "action target"
Table Definitions & SQL
This section contains the SQL DDL statements for creating the database schema. It is intended to be used for Supabase migrations.
-- Enable UUID extension if not already enabled
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- Enum for user roles
CREATE TYPE user_role AS ENUM ('farm_manager', 'operator', 'ha_power_user', 'admin');
-- Table for user profiles, linking to Supabase auth.users
CREATE TABLE public.user_profiles (
id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
role user_role NOT NULL DEFAULT 'operator',
name TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Indexes for user_profiles
CREATE INDEX idx_user_profiles_role ON public.user_profiles(role);
-- RLS Policies for user_profiles (Example - apply as needed in a separate migration or step)
-- ALTER TABLE public.user_profiles ENABLE ROW LEVEL SECURITY;
-- CREATE POLICY "Users can view their own profile" ON public.user_profiles FOR SELECT USING (auth.uid() = id);
-- CREATE POLICY "Admins can manage all profiles" ON public.user_profiles FOR ALL USING (EXISTS (SELECT 1 FROM public.user_profiles WHERE id = auth.uid() AND role = 'admin'));
-- Farm Hierarchy Tables
CREATE TABLE public.farms (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
name TEXT NOT NULL UNIQUE,
location TEXT,
manager_id UUID REFERENCES public.user_profiles(id) ON DELETE SET NULL,
width NUMERIC,
depth NUMERIC,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX idx_farms_manager_id ON public.farms(manager_id);
CREATE TABLE public.rows (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
farm_id UUID NOT NULL REFERENCES public.farms(id) ON DELETE CASCADE,
name TEXT NOT NULL,
position INTEGER NOT NULL, -- Order of the row within the farm
position_x NUMERIC,
position_y NUMERIC,
length NUMERIC,
depth NUMERIC, -- Can be different from length based on representation
orientation TEXT, -- e.g., 'horizontal', 'vertical'
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE (farm_id, name),
UNIQUE (farm_id, position)
);
CREATE INDEX idx_rows_farm_id ON public.rows(farm_id);
CREATE TABLE public.racks (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
row_id UUID NOT NULL REFERENCES public.rows(id) ON DELETE CASCADE,
name TEXT NOT NULL,
position_in_row INTEGER NOT NULL,
width NUMERIC,
depth NUMERIC,
height NUMERIC,
max_shelves INTEGER,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE (row_id, name),
UNIQUE (row_id, position_in_row)
);
CREATE INDEX idx_racks_row_id ON public.racks(row_id);
CREATE TABLE public.shelves (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
rack_id UUID NOT NULL REFERENCES public.racks(id) ON DELETE CASCADE,
name TEXT NOT NULL,
position_in_rack INTEGER NOT NULL,
width NUMERIC,
depth NUMERIC,
max_weight NUMERIC,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE (rack_id, name),
UNIQUE (rack_id, position_in_rack)
);
CREATE INDEX idx_shelves_rack_id ON public.shelves(rack_id);
-- Device Assignment
CREATE TABLE public.device_assignments (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
shelf_id UUID REFERENCES public.shelves(id) ON DELETE CASCADE,
rack_id UUID REFERENCES public.racks(id) ON DELETE CASCADE,
row_id UUID REFERENCES public.rows(id) ON DELETE CASCADE,
farm_id UUID REFERENCES public.farms(id) ON DELETE CASCADE,
entity_id TEXT NOT NULL, -- ID from Home Assistant or other source
entity_type TEXT NOT NULL, -- e.g., 'sensor', 'switch', 'light'
friendly_name TEXT,
assigned_by UUID REFERENCES public.user_profiles(id) ON DELETE SET NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
CONSTRAINT chk_device_assignment_level
CHECK (
(shelf_id IS NOT NULL AND rack_id IS NULL AND row_id IS NULL AND farm_id IS NULL) OR
(shelf_id IS NULL AND rack_id IS NOT NULL AND row_id IS NULL AND farm_id IS NULL) OR
(shelf_id IS NULL AND rack_id IS NULL AND row_id IS NOT NULL AND farm_id IS NULL) OR
(shelf_id IS NULL AND rack_id IS NULL AND row_id IS NULL AND farm_id IS NOT NULL)
)
);
CREATE INDEX idx_device_assignments_shelf_id ON public.device_assignments(shelf_id);
CREATE INDEX idx_device_assignments_rack_id ON public.device_assignments(rack_id);
CREATE INDEX idx_device_assignments_row_id ON public.device_assignments(row_id);
CREATE INDEX idx_device_assignments_farm_id ON public.device_assignments(farm_id);
CREATE INDEX idx_device_assignments_entity_id ON public.device_assignments(entity_id);
-- Species (for plants/crops)
CREATE TABLE public.species (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
name TEXT NOT NULL UNIQUE,
description TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Grow Recipes
CREATE TABLE public.grow_recipes (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
species_id UUID NOT NULL REFERENCES public.species(id) ON DELETE RESTRICT,
name TEXT NOT NULL,
grow_days INTEGER,
light_hours_per_day NUMERIC,
watering_frequency_hours NUMERIC,
target_temperature_min NUMERIC,
target_temperature_max NUMERIC,
target_humidity_min NUMERIC,
target_humidity_max NUMERIC,
target_ph_min NUMERIC,
target_ph_max NUMERIC,
target_ec_min NUMERIC,
target_ec_max NUMERIC,
average_yield NUMERIC,
sowing_rate NUMERIC,
custom_parameters JSONB,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE (species_id, name)
);
CREATE INDEX idx_grow_recipes_species_id ON public.grow_recipes(species_id);
-- Schedules (for what's growing on a shelf)
CREATE TYPE schedule_status AS ENUM ('planned', 'active', 'completed', 'aborted');
CREATE TABLE public.schedules (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
shelf_id UUID NOT NULL REFERENCES public.shelves(id) ON DELETE CASCADE,
grow_recipe_id UUID NOT NULL REFERENCES public.grow_recipes(id) ON DELETE RESTRICT,
start_date TIMESTAMP WITH TIME ZONE NOT NULL,
estimated_end_date TIMESTAMP WITH TIME ZONE,
actual_end_date TIMESTAMP WITH TIME ZONE,
status schedule_status NOT NULL DEFAULT 'planned',
notes TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX idx_schedules_shelf_id ON public.schedules(shelf_id);
CREATE INDEX idx_schedules_grow_recipe_id ON public.schedules(grow_recipe_id);
CREATE INDEX idx_schedules_status ON public.schedules(status);
-- Scheduled Actions (for automation)
CREATE TYPE action_type AS ENUM ('light_on', 'light_off', 'water_pump_on', 'water_pump_off', 'nutrient_dose', 'fan_on', 'fan_off', 'alert');
CREATE TYPE action_status AS ENUM ('pending', 'executed', 'failed', 'skipped', 'cancelled');
CREATE TABLE public.scheduled_actions (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
schedule_id UUID REFERENCES public.schedules(id) ON DELETE CASCADE,
device_assignment_id UUID REFERENCES public.device_assignments(id) ON DELETE CASCADE,
action_type action_type NOT NULL,
parameters JSONB,
execution_time TIMESTAMP WITH TIME ZONE NOT NULL,
status action_status NOT NULL DEFAULT 'pending',
executed_at TIMESTAMP WITH TIME ZONE,
error_message TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX idx_scheduled_actions_schedule_id ON public.scheduled_actions(schedule_id);
CREATE INDEX idx_scheduled_actions_device_assignment_id ON public.scheduled_actions(device_assignment_id);
CREATE INDEX idx_scheduled_actions_execution_time ON public.scheduled_actions(execution_time);
CREATE INDEX idx_scheduled_actions_status ON public.scheduled_actions(status);
-- Sensor Readings (formerly Sensor History)
CREATE TABLE public.sensor_readings (
id BIGSERIAL PRIMARY KEY,
device_assignment_id UUID NOT NULL REFERENCES public.device_assignments(id) ON DELETE CASCADE,
reading_type TEXT NOT NULL, -- e.g., 'temperature', 'humidity', 'ph', 'ec'
value NUMERIC NOT NULL,
unit TEXT, -- e.g., 'C', '%', 'ppm'
timestamp TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_sensor_readings_device_assignment_id_timestamp ON public.sensor_readings(device_assignment_id, timestamp DESC);
CREATE INDEX idx_sensor_readings_timestamp ON public.sensor_readings(timestamp DESC);
-- Consider TimescaleDB for sensor_readings if volume is very high:
-- SELECT create_hypertable('sensor_readings', 'timestamp');
-- Harvests
CREATE TABLE public.harvests (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
schedule_id UUID NOT NULL REFERENCES public.schedules(id) ON DELETE RESTRICT,
shelf_id UUID NOT NULL REFERENCES public.shelves(id) ON DELETE RESTRICT, -- Consider if needed, as schedule has shelf_id
yield_amount NUMERIC NOT NULL,
yield_unit TEXT NOT NULL, -- e.g., 'grams', 'kg', 'heads'
harvest_date TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
notes TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX idx_harvests_schedule_id ON public.harvests(schedule_id);
CREATE INDEX idx_harvests_shelf_id ON public.harvests(shelf_id);
-- Automation Rules
CREATE TABLE public.automation_rules (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
name TEXT NOT NULL UNIQUE,
description TEXT,
farm_id UUID REFERENCES public.farms(id) ON DELETE CASCADE,
trigger_source_device_id UUID REFERENCES public.device_assignments(id) ON DELETE CASCADE,
trigger_reading_type TEXT,
trigger_condition TEXT, -- e.g., '>', '<', '='
trigger_value NUMERIC,
action_target_device_id UUID REFERENCES public.device_assignments(id) ON DELETE CASCADE,
action_type action_type,
action_parameters JSONB,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX idx_automation_rules_farm_id ON public.automation_rules(farm_id);
CREATE INDEX idx_automation_rules_is_active ON public.automation_rules(is_active);
Notes & Considerations
- RLS Policies: Row Level Security policies are commented out in the SQL DDL. These should be reviewed, tailored, and applied in a subsequent migration or step, once the basic schema is confirmed.
- Physical Dimensions: Added
width
,depth
,height
,position_x
,position_y
,length
,orientation
,max_shelves
,max_weight
to the farm hierarchy tables (farms
,rows
,racks
,shelves
) to support detailed layout configuration and visualization. - Device Assignment: Modified
device_assignments
to allow linking devices atfarm
,row
,rack
, orshelf
level using a check constraint. - Sensor Readings: Renamed
sensor_history
tosensor_readings
for clarity and usedBIGSERIAL
for its primary key. - ENUM Types: Defined
user_role
,schedule_status
,action_type
, andaction_status
as ENUM types. - Default Values:
DEFAULT uuid_generate_v4()
for UUID primary keys andDEFAULT NOW()
forcreated_at
/updated_at
timestamps are used. - Indexes: Basic indexes are included for foreign keys and commonly queried columns. More specific or composite indexes might be needed based on query patterns.
- TimescaleDB: A comment is included regarding the potential use of TimescaleDB for the
sensor_readings
table if data volume becomes very large.