# Database Design – Rentman.io Clone > **Version:** 1.0 > **Date:** 2026-05-31 > **Project:** Rentman.io-Nachbau > **Architect:** Solution Architect (A0 Software Orchestrator) --- ## 1. Entity-Relationship Diagram (Textual) ``` ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │ Account │ │ User │ │ Role │ │ (tenant) │ │ │ │ │ │ id (PK) │1───*│ id (PK) │*───*│ id (PK) │ │ name │ │ account_id (FK) │ │ name │ │ created_at │ │ email │ │ permissions (JSON│ └──────────────────┘ │ full_name │ └──────────────────┘ │ password_hash │ │ role_id (FK) │ └────────────────────┘ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │ Tag │ │ Contact │ │ Document │ │ id (PK) │*───*│ id (PK) │1───*│ id (PK) │ │ name │ │ account_id (FK) │ │ account_id (FK) │ │ color │ │ type │ │ name │ └──────────────────┘ │ company_name │ │ file_path │ │ first_name │ │ mime_type │ │ last_name │ │ size_bytes │ │ email, phone, mobile│ │ project_id (FK) │ │ street, number, │ │ uploaded_by (FK) │ │ postalcode, city, │ │ created_at │ │ country, tax_number│ └──────────────────┘ │ note │ └───────────────────┘ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │ Project │ │ Subproject │ │ ProjectFunctionGroup│ │ id (PK) │1───*│ id (PK) │1───*│ id (PK) │ │ account_id (FK) │ │ project_id (FK) │ │ project_id (FK) │ │ name │ │ name │ │ subproject_id (FK)│ │ reference │ │ custom_fields (JSON)│ │ name │ │ number │ └──────────────────┘ │ usageperiod_start/end│ │ customer_id (FK) │ │ planperiod_start/end│ │ contact_person_id │ │ remark │ │ account_manager_id│ └──────┬───────────┘ │ project_type │ │ │ planperiod_start/end│ │ │ usageperiod_start/end│ │ │ status │ ┌──────▼──────────┐ │ location_* │ │ ProjectFunction │ │ remark │ │ id (PK) │ │ price, cost │ │ project_id (FK) │ │ created_at, updated_at│ │ subproject_id (FK)│ │ archived │ │ group_id (FK) │ └──────┬────────────┘ │ type (enum) │ │ │ amount, usg/plan│ │ │ cost_rate, price_rate│ │ │ remark │ │ └──────┬──────────┘ │ │ │ ┌──────▼──────────┐ ├───────────────────────────────────────────►│ ProjectCrew │ │ │ id (PK) │ │ ProjectEquipmentGroup │ project_id (FK) │ ├───────────────────────────────────────────►│ function_id (FK) │ │ │ crew_id (FK) │ │ ProjectEquipment (within group) │ start, end │ │ └──────────────────┘ │ ├──► Quote │ (id, project_id, number, status, valid_until, │ template, content_html, total_net, total_gross, created_at) │ ├──► Invoice │ (id, project_id, number, status, due_date, │ total_net, total_gross, created_at) │ ├──► ProjectRequest (id, name, contact fields, location, periods, status, remark) │ │ │ └──► ProjectRequestEquipment (id, request_id, name, quantity, unit_price, linked_equipment_id) │ └──► VehicleAssignment (id, vehicle_id, project_id, start, end, driver_id) ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │ Equipment │ │ Crew │ │ Vehicle │ │ id (PK) │ │ id (PK) │ │ id (PK) │ │ account_id (FK) │ │ account_id (FK) │ │ account_id (FK) │ │ name │ │ first_name │ │ name │ │ category │ │ last_name │ │ license_plate │ │ serial_number │ │ email, phone │ │ type │ │ barcode, qr_code │ │ type (internal/ │ │ payload_kg │ │ stock_location_id │ │ freelancer) │ │ volume_m3 │ │ status │ │ hourly_rate │ │ is_active │ │ purchase_price │ │ daily_rate │ └──────────────────┘ │ current_value │ │ skills (JSON) │ │ weight_kg, dims, │ │ note │ │ power_watt, notes │ │ user_id (FK) │ │ custom_fields, img│ │ is_active │ └──────────────────┘ └────────┬──────────┘ │ ┌──────────────────┐ ┌────────▼──────────┐ │ EquipmentGroup │ │ CrewAvailability │ │ (Bundle) │ │ id (PK) │ │ id (PK) │ │ crew_id (FK) │ │ account_id (FK) │ │ start, end │ │ name │ │ type (avail/unavail│ │ items (JSON/M2M) │ │ reason │ └──────────────────┘ └───────────────────┘ ┌───────────────────┐ │ StockLocation │ │ id (PK) │ │ account_id (FK) │ │ name │ │ address │ │ is_default │ └───────────────────┘ ┌───────────────────┐ │ AuditLog │ │ id (PK) │ │ account_id (FK) │ │ user_id (FK) │ │ action │ │ entity_type │ │ entity_id │ │ details (JSON) │ │ timestamp │ └───────────────────┘ ``` ## 2. Table Definitions ### 2.1 Account | Column | Type | Constraints | |--------|------|-------------| | id | UUID | PRIMARY KEY, DEFAULT gen_random_uuid() (on PG) | | name | VARCHAR(255) | NOT NULL | | created_at | TIMESTAMP WITH TIME ZONE | NOT NULL, DEFAULT now() | **Indexes:** None extra beyond PK. --- ### 2.2 User | Column | Type | Constraints | |--------|------|-------------| | id | UUID | PK | | account_id | UUID | NOT NULL, FK → Account(id) | | email | VARCHAR(255) | NOT NULL, UNIQUE | | full_name | VARCHAR(255) | NOT NULL | | password_hash | VARCHAR(255) | NOT NULL | | role_id | UUID | NOT NULL, FK → Role(id) | | is_active | BOOLEAN | NOT NULL, DEFAULT TRUE | | created_at | TIMESTAMPTZ | NOT NULL, DEFAULT now() | | updated_at | TIMESTAMPTZ | NOT NULL, DEFAULT now() | **Indexes:** - `ix_user_account_id` on `account_id` - `ix_user_email` UNIQUE on `email` --- ### 2.3 Role | Column | Type | Constraints | |--------|------|-------------| | id | UUID | PK | | account_id | UUID | NOT NULL, FK → Account(id) | | name | VARCHAR(100) | NOT NULL | | permissions | JSONB | NOT NULL (array of strings like `["projects:read", "projects:write"]`) | **Indexes:** - `ix_role_account_id` on `account_id` --- ### 2.4 Tag | Column | Type | Constraints | |--------|------|-------------| | id | UUID | PK | | account_id | UUID | NOT NULL, FK → Account(id) | | name | VARCHAR(100) | NOT NULL | | color | VARCHAR(7) | NULLABLE (hex color) | **Indexes:** - `ix_tag_account_id` on `account_id` **Relationships:** - Many-to-Many with Equipment, Project, Crew (via join tables: `equipment_tags`, `project_tags`, `crew_tags`). --- ### 2.5 Contact | Column | Type | Constraints | |--------|------|-------------| | id | UUID | PK | | account_id | UUID | NOT NULL, FK → Account(id) | | type | ENUM('company','person') | NOT NULL | | company_name | VARCHAR(255) | NULLABLE | | first_name | VARCHAR(100) | NULLABLE | | last_name | VARCHAR(100) | NULLABLE | | email | VARCHAR(255) | NULLABLE | | phone | VARCHAR(50) | NULLABLE | | mobile | VARCHAR(50) | NULLABLE | | street | VARCHAR(255) | NULLABLE | | number | VARCHAR(20) | NULLABLE | | postalcode | VARCHAR(20) | NULLABLE | | city | VARCHAR(100) | NULLABLE | | country | VARCHAR(100) | NULLABLE | | tax_number | VARCHAR(50) | NULLABLE | | note | TEXT | NULLABLE | **Indexes:** - `ix_contact_account_id` on `account_id` - `ix_contact_type` on `type` --- ### 2.6 Document | Column | Type | Constraints | |--------|------|-------------| | id | UUID | PK | | account_id | UUID | NOT NULL, FK → Account(id) | | name | VARCHAR(255) | NOT NULL | | original_filename | VARCHAR(255) | NOT NULL | | file_path | VARCHAR(1024) | NOT NULL (relative to storage root) | | mime_type | VARCHAR(127) | NOT NULL | | size_bytes | INTEGER | NOT NULL | | project_id | UUID | NULLABLE, FK → Project(id) | | uploaded_by | UUID | NOT NULL, FK → User(id) | | created_at | TIMESTAMPTZ | NOT NULL, DEFAULT now() | **Indexes:** - `ix_document_account_id` on `account_id` - `ix_document_project_id` on `project_id` --- ### 2.7 Project | Column | Type | Constraints | |--------|------|-------------| | id | UUID | PK | | account_id | UUID | NOT NULL, FK → Account(id) | | name | VARCHAR(255) | NOT NULL | | reference | VARCHAR(50) | NULLABLE | | number | VARCHAR(50) | NOT NULL, automatically generated (prefix + sequential) | | displayname | VARCHAR(512) | GENERATED (name + number) | | customer_id | UUID | NULLABLE, FK → Contact(id) | | contact_person_id | UUID | NULLABLE, FK → Contact(id) | | account_manager_id | UUID | NULLABLE, FK → User(id) | | project_type | ENUM('rental','service','sale') | NULLABLE | | planperiod_start | TIMESTAMPTZ | NOT NULL | | planperiod_end | TIMESTAMPTZ | NOT NULL | | usageperiod_start | TIMESTAMPTZ | NOT NULL | | usageperiod_end | TIMESTAMPTZ | NOT NULL | | status | ENUM('request','planned','confirmed','active','completed','cancelled') | DEFAULT 'planned' | | location_name | VARCHAR(255) | NULLABLE | | location_street | VARCHAR(255) | NULLABLE | | location_city | VARCHAR(100) | NULLABLE | | location_postalcode | VARCHAR(20) | NULLABLE | | location_country | VARCHAR(100) | NULLABLE | | remark | TEXT | NULLABLE | | price | DECIMAL(12,2) | NULLABLE (calculated field, updated by triggers) | | cost | DECIMAL(12,2) | NULLABLE | | created_at | TIMESTAMPTZ | NOT NULL, DEFAULT now() | | updated_at | TIMESTAMPTZ | NOT NULL, DEFAULT now() | | archived | BOOLEAN | NOT NULL, DEFAULT FALSE | **Indexes:** - `ix_project_account_id` on `account_id` - `ix_project_status` on `status` - `ix_project_planperiod` on `planperiod_start`, `planperiod_end` (for availability queries) - `ix_project_customer_id` on `customer_id` --- ### 2.8 Subproject | Column | Type | Constraints | |--------|------|-------------| | id | UUID | PK | | project_id | UUID | NOT NULL, FK → Project(id) ON DELETE CASCADE | | name | VARCHAR(255) | NOT NULL | | custom_fields | JSONB | NULLABLE | **Indexes:** - `ix_subproject_project_id` on `project_id` --- ### 2.9 ProjectFunctionGroup | Column | Type | Constraints | |--------|------|-------------| | id | UUID | PK | | project_id | UUID | NOT NULL, FK → Project(id) ON DELETE CASCADE | | subproject_id | UUID | NULLABLE, FK → Subproject(id) | | name | VARCHAR(255) | NOT NULL | | usageperiod_start | TIMESTAMPTZ | NULLABLE | | usageperiod_end | TIMESTAMPTZ | NULLABLE | | planperiod_start | TIMESTAMPTZ | NULLABLE | | planperiod_end | TIMESTAMPTZ | NULLABLE | | remark | TEXT | NULLABLE | **Indexes:** - `ix_pfg_project_id` on `project_id` --- ### 2.10 ProjectFunction | Column | Type | Constraints | |--------|------|-------------| | id | UUID | PK | | project_id | UUID | NOT NULL, FK → Project(id) ON DELETE CASCADE | | subproject_id | UUID | NULLABLE, FK → Subproject(id) | | group_id | UUID | NULLABLE, FK → ProjectFunctionGroup(id) | | name | VARCHAR(255) | NOT NULL | | type | ENUM('crew_function','equipment_function','transport_function') | NOT NULL | | amount | INTEGER | DEFAULT 1 | | usageperiod_start | TIMESTAMPTZ | NULLABLE | | usageperiod_end | TIMESTAMPTZ | NULLABLE | | planperiod_start | TIMESTAMPTZ | NULLABLE | | planperiod_end | TIMESTAMPTZ | NULLABLE | | cost_rate | DECIMAL(10,2) | NULLABLE | | price_rate | DECIMAL(10,2) | NULLABLE | | remark | TEXT | NULLABLE | **Indexes:** - `ix_projectfunction_project_id` on `project_id` - `ix_projectfunction_group_id` on `group_id` --- ### 2.11 ProjectCrew | Column | Type | Constraints | |--------|------|-------------| | id | UUID | PK | | project_id | UUID | NOT NULL, FK → Project(id) | | function_id | UUID | NOT NULL, FK → ProjectFunction(id) ON DELETE CASCADE | | crew_id | UUID | NOT NULL, FK → Crew(id) | | start | TIMESTAMPTZ | NULLABLE (can default to project usage period) | | end | TIMESTAMPTZ | NULLABLE | **Indexes:** - `ix_projectcrew_project_id` on `project_id` - `ix_projectcrew_function_id` on `function_id` - `ix_projectcrew_crew_id` on `crew_id` --- ### 2.12 ProjectEquipmentGroup | Column | Type | Constraints | |--------|------|-------------| | id | UUID | PK | | project_id | UUID | NOT NULL, FK → Project(id) ON DELETE CASCADE | | subproject_id | UUID | NULLABLE, FK → Subproject(id) | | name | VARCHAR(255) | NOT NULL | | usageperiod_start | TIMESTAMPTZ | NULLABLE | | usageperiod_end | TIMESTAMPTZ | NULLABLE | | planperiod_start | TIMESTAMPTZ | NULLABLE | | planperiod_end | TIMESTAMPTZ | NULLABLE | | in_price_calculation | BOOLEAN | DEFAULT TRUE | | remark | TEXT | NULLABLE | **Indexes:** - `ix_peg_project_id` on `project_id` --- ### 2.13 ProjectEquipment | Column | Type | Constraints | |--------|------|-------------| | id | UUID | PK | | project_id | UUID | NOT NULL, FK → Project(id) | | equipment_group_id | UUID | NOT NULL, FK → ProjectEquipmentGroup(id) ON DELETE CASCADE | | equipment_id | UUID | NULLABLE, FK → Equipment(id) | | name | VARCHAR(255) | NOT NULL (can be free-text if equipment_id is NULL) | | quantity | INTEGER | NOT NULL, DEFAULT 1 | | quantity_total | INTEGER | NULLABLE (available inventory count) | | unit_price | DECIMAL(10,2) | NULLABLE | | discount | DECIMAL(5,2) | NULLABLE (percentage) | | external_remark | TEXT | NULLABLE | | internal_remark | TEXT | NULLABLE | **Indexes:** - `ix_projectequipment_group_id` on `equipment_group_id` - `ix_projectequipment_equipment_id` on `equipment_id` --- ### 2.14 Equipment (Catalog) | Column | Type | Constraints | |--------|------|-------------| | id | UUID | PK | | account_id | UUID | NOT NULL, FK → Account(id) | | name | VARCHAR(255) | NOT NULL | | category | VARCHAR(100) | NULLABLE | | brand | VARCHAR(100) | NULLABLE | | model | VARCHAR(100) | NULLABLE | | serial_number | VARCHAR(100) | NULLABLE | | barcode | VARCHAR(100) | NULLABLE, UNIQUE | | qr_code | VARCHAR(100) | NULLABLE, UNIQUE | | stock_location_id | UUID | NULLABLE, FK → StockLocation(id) | | status | ENUM('available','maintenance','defect','retired') | DEFAULT 'available' | | purchase_price | DECIMAL(10,2) | NULLABLE | | current_value | DECIMAL(10,2) | NULLABLE | | weight_kg | DECIMAL(10,2) | NULLABLE | | dimensions_cm | VARCHAR(50) | NULLABLE | | power_watt | INTEGER | NULLABLE | | custom_fields | JSONB | NULLABLE | | notes | TEXT | NULLABLE | | image_url | VARCHAR(2048) | NULLABLE | **Indexes:** - `ix_equipment_account_id` on `account_id` - `ix_equipment_category` on `category` - `ix_equipment_status` on `status` - `ix_equipment_barcode` UNIQUE on `barcode` - `ix_equipment_qrcode` UNIQUE on `qr_code` --- ### 2.15 StockLocation | Column | Type | Constraints | |--------|------|-------------| | id | UUID | PK | | account_id | UUID | NOT NULL, FK → Account(id) | | name | VARCHAR(100) | NOT NULL | | address | VARCHAR(255) | NULLABLE | | is_default | BOOLEAN | DEFAULT FALSE | **Indexes:** - `ix_stocklocation_account_id` on `account_id` --- ### 2.16 Crew | Column | Type | Constraints | |--------|------|-------------| | id | UUID | PK | | account_id | UUID | NOT NULL, FK → Account(id) | | first_name | VARCHAR(100) | NOT NULL | | last_name | VARCHAR(100) | NOT NULL | | email | VARCHAR(255) | NULLABLE | | phone | VARCHAR(50) | NULLABLE | | type | ENUM('internal','freelancer') | NOT NULL | | user_id | UUID | NULLABLE, FK → User(id) (if they have login) | | hourly_rate | DECIMAL(10,2) | NULLABLE | | daily_rate | DECIMAL(10,2) | NULLABLE | | skills | JSONB | NULLABLE (array of strings) | | is_active | BOOLEAN | DEFAULT TRUE | | note | TEXT | NULLABLE | **Indexes:** - `ix_crew_account_id` on `account_id` - `ix_crew_type` on `type` - `ix_crew_is_active` on `is_active` --- ### 2.17 CrewAvailability | Column | Type | Constraints | |--------|------|-------------| | id | UUID | PK | | crew_id | UUID | NOT NULL, FK → Crew(id) ON DELETE CASCADE | | start | TIMESTAMPTZ | NOT NULL | | end | TIMESTAMPTZ | NOT NULL | | type | ENUM('available','unavailable','tentative') | DEFAULT 'unavailable' | | reason | VARCHAR(255) | NULLABLE | **Indexes:** - `ix_crewavailability_crew_id` on `crew_id` - `ix_crewavailability_period` on `start`, `end` (for overlap checks) --- ### 2.18 Vehicle | Column | Type | Constraints | |--------|------|-------------| | id | UUID | PK | | account_id | UUID | NOT NULL, FK → Account(id) | | name | VARCHAR(100) | NOT NULL | | license_plate | VARCHAR(20) | NULLABLE | | type | VARCHAR(50) | NULLABLE | | payload_kg | INTEGER | NULLABLE | | volume_m3 | DECIMAL(10,2) | NULLABLE | | is_active | BOOLEAN | DEFAULT TRUE | **Indexes:** - `ix_vehicle_account_id` on `account_id` --- ### 2.19 VehicleAssignment | Column | Type | Constraints | |--------|------|-------------| | id | UUID | PK | | vehicle_id | UUID | NOT NULL, FK → Vehicle(id) | | project_id | UUID | NOT NULL, FK → Project(id) | | start | TIMESTAMPTZ | NOT NULL | | end | TIMESTAMPTZ | NOT NULL | | driver_id | UUID | NULLABLE, FK → Crew(id) | **Indexes:** - `ix_vehicleassignment_vehicle_id` on `vehicle_id` - `ix_vehicleassignment_project_id` on `project_id` - `ix_vehicleassignment_period` on `start`, `end` --- ### 2.20 ProjectRequest | Column | Type | Constraints | |--------|------|-------------| | id | UUID | PK | | account_id | UUID | NOT NULL, FK → Account(id) | | name | VARCHAR(255) | NOT NULL | | contact_name | VARCHAR(255) | NULLABLE | | contact_person_first_name | VARCHAR(100) | NULLABLE | | contact_person_lastname | VARCHAR(100) | NULLABLE | | contact_person_email | VARCHAR(255) | NULLABLE | | location_name | VARCHAR(255) | NULLABLE | | location_mailing_street | VARCHAR(255) | NULLABLE | | location_mailing_number | VARCHAR(20) | NULLABLE | | location_mailing_postalcode | VARCHAR(20) | NULLABLE | | location_mailing_city | VARCHAR(100) | NULLABLE | | location_mailing_country | VARCHAR(100) | NULLABLE | | planperiod_start | TIMESTAMPTZ | NULLABLE | | planperiod_end | TIMESTAMPTZ | NULLABLE | | usageperiod_start | TIMESTAMPTZ | NULLABLE | | usageperiod_end | TIMESTAMPTZ | NULLABLE | | status | ENUM('new','processing','converted','rejected') | DEFAULT 'new' | | remark | TEXT | NULLABLE | | created_at | TIMESTAMPTZ | DEFAULT now() | **Indexes:** - `ix_projectrequest_account_id` on `account_id` - `ix_projectrequest_status` on `status` --- ### 2.21 ProjectRequestEquipment | Column | Type | Constraints | |--------|------|-------------| | id | UUID | PK | | project_request_id | UUID | NOT NULL, FK → ProjectRequest(id) ON DELETE CASCADE | | name | VARCHAR(255) | NOT NULL | | quantity | INTEGER | NOT NULL | | quantity_total | INTEGER | NULLABLE | | unit_price | DECIMAL(10,2) | NULLABLE | | linked_equipment_id | UUID | NULLABLE, FK → Equipment(id) | **Indexes:** - `ix_prequestequip_request_id` on `project_request_id` --- ### 2.22 Quote | Column | Type | Constraints | |--------|------|-------------| | id | UUID | PK | | account_id | UUID | NOT NULL, FK → Account(id) | | project_id | UUID | NOT NULL, FK → Project(id) | | number | VARCHAR(50) | NOT NULL (auto-generated) | | status | ENUM('draft','sent','accepted','rejected') | DEFAULT 'draft' | | valid_until | DATE | NULLABLE | | template | VARCHAR(50) | NULLABLE | | content_html | TEXT | NULLABLE (generated PDF content) | | total_net | DECIMAL(12,2) | NULLABLE | | total_gross | DECIMAL(12,2) | NULLABLE | | created_at | TIMESTAMPTZ | DEFAULT now() | **Indexes:** - `ix_quote_account_id` on `account_id` - `ix_quote_project_id` on `project_id` - `ix_quote_status` on `status` --- ### 2.23 Invoice | Column | Type | Constraints | |--------|------|-------------| | id | UUID | PK | | account_id | UUID | NOT NULL, FK → Account(id) | | project_id | UUID | NOT NULL, FK → Project(id) | | number | VARCHAR(50) | NOT NULL (auto-generated) | | status | ENUM('draft','sent','paid','overdue','cancelled') | DEFAULT 'draft' | | due_date | DATE | NULLABLE | | total_net | DECIMAL(12,2) | NULLABLE | | total_gross | DECIMAL(12,2) | NULLABLE | | created_at | TIMESTAMPTZ | DEFAULT now() | **Indexes:** - `ix_invoice_account_id` on `account_id` - `ix_invoice_project_id` on `project_id` - `ix_invoice_status` on `status` --- ### 2.24 Template | Column | Type | Constraints | |--------|------|-------------| | id | UUID | PK | | account_id | UUID | NOT NULL, FK → Account(id) | | type | ENUM('quote','invoice','packing_list','delivery_note') | NOT NULL | | name | VARCHAR(100) | NOT NULL | | content_html | TEXT | NOT NULL | | is_default | BOOLEAN | DEFAULT FALSE | **Indexes:** - `ix_template_account_id` on `account_id` --- ### 2.25 Webhook | Column | Type | Constraints | |--------|------|-------------| | id | UUID | PK | | account_id | UUID | NOT NULL, FK → Account(id) | | url | VARCHAR(2048) | NOT NULL | | secret | VARCHAR(255) | NOT NULL | | events | JSONB | NOT NULL (array of event names) | | is_active | BOOLEAN | DEFAULT TRUE | **Indexes:** - `ix_webhook_account_id` on `account_id` --- ### 2.26 WebhookLog | Column | Type | Constraints | |--------|------|-------------| | id | UUID | PK | | webhook_id | UUID | NOT NULL, FK → Webhook(id) ON DELETE CASCADE | | event | VARCHAR(100) | NOT NULL | | request_body | JSONB | NOT NULL | | response_status | INTEGER | NOT NULL | | response_body | TEXT | NULLABLE | | success | BOOLEAN | NOT NULL | | created_at | TIMESTAMPTZ | DEFAULT now() | **Indexes:** - `ix_webhooklog_webhook_id` on `webhook_id` --- ### 2.27 AuditLog | Column | Type | Constraints | |--------|------|-------------| | id | UUID | PK | | account_id | UUID | NOT NULL, FK → Account(id) | | user_id | UUID | NOT NULL, FK → User(id) | | action | VARCHAR(50) | NOT NULL (e.g., 'create', 'update', 'delete') | | entity_type | VARCHAR(50) | NOT NULL (e.g., 'project', 'equipment') | | entity_id | UUID | NOT NULL | | details | JSONB | NULLABLE (changed fields) | | timestamp | TIMESTAMPTZ | NOT NULL, DEFAULT now() | **Indexes:** - `ix_auditlog_account_id` on `account_id` - `ix_auditlog_user_id` on `user_id` - `ix_auditlog_entity` on `entity_type`, `entity_id` - `ix_auditlog_timestamp` on `timestamp` --- ### 2.28 EquipmentGroup (Bundle) | Column | Type | Constraints | |--------|------|-------------| | id | UUID | PK | | account_id | UUID | NOT NULL, FK → Account(id) | | name | VARCHAR(255) | NOT NULL | **Relationships:** - Items stored in junction table `equipment_group_items` with columns `equipment_group_id (FK)`, `equipment_id (FK)`, `quantity` (INTEGER). --- ## 3. Junction / Many-to-Many Tables ### 3.1 EquipmentGroupItems | Column | Type | Constraints | |--------|------|-------------| | equipment_group_id | UUID | NOT NULL, FK → EquipmentGroup(id) ON DELETE CASCADE | | equipment_id | UUID | NOT NULL, FK → Equipment(id) ON DELETE CASCADE | | quantity | INTEGER | NOT NULL | | PRIMARY KEY (equipment_group_id, equipment_id) | ### 3.2 EquipmentTags | Column | Type | Constraints | |--------|------|-------------| | equipment_id | UUID | NOT NULL, FK → Equipment(id) ON DELETE CASCADE | | tag_id | UUID | NOT NULL, FK → Tag(id) ON DELETE CASCADE | | PRIMARY KEY (equipment_id, tag_id) | ### 3.3 ProjectTags | Column | Type | Constraints | |--------|------|-------------| | project_id | UUID | NOT NULL, FK → Project(id) ON DELETE CASCADE | | tag_id | UUID | NOT NULL, FK → Tag(id) ON DELETE CASCADE | | PRIMARY KEY (project_id, tag_id) | ### 3.4 CrewTags | Column | Type | Constraints | |--------|------|-------------| | crew_id | UUID | NOT NULL, FK → Crew(id) ON DELETE CASCADE | | tag_id | UUID | NOT NULL, FK → Tag(id) ON DELETE CASCADE | | PRIMARY KEY (crew_id, tag_id) | --- ## 4. Migration Strategy We use Alembic for database migrations. The migration environment is configured with `async` support for SQLAlchemy (since our backend uses async sessions). Initial migration creates all tables with UUID generation appropriate to the dialect: - PostgreSQL: `server_default=func.gen_random_uuid()` - SQLite: a Python side UUID generation on insert (via model default) For development, SQLite will be used; however, tests should run against PostgreSQL to catch dialect-specific issues early. --- ## 5. Key Database Constraints - **Unique constraints:** User `email` globally; Equipment `barcode` and `qr_code` per account (not globally, since multi-tenant). We'll make `(account_id, barcode)` unique if needed. - **Foreign key cascades:** Sub-objects (subprojects, functions, equipment groups, crew assignments) should be deleted when the parent project is deleted (CASCADE). AuditLog and other history are kept (NO ACTION or SET NULL). - **Check constraints:** Project `planperiod_end >= planperiod_start`, etc. - **Default values:** Most ENUMs have a sensible default.