Database Schema
Audit Fields
All tables include these audit fields for tracking creation and modifications:
| Column |
Type |
Constraints |
Description |
_created_at |
TIMESTAMP |
NOT NULL |
UTC timestamp when record was created |
_updated_at |
TIMESTAMP |
NULL |
UTC timestamp when record was last updated (auto via DB trigger) |
_user_created |
TEXT |
NOT NULL |
ID or type of user who created the record (e.g., UUID, "SYSTEM", "WEBHOOK:AERODATABOX") |
_user_updated |
TEXT |
NULL |
ID or type of user who last modified the record |
Supported Actor Types for _user_created / _user_updated
| Actor Type |
Example |
Description |
| UUID |
550e8400-e29b-41d4-a716-446655440000 |
Human user ID |
SYSTEM |
SYSTEM |
System operations |
WEBHOOK:AERODATABOX |
WEBHOOK:AERODATABOX |
Flight webhook updates |
IMPORT |
IMPORT |
Data import operations |
SEED |
SEED |
Database seeding |
MIGRATION |
MIGRATION |
Migration scripts |
Auto-Update Trigger
A PostgreSQL trigger automatically updates _updated_at when a row is modified:
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW._updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ language 'plpgsql';
CREATE TRIGGER update_user_updated_at
BEFORE UPDATE ON user
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
Core Tables
tenant
| Column |
Type |
Constraints |
| id |
UUID |
PRIMARY KEY |
| name |
VARCHAR(255) |
NOT NULL |
| flight_notification_level |
VARCHAR(50) |
NOT NULL DEFAULT 'basic' |
| _created_at |
TIMESTAMP |
NOT NULL |
| _updated_at |
TIMESTAMP |
NULL |
| _user_created |
TEXT |
NOT NULL |
| _user_updated |
TEXT |
NULL |
role
| Column |
Type |
Constraints |
| id |
UUID |
PRIMARY KEY |
| name |
VARCHAR(255) |
NOT NULL |
| permissions |
TEXT |
NOT NULL |
| _created_at |
TIMESTAMP |
NOT NULL |
| _updated_at |
TIMESTAMP |
NULL |
| _user_created |
TEXT |
NOT NULL |
| _user_updated |
TEXT |
NULL |
"user"
The user table requires quoting in SQL as it's a reserved word.
| Column |
Type |
Constraints |
| id |
UUID |
PRIMARY KEY |
| email |
VARCHAR(255) |
UNIQUE, NOT NULL, INDEX |
| hashed_password |
VARCHAR(255) |
NOT NULL |
| is_active |
BOOLEAN |
NOT NULL DEFAULT true |
| tenant_id |
UUID |
FK → tenant(id), INDEX |
| verification_code |
VARCHAR(10) |
|
| verification_code_expires |
TIMESTAMP |
|
| reset_code |
VARCHAR(10) |
|
| reset_code_expires |
TIMESTAMP |
|
| _created_at |
TIMESTAMP |
NOT NULL |
| _updated_at |
TIMESTAMP |
NULL |
| _user_created |
TEXT |
NOT NULL |
| _user_updated |
TEXT |
NULL |
user_tenant
| Column |
Type |
Constraints |
| user_id |
UUID |
FK → "user"(id), PRIMARY KEY |
| role_id |
UUID |
FK → role(id), PRIMARY KEY |
| _created_at |
TIMESTAMP |
NOT NULL |
| _updated_at |
TIMESTAMP |
NULL |
| _user_created |
TEXT |
NOT NULL |
| _user_updated |
TEXT |
NULL |
session
| Column |
Type |
Constraints |
| id |
UUID |
PRIMARY KEY |
| user_id |
UUID |
FK → "user"(id) |
| tenant_id |
UUID |
FK → tenant(id) |
| refresh_token_hash |
VARCHAR(255) |
NOT NULL |
| expires_at |
TIMESTAMP |
NOT NULL |
| revoked |
BOOLEAN |
NOT NULL DEFAULT false |
| _created_at |
TIMESTAMP |
NOT NULL |
| _updated_at |
TIMESTAMP |
NULL |
| _user_created |
TEXT |
NOT NULL |
| _user_updated |
TEXT |
NULL |
Shipments Tables
shipment
| Column |
Type |
Constraints |
| id |
UUID |
PRIMARY KEY |
| tracking_id |
VARCHAR(20) |
UNIQUE, NOT NULL, INDEX |
| sender_name |
VARCHAR(255) |
NOT NULL |
| sender_phone |
VARCHAR(50) |
NOT NULL |
| recipient_id |
UUID |
FK → recipient(id), INDEX |
| recipient_email |
VARCHAR(255) |
|
| recipient_phone |
VARCHAR(50) |
|
| origin |
VARCHAR(255) |
NOT NULL |
| destination |
VARCHAR(255) |
NOT NULL |
| description |
TEXT |
|
| weight |
DECIMAL(10,2) |
|
| price |
DECIMAL(10,2) |
|
| status |
VARCHAR(50) |
NOT NULL DEFAULT 'RECEIVED' |
| payment_status |
VARCHAR(50) |
NOT NULL DEFAULT 'PENDING' |
| delivery_proof_url |
TEXT |
|
| bag_id |
UUID |
FK → bag(id) |
| tenant_id |
UUID |
FK → tenant(id), INDEX |
| notification_whatsapp |
BOOLEAN |
DEFAULT true |
| notification_email |
BOOLEAN |
DEFAULT false |
| notification_push |
BOOLEAN |
DEFAULT false |
| sender_email |
VARCHAR(255) |
|
| device_token |
TEXT |
|
| last_notification_sent |
TIMESTAMP |
|
| estimated_arrival |
TIMESTAMP |
|
| status_changed_at |
TIMESTAMP |
|
| _created_at |
TIMESTAMP |
NOT NULL |
| _updated_at |
TIMESTAMP |
NULL |
| _user_created |
TEXT |
NOT NULL |
| _user_updated |
TEXT |
NULL |
bag
| Column |
Type |
Constraints |
| id |
UUID |
PRIMARY KEY |
| courier_id |
UUID |
FK → courier(id) |
| warehouse_id |
UUID |
INDEX |
| route |
VARCHAR(20) |
NOT NULL |
| departure_date |
TIMESTAMP |
NOT NULL |
| status |
VARCHAR(50) |
NOT NULL DEFAULT 'INIT' |
| airline_tag_code |
VARCHAR(10) |
|
| airline |
VARCHAR(255) |
DEFAULT '' |
| flight_number |
VARCHAR(50) |
DEFAULT '' |
| capacity_kg |
DECIMAL(10,2) |
DEFAULT 23.0 |
| current_weight |
DECIMAL(10,2) |
DEFAULT 0.0 |
| total_packages |
INTEGER |
DEFAULT 0 |
| estimated_arrival |
TIMESTAMP |
|
| actual_arrival |
TIMESTAMP |
|
| status_changed_at |
TIMESTAMP |
|
| tenant_id |
UUID |
FK → tenant(id), INDEX |
| _created_at |
TIMESTAMP |
NOT NULL |
| _updated_at |
TIMESTAMP |
NULL |
| _user_created |
TEXT |
NOT NULL |
| _user_updated |
TEXT |
NULL |
flight
| Column |
Type |
Constraints |
| id |
UUID |
PRIMARY KEY |
| origin |
VARCHAR(10) |
NOT NULL |
| destination |
VARCHAR(10) |
NOT NULL |
| departure_date |
TIMESTAMP |
NOT NULL |
| user_id |
UUID |
FK → "user"(id) |
| courier_id |
UUID |
FK → courier(id) |
| flight_number |
VARCHAR(50) |
DEFAULT '' |
| airline |
VARCHAR(255) |
DEFAULT '' |
| airline_iata |
VARCHAR(10) |
DEFAULT '' |
| departure_airport |
VARCHAR(10) |
DEFAULT '' |
| arrival_airport |
VARCHAR(10) |
DEFAULT '' |
| scheduled_departure |
TIMESTAMP |
|
| scheduled_arrival |
TIMESTAMP |
|
| estimated_departure |
TIMESTAMP |
|
| estimated_arrival |
TIMESTAMP |
|
| actual_departure |
TIMESTAMP |
|
| actual_arrival |
TIMESTAMP |
|
| flight_status |
VARCHAR(50) |
DEFAULT 'unknown' |
| max_weight_kg |
DECIMAL(10,2) |
DEFAULT 23.0 |
| available_weight_kg |
DECIMAL(10,2) |
DEFAULT 23.0 |
| last_flight_update |
TIMESTAMP |
|
| bag_id |
UUID |
FK → bag(id) |
| tenant_id |
UUID |
FK → tenant(id), INDEX |
| status_changed_at |
TIMESTAMP |
|
| base_cost |
DECIMAL(10,2) |
DEFAULT 0.0 |
| _created_at |
TIMESTAMP |
NOT NULL |
| _updated_at |
TIMESTAMP |
NULL |
| _user_created |
TEXT |
NOT NULL |
| _user_updated |
TEXT |
NULL |
courier
| Column |
Type |
Constraints |
| id |
UUID |
PRIMARY KEY |
| user_id |
UUID |
FK → "user"(id), UNIQUE |
| tenant_id |
UUID |
FK → tenant(id), INDEX |
| full_name |
VARCHAR(255) |
NOT NULL |
| passport_number |
VARCHAR(50) |
NOT NULL |
| nationality |
VARCHAR(10) |
DEFAULT '' |
| phone |
VARCHAR(50) |
DEFAULT '' |
| email |
VARCHAR(255) |
DEFAULT '' |
| flights_completed |
INTEGER |
DEFAULT 0 |
| packages_delivered |
INTEGER |
DEFAULT 0 |
| rating |
DECIMAL(3,2) |
DEFAULT 5.0 |
| no_show_count |
INTEGER |
DEFAULT 0 |
| status |
VARCHAR(50) |
DEFAULT 'ACTIVE' |
| home_country |
VARCHAR(10) |
DEFAULT '' |
| preferred_routes |
TEXT |
DEFAULT '[]' |
| _created_at |
TIMESTAMP |
NOT NULL |
| _updated_at |
TIMESTAMP |
NULL |
| _user_created |
TEXT |
NOT NULL |
| _user_updated |
TEXT |
NULL |
package_event
| Column |
Type |
Constraints |
| id |
UUID |
PRIMARY KEY |
| tenant_id |
UUID |
FK → tenant(id), INDEX |
| shipment_id |
UUID |
FK → shipment(id), INDEX |
| event_type |
VARCHAR(50) |
NOT NULL, INDEX |
| timestamp |
TIMESTAMP |
NOT NULL |
| warehouse_id |
UUID |
|
| actor_id |
UUID |
FK → "user"(id) |
| evidence_url |
TEXT |
|
| event_metadata |
TEXT |
|
| _created_at |
TIMESTAMP |
NOT NULL |
| _updated_at |
TIMESTAMP |
NULL |
| _user_created |
TEXT |
NOT NULL |
| _user_updated |
TEXT |
NULL |
bag_assignment
| Column |
Type |
Constraints |
| id |
UUID |
PRIMARY KEY |
| shipment_id |
UUID |
FK → shipment(id), INDEX |
| bag_id |
UUID |
FK → bag(id), INDEX |
| added_at |
TIMESTAMP |
NOT NULL |
| added_by |
UUID |
FK → "user"(id) |
| position |
INTEGER |
|
| removed |
BOOLEAN |
DEFAULT false |
| _created_at |
TIMESTAMP |
NOT NULL |
| _updated_at |
TIMESTAMP |
NULL |
| _user_created |
TEXT |
NOT NULL |
| _user_updated |
TEXT |
NULL |
package
| Column |
Type |
Constraints |
| id |
UUID |
PRIMARY KEY |
| tenant_id |
UUID |
FK → tenant(id), INDEX |
| shipment_id |
UUID |
FK → shipment(id), INDEX |
| qr_code |
VARCHAR(255) |
UNIQUE, INDEX |
| weight |
DECIMAL(10,2) |
NOT NULL |
| category |
VARCHAR(50) |
DEFAULT 'general' |
| status |
VARCHAR(50) |
DEFAULT 'CREATED' |
| status_changed_at |
TIMESTAMP |
|
| _created_at |
TIMESTAMP |
NOT NULL |
| _updated_at |
TIMESTAMP |
NULL |
| _user_created |
TEXT |
NOT NULL |
| _user_updated |
TEXT |
NULL |
bag_shipment
| Column |
Type |
Constraints |
| bag_id |
UUID |
FK → bag(id), PRIMARY KEY |
| shipment_id |
UUID |
FK → shipment(id), PRIMARY KEY |
| assigned_at |
TIMESTAMP |
NOT NULL |
| _created_at |
TIMESTAMP |
NOT NULL |
| _updated_at |
TIMESTAMP |
NULL |
| _user_created |
TEXT |
NOT NULL |
| _user_updated |
TEXT |
NULL |
bag_flight
| Column |
Type |
Constraints |
| bag_id |
UUID |
FK → bag(id), PRIMARY KEY |
| flight_id |
UUID |
FK → flight(id), PRIMARY KEY |
| extra_cost |
DECIMAL(10,2) |
DEFAULT 0.0 |
| assigned_at |
TIMESTAMP |
NOT NULL |
| _created_at |
TIMESTAMP |
NOT NULL |
| _updated_at |
TIMESTAMP |
NULL |
| _user_created |
TEXT |
NOT NULL |
| _user_updated |
TEXT |
NULL |
flight_alert_subscription
| Column |
Type |
Constraints |
| id |
UUID |
PRIMARY KEY |
| flight_id |
UUID |
FK → flight(id), INDEX |
| external_subscription_id |
VARCHAR(255) |
DEFAULT '' |
| flight_number |
VARCHAR(50) |
INDEX |
| departure_date |
TIMESTAMP |
NOT NULL |
| webhook_url |
VARCHAR(500) |
DEFAULT '' |
| expires_at |
TIMESTAMP |
|
| is_active |
BOOLEAN |
DEFAULT true |
| _created_at |
TIMESTAMP |
NOT NULL |
| _updated_at |
TIMESTAMP |
NULL |
| _user_created |
TEXT |
NOT NULL |
| _user_updated |
TEXT |
NULL |
bag_incidence
| Column |
Type |
Constraints |
| id |
UUID |
PRIMARY KEY |
| bag_id |
UUID |
FK → bag(id), INDEX |
| missing_package_ids |
TEXT |
DEFAULT '[]' |
| reported_by |
UUID |
FK → "user"(id) |
| notes |
TEXT |
|
| _created_at |
TIMESTAMP |
NOT NULL |
| _updated_at |
TIMESTAMP |
NULL |
| _user_created |
TEXT |
NOT NULL |
| _user_updated |
TEXT |
NULL |
Warehouse Tables
warehouse
| Column |
Type |
Constraints |
| id |
UUID |
PRIMARY KEY |
| tenant_id |
UUID |
NOT NULL, INDEX |
| name |
VARCHAR(255) |
NOT NULL |
| code |
VARCHAR(50) |
NOT NULL, INDEX |
| country |
VARCHAR(2) |
NOT NULL |
| city |
VARCHAR(255) |
NOT NULL |
| address |
TEXT |
NOT NULL |
| phone |
VARCHAR(50) |
DEFAULT '' |
| email |
VARCHAR(255) |
DEFAULT '' |
| status |
VARCHAR(50) |
DEFAULT 'ACTIVE' |
| latitude |
DECIMAL(10,8) |
|
| longitude |
DECIMAL(11,8) |
|
| max_storage_capacity |
INTEGER |
|
| current_storage_count |
INTEGER |
DEFAULT 0 |
| _created_at |
TIMESTAMP |
NOT NULL |
| _updated_at |
TIMESTAMP |
NULL |
| _user_created |
TEXT |
NOT NULL |
| _user_updated |
TEXT |
NULL |
package_received_event
| Column |
Type |
Constraints |
| id |
UUID |
PRIMARY KEY |
| tenant_id |
UUID |
NOT NULL, INDEX |
| shipment_id |
UUID |
NOT NULL, INDEX |
| warehouse_id |
UUID |
NOT NULL, INDEX |
| received_at |
TIMESTAMP |
NOT NULL |
| received_by |
UUID |
|
| condition |
VARCHAR(50) |
DEFAULT '' |
| notes |
TEXT |
DEFAULT '' |
| _created_at |
TIMESTAMP |
NOT NULL |
| _updated_at |
TIMESTAMP |
NULL |
| _user_created |
TEXT |
NOT NULL |
| _user_updated |
TEXT |
NULL |
Shared Tables
recipient
| Column |
Type |
Constraints |
| id |
UUID |
PRIMARY KEY |
| tenant_id |
UUID |
FK → tenant(id), INDEX |
| name |
VARCHAR(255) |
NOT NULL, INDEX |
| dni |
VARCHAR(20) |
INDEX |
| email |
VARCHAR(255) |
DEFAULT '' |
| phone |
VARCHAR(50) |
DEFAULT '' |
| _created_at |
TIMESTAMP |
NOT NULL |
| _updated_at |
TIMESTAMP |
NULL |
| _user_created |
TEXT |
NOT NULL |
| _user_updated |
TEXT |
NULL |
status_history
| Column |
Type |
Constraints |
| id |
UUID |
PRIMARY KEY |
| tenant_id |
UUID |
FK → tenant(id), INDEX |
| entity_type |
VARCHAR(50) |
NOT NULL, INDEX |
| entity_id |
UUID |
NOT NULL, INDEX |
| from_status |
VARCHAR(50) |
|
| to_status |
VARCHAR(50) |
NOT NULL |
| changed_at |
TIMESTAMP |
NOT NULL, INDEX |
| changed_by |
VARCHAR(255) |
|
| trigger_type |
VARCHAR(50) |
NOT NULL |
| metadata |
JSONB |
|
| _created_at |
TIMESTAMP |
NOT NULL |
| _updated_at |
TIMESTAMP |
NULL |
| _user_created |
TEXT |
NOT NULL |
| _user_updated |
TEXT |
NULL |
scan
QR scan records for tracking package/bag scans by users.
| Column |
Type |
Constraints |
| id |
UUID |
PRIMARY KEY |
| user_id |
UUID |
NOT NULL, INDEX |
| tenant_id |
UUID |
NOT NULL, INDEX |
| entity_type |
VARCHAR(50) |
NULL (pkg, bag) |
| entity_id |
UUID |
NULL, INDEX |
| qr_data |
JSONB |
NOT NULL DEFAULT '{}' |
| scanned_at |
TIMESTAMP |
NOT NULL |
| validated_at |
TIMESTAMP |
NULL |
| validation_result |
VARCHAR(50) |
NULL (valid, hmac_invalid, wrong_tenant, etc.) |
| hmac_valid |
BOOLEAN |
NULL |
| latitude |
DECIMAL(10,8) |
NULL |
| longitude |
DECIMAL(11,8) |
NULL |
| location_accuracy |
DECIMAL(10,2) |
NULL |
| app_version |
VARCHAR(20) |
NULL |
| platform |
VARCHAR(20) |
NULL |
| reviewed |
BOOLEAN |
NOT NULL DEFAULT false |
| reviewed_by |
UUID |
NULL |
| reviewed_at |
TIMESTAMP |
NULL |
| _created_at |
TIMESTAMP |
NOT NULL |
| _updated_at |
TIMESTAMP |
NULL |
| _user_created |
TEXT |
NOT NULL |
| _user_updated |
TEXT |
NULL |
Scan Validation Result Values
| Value |
Description |
valid |
QR code is valid and belongs to tenant |
hmac_invalid |
HMAC signature verification failed |
wrong_tenant |
QR code belongs to different tenant |
entity_not_found |
Package/bag no longer exists |
qr_decode_error |
QR data could not be parsed |
rate_limited |
User exceeded 100 scans/hour limit |
Row Level Security (RLS)
All tenant-scoped tables have RLS enabled:
ENABLE ROW LEVEL SECURITY;
FORCE ROW LEVEL SECURITY;
POLICY mandatory_tenant_policy ON table_name
USING (tenant_id = current_setting('app.current_tenant')::uuid)
WITH CHECK (tenant_id = current_setting('app.current_tenant')::uuid);
RLS Context Manager in Python:
class TenantContext:
async def __aenter__(self):
await conn.execute("SET LOCAL app.current_tenant = %s", tenant_id)
async def __aexit__(self, *args):
await conn.execute("RESET app.current_tenant")
Shipment Status Values
| Status |
Description |
RECEIVED |
Shipment received, awaiting bag assignment |
PACKING |
At least one package assigned to a bag |
PACKED |
Packed in bag |
TO_AIRPORT |
In transit to airport |
IN_FLY |
Flying to destination |
IN_AIRPORT |
Arrived at destination airport |
WITH_COURIER |
With courier for delivery |
IN_WAREHOUSE |
In destination warehouse |
CONSOLIDATING |
Consolidating with other packages |
CONSOLIDATED |
Consolidation complete |
DELIVERED |
Delivered to recipient |
RETURNED |
Returned to sender |
Payment Status Values
| Status |
Description |
PENDING |
Awaiting payment |
PAID |
Payment received |
REFUNDED |
Payment refunded |
Ver también: Domain Model · ER Diagram · Migrations