# Database Schema Documentation This document provides a comprehensive analysis of the Tour Builder Platform database schema, including all models, fields, relationships, constraints, and configurations. ## Overview - **Database**: PostgreSQL - **ORM**: Sequelize v6 - **Table Naming**: `freezeTableName: true` - table names match model names exactly - **Soft Delete**: All models use `paranoid: true` - records are soft-deleted via `deletedAt` timestamp - **Timestamps**: All models include `createdAt`, `updatedAt`, and `deletedAt` columns - **Primary Keys**: All models use UUID v4 as primary key ## Database Configuration Configuration is environment-based in `backend/src/db/db-config.ts`: | Environment | Database | Logging | |-------------|----------|---------| | production | `DB_*` env vars | Disabled | | development | `db_tour_builder_platform` | Console | | dev_stage | `DB_*` env vars | Console | **Migration Settings:** - Migration storage: `sequelize` (SequelizeMeta table) - Seeder storage: `sequelize` --- ## Entity-Relationship Overview ``` ┌──────────────┐ ┌─────────────────┐ ┌──────────────────┐ │ users │────<│project_memberships│>───│ projects │ └──────────────┘ └─────────────────┘ └──────────────────┘ │ │ │ app_roleId │ hasMany ▼ ▼ ┌──────────────┐ ┌──────────────────┐ │ roles │ │ tour_pages │ └──────────────┘ │ (ui_schema_json) │ │ └──────────────────┘ │ belongsToMany ▼ ┌──────────────┐ │ permissions │ └──────────────┘ ┌──────────────┐ │ assets │────< asset_variants └──────────────┘ ┌────────────────────────┐ snapshotted from ┌────────────────────────┐ │ project_element_defaults│ ─────────────────────── │ element_type_defaults │ └────────────────────────┘ (source_element_id) └────────────────────────┘ │ │ │ belongsTo │ (global defaults) ▼ │ ┌──────────────────┐ │ │ projects │<────────────────────────────────────────┘ └──────────────────┘ (templates for new projects) ``` **Note:** Page elements, navigation links, and transition videos are stored directly in `tour_pages.ui_schema_json` rather than in separate tables. This simplifies the data model and avoids ID remapping issues when publishing between environments. --- ## Core Models ### 1. users User accounts for authentication and authorization. | Field | Type | Constraints | Description | |-------|------|-------------|-------------| | `id` | UUID | PK, default: UUIDv4 | Primary identifier | | `firstName` | TEXT | nullable | User's first name | | `lastName` | TEXT | nullable | User's last name | | `phoneNumber` | TEXT | nullable | Contact phone | | `email` | TEXT | NOT NULL, UNIQUE | Email (validated) | | `disabled` | BOOLEAN | NOT NULL, default: false | Account disabled flag | | `password` | TEXT | NOT NULL | Bcrypt hashed password | | `emailVerified` | BOOLEAN | NOT NULL, default: false | Email verification status | | `emailVerificationToken` | TEXT | nullable | Token for email verification | | `emailVerificationTokenExpiresAt` | DATE | nullable | Token expiry | | `passwordResetToken` | TEXT | nullable | Password reset token | | `passwordResetTokenExpiresAt` | DATE | nullable | Reset token expiry | | `provider` | TEXT | NOT NULL, default: 'local' | Auth provider (local, google, microsoft) | | `importHash` | STRING(255) | UNIQUE, nullable | Import deduplication key | | `app_roleId` | UUID | FK → roles.id | User's application role | | `createdById` | UUID | FK → users.id | Record creator | | `updatedById` | UUID | FK → users.id | Last modifier | **Indexes:** - `email` (unique) - `app_roleId` - `deletedAt` **Associations:** - `belongsTo` roles (as `app_role`) - `hasMany` project_memberships - `hasMany` presigned_url_requests - `hasMany` publish_events - `hasMany` access_logs - `belongsToMany` permissions (as `custom_permissions`) - `hasMany` file (as `avatar`, polymorphic) **Hooks:** - `beforeCreate`: Trims string fields, auto-generates password for OAuth users, sets `emailVerified: true` for OAuth - `beforeUpdate`: Trims string fields --- ### 2. roles Application-level roles for RBAC. | Field | Type | Constraints | Description | |-------|------|-------------|-------------| | `id` | UUID | PK, default: UUIDv4 | Primary identifier | | `name` | TEXT | NOT NULL, len: 1-100 | Role name | | `role_customization` | TEXT | nullable | Custom role settings | | `importHash` | STRING(255) | UNIQUE, nullable | Import deduplication | **Associations:** - `hasMany` users (as `users_app_role`) - `belongsToMany` permissions (through `rolesPermissionsPermissions`) **Seeded Roles:** - Administrator (full access) - Platform Owner - Account Manager - Tour Designer - Content Reviewer - Analytics Viewer - Public (minimal access) --- ### 3. permissions Individual permission definitions for RBAC. | Field | Type | Constraints | Description | |-------|------|-------------|-------------| | `id` | UUID | PK, default: UUIDv4 | Primary identifier | | `name` | TEXT | NOT NULL, UNIQUE, len: 1-100 | Permission name | | `importHash` | STRING(255) | UNIQUE, nullable | Import deduplication | **Permission Naming Convention:** - `CREATE_` - Create records - `READ_` - Read records - `UPDATE_` - Modify records - `DELETE_` - Delete records - `READ_API_DOCS` - Access API documentation - `CREATE_SEARCH` - Perform searches **Seeded Entities with CRUD Permissions:** users, roles, permissions, projects, project_memberships, assets, asset_variants, presigned_url_requests, tour_pages, project_audio_tracks, publish_events, pwa_caches, access_logs, element_type_defaults, project_element_defaults Private production presentation access is intentionally not part of the generic CRUD permission seed set. It is represented by project visibility plus explicit customer grants in `production_presentation_access`. --- ### 4. projects Virtual tour projects - the main organizational unit. | Field | Type | Constraints | Description | |-------|------|-------------|-------------| | `id` | UUID | PK, default: UUIDv4 | Primary identifier | | `name` | TEXT | NOT NULL, len: 1-255 | Project name | | `slug` | TEXT | NOT NULL, UNIQUE, regex: `^[a-z0-9_-]+$/i`, len: 1-255 | URL-safe identifier | | `description` | TEXT | nullable | Project description | | `logo_url` | TEXT | nullable | Project logo URL | | `favicon_url` | TEXT | nullable | Favicon URL | | `og_image_url` | TEXT | nullable | Open Graph image URL | | `production_presentation_visibility` | ENUM | NOT NULL, default: 'public' | Production runtime visibility: `public`, `private` | | `design_width` | INTEGER | nullable, default: 1920 | Design canvas width (px) | | `design_height` | INTEGER | nullable, default: 1080 | Design canvas height (px) | | `importHash` | STRING(255) | UNIQUE, nullable | Import deduplication | **Indexes:** - `slug` (unique) - `deletedAt` **Associations:** - `hasMany` project_memberships - `hasMany` assets - `hasMany` presigned_url_requests - `hasMany` tour_pages - `hasMany` project_audio_tracks - `hasMany` project_element_defaults - `hasMany` publish_events - `hasMany` pwa_caches - `hasMany` access_logs - `hasMany` production_presentation_access - `hasMany` project_ui_control_settings **Cascade Behavior:** All child records are deleted when project is deleted. **Auto-Snapshot on Create:** When a project is created, all `element_type_defaults` records are automatically snapshotted to `project_element_defaults` for the new project. ### Global UI Controls Global UI controls configure the system-owned fullscreen, sound, and offline buttons. Defaults live in `global_ui_control_defaults`, project/environment overrides live in `project_ui_control_settings`, and page overrides live in `tour_pages.global_ui_controls_settings_json`. Sizes are stored as canvas-width-relative percentage fields (`buttonSizePercent`, `iconSizePercent`, `borderRadiusPercent`). Positions are stored as canvas-relative percentages (`xPercent`, `yPercent`) for stable placement across devices. Icons, background colors, and border colors are state-specific: `defaultIconUrl`/`activeIconUrl`, `defaultBackgroundColor`/`activeBackgroundColor`, and `defaultBorderColor`/`activeBorderColor`. #### `global_ui_control_defaults` Singleton table for platform-wide defaults. | Field | Type | Constraints | Description | |-------|------|-------------|-------------| | `id` | UUID | PK, default UUIDv4 | Primary identifier | | `settings_json` | JSON | NOT NULL | Defaults for `offline`, `fullscreen`, `sound` | | `createdById` | UUID | nullable FK users | Creator | | `updatedById` | UUID | nullable FK users | Last updater | | `createdAt`, `updatedAt`, `deletedAt` | DATE | paranoid timestamps | Lifecycle fields | #### `project_ui_control_settings` Project/environment-level overrides. | Field | Type | Constraints | Description | |-------|------|-------------|-------------| | `id` | UUID | PK, default UUIDv4 | Primary identifier | | `projectId` | UUID | NOT NULL, FK projects, cascade delete | Owning project | | `environment` | ENUM | NOT NULL: `dev`, `stage`, `production` | Content environment | | `source_key` | TEXT | nullable | Snapshot/publish/clone provenance | | `settings_json` | JSON | NOT NULL | Project-level UI-control overrides | | `importHash` | STRING(255) | nullable unique | Import deduplication | | `createdById`, `updatedById` | UUID | nullable FK users | Audit users | | `createdAt`, `updatedAt`, `deletedAt` | DATE | paranoid timestamps | Lifecycle fields | **Indexes:** - unique partial index on `("projectId", environment)` where `deletedAt IS NULL` - index on `deletedAt` --- ### 5. production_presentation_access Explicit customer access grants for private production presentations. | Field | Type | Constraints | Description | |-------|------|-------------|-------------| | `id` | UUID | PK, default: UUIDv4 | Primary identifier | | `projectId` | UUID | FK → projects.id, NOT NULL | Private production presentation project | | `userId` | UUID | FK → users.id, NOT NULL | Customer user allowed to view the presentation | | `createdById` | UUID | FK → users.id | Record creator | | `updatedById` | UUID | FK → users.id | Last modifier | | `importHash` | STRING(255) | UNIQUE, nullable | Import deduplication | **Indexes:** - `projectId` - `userId` - `projectId, userId` (unique for active rows where `deletedAt IS NULL`) **Access Rules:** - Public production projects do not require rows in this table. - Staff users with any RBAC permission can view every private production presentation. - Public-role customer users with no RBAC permissions need an active row for each private production presentation. --- ### 6. project_memberships Junction table linking users to projects with access levels. | Field | Type | Constraints | Description | |-------|------|-------------|-------------| | `id` | UUID | PK, default: UUIDv4 | Primary identifier | | `access_level` | ENUM | NOT NULL, default: 'viewer' | Access level: `owner`, `editor`, `reviewer`, `viewer` | | `is_active` | BOOLEAN | NOT NULL, default: false | Membership active status | | `invited_at` | DATE | nullable | Invitation timestamp | | `accepted_at` | DATE | nullable | Acceptance timestamp | | `projectId` | UUID | FK → projects.id | Associated project | | `userId` | UUID | FK → users.id | Associated user | | `importHash` | STRING(255) | UNIQUE, nullable | Import deduplication | **Indexes:** - `projectId` - `userId` - `projectId, userId` (unique composite) - `is_active` - `deletedAt` **Cascade Behavior:** Deleted when associated project or user is deleted. --- ## Tour Content Models ### 7. tour_pages Individual pages/scenes within a tour. | Field | Type | Constraints | Description | |-------|------|-------------|-------------| | `id` | UUID | PK, default: UUIDv4 | Primary identifier | | `environment` | ENUM | NOT NULL, default: 'dev' | Environment: `dev`, `stage`, `production` | | `source_key` | TEXT | nullable | Reference to source version when published | | `name` | TEXT | NOT NULL, len: 1-255 | Page display name | | `slug` | TEXT | NOT NULL, regex: `^[a-z0-9_-]+$/i`, len: 1-255 | URL-safe identifier | | `sort_order` | INTEGER | NOT NULL, default: 0 | Presentation display order; first sorted page is the entry page | | `background_image_url` | TEXT | nullable | Background image URL | | `background_video_url` | TEXT | nullable | Background video URL | | `background_embed_url` | TEXT | nullable | Background 360/embed URL | | `background_audio_url` | TEXT | nullable | Background audio URL | | `background_loop` | BOOLEAN | NOT NULL, default: false | Loop background media | | `background_video_autoplay` | BOOLEAN | NOT NULL, default: true | Autoplay background video | | `background_video_loop` | BOOLEAN | NOT NULL, default: true | Loop background video | | `background_video_muted` | BOOLEAN | NOT NULL, default: true | Mute background video | | `background_video_start_time` | DECIMAL(10,1) | nullable | Background video start time (seconds) | | `background_video_end_time` | DECIMAL(10,1) | nullable | Background video end time (seconds) | | `background_video_play_once` | BOOLEAN | NOT NULL, default: false | Play video only once per session (show last frame on revisit) | | `background_audio_autoplay` | BOOLEAN | NOT NULL, default: true | Autoplay background audio | | `background_audio_loop` | BOOLEAN | NOT NULL, default: true | Loop background audio | | `background_audio_start_time` | DECIMAL(10,1) | nullable | Background audio start time (seconds) | | `background_audio_end_time` | DECIMAL(10,1) | nullable | Background audio end time (seconds) | | `design_width` | INTEGER | nullable, default: null | Design canvas width (px) - copied from project on save | | `design_height` | INTEGER | nullable, default: null | Design canvas height (px) - copied from project on save | | `requires_auth` | BOOLEAN | NOT NULL, default: false | Requires authentication | | `ui_schema_json` | JSON | nullable | UI element schema | | `projectId` | UUID | FK → projects.id | Parent project | | `importHash` | STRING(255) | UNIQUE, nullable | Import deduplication | **Indexes:** - `projectId` - `projectId, environment, slug` (unique composite) - `projectId, environment, sort_order` - `deletedAt` **Ordering semantics:** - Constructor page reordering updates `sort_order` for dev pages only. - Constructor page duplication creates a new dev row with a unique slug, fresh page ID, fresh inline element IDs in `ui_schema_json`, and `sort_order = max(project dev sort_order) + 1`. - Constructor page deletion removes the dev row through the standard `DELETE /api/tour_pages/:id` endpoint; stage and production keep their previous copies until Save to Stage and Publish run. - Runtime loaders sort by `sort_order`; the first page after sorting is the presentation entry page. - Save to Stage copies dev `sort_order` to stage. Publish copies stage `sort_order` to production. **Associations:** - `belongsTo` projects (as `project`) **UI Schema JSON Structure:** The `ui_schema_json` field contains all page elements and navigation configuration: ```json { "elements": [{ "id": "unique-element-id", "type": "navigation_next", "name": "Next Page Button", "xPercent": 90, "yPercent": 85, "widthPercent": 8, "heightPercent": 10, "targetPageSlug": "page-2", "transitionVideoUrl": "assets/.../transition.mp4", "iconUrl": "assets/.../icon.png", "styleJson": { ... } }] } ``` **Element Types:** - `navigation_next` - Forward navigation button - `navigation_prev` - Back navigation button - `spot` - Hotspot/clickable area - `description` - Text description - `tooltip` - Hover tooltip - `gallery` - Image gallery - `carousel` - Image carousel - `logo` - Logo element - `video_player` - Video player - `audio_player` - Audio player - `popup` - Popup/modal --- ## Asset Management Models ### 7. assets Media files (images, videos, audio, documents) used in tours. | Field | Type | Constraints | Description | |-------|------|-------------|-------------| | `id` | UUID | PK, default: UUIDv4 | Primary identifier | | `name` | TEXT | nullable, len: 0-255 | Asset display name | | `asset_type` | ENUM | NOT NULL | Media type: `image`, `video`, `audio`, `file` | | `type` | ENUM | NOT NULL, default: 'general' | Usage type (see values below) | | `cdn_url` | TEXT | nullable | Public CDN URL | | `storage_key` | TEXT | nullable | S3/storage key | | `mime_type` | TEXT | nullable, validated | MIME type (e.g., `image/png`) | | `size_mb` | DECIMAL | nullable | File size in MB | | `width_px` | INTEGER | nullable | Width in pixels | | `height_px` | INTEGER | nullable | Height in pixels | | `duration_sec` | DECIMAL | nullable | Duration for audio/video | | `frame_rate` | DECIMAL | nullable | Video FPS from backend ffprobe | | `checksum` | TEXT | nullable | File checksum | | `is_public` | BOOLEAN | NOT NULL, default: false | Publicly accessible | | `projectId` | UUID | FK → projects.id | Parent project | | `importHash` | STRING(255) | UNIQUE, nullable | Import deduplication | **Asset Usage Types:** - `icon` - UI icons - `background_image` - Page backgrounds - `audio` - Audio files - `video` - Video files - `transition` - Transition videos - `logo` - Project logos - `favicon` - Favicon images - `document` - PDF/documents - `general` - General assets **Indexes:** - `projectId` - `storage_key` partial active-row index (`assets_storage_key_active`, where `deletedAt IS NULL` and `storage_key IS NOT NULL`) for transition/reversed-video lookup by canonical storage key - `asset_type` - `type` - `is_public` - `deletedAt` **Associations:** - `belongsTo` projects (as `project`) - `hasMany` asset_variants (as `asset_variants_asset`) --- ### 8. asset_variants Processed variants of assets (thumbnails, transcoded videos, reversed videos, etc.). | Field | Type | Constraints | Description | |-------|------|-------------|-------------| | `id` | UUID | PK, default: UUIDv4 | Primary identifier | | `variant_type` | ENUM | nullable | Variant type (see values below) | | `cdn_url` | TEXT | nullable, len: 0-2048, URL validated | Variant CDN URL | | `storage_key` | TEXT | nullable | Private storage path (e.g., `assets/{assetId}/reversed.mp4`) | | `width_px` | INTEGER | nullable, min: 0 | Width in pixels | | `height_px` | INTEGER | nullable, min: 0 | Height in pixels | | `size_mb` | DECIMAL | nullable, min: 0 | File size in MB | | `assetId` | UUID | FK → assets.id | Parent asset | | `importHash` | STRING(255) | UNIQUE, nullable | Import deduplication | **Variant Types:** - `thumbnail` - Small preview - `preview` - Medium preview - `webp` - WebP format - `mp4_low` - Low-quality MP4 - `mp4_high` - High-quality MP4 - `original` - Original file - `reversed` - Reversed video for back navigation transitions (generated server-side with FFmpeg) **Cascade Behavior:** Deleted when parent asset is deleted. **Indexes:** - `assetId, variant_type` partial active-row index (`asset_variants_asset_id_variant_type_active`, where `deletedAt IS NULL`) for asset variant joins and reversed-variant lookup --- ### 9. presigned_url_requests Tracks presigned URL requests for secure uploads/downloads. | Field | Type | Constraints | Description | |-------|------|-------------|-------------| | `id` | UUID | PK, default: UUIDv4 | Primary identifier | | `purpose` | ENUM | nullable | Purpose: `upload`, `download` | | `asset_type` | ENUM | nullable | Asset type: `image`, `video`, `audio`, `file` | | `requested_key` | TEXT | nullable, len: 0-1024 | Requested storage key | | `mime_type` | TEXT | nullable, len: 0-255, validated | Expected MIME type | | `requested_size_mb` | DECIMAL | nullable, min: 0 | Expected file size | | `expires_at` | DATE | nullable | URL expiration time | | `status` | TEXT | nullable | Request status | | `projectId` | UUID | FK → projects.id | Associated project | | `userId` | UUID | FK → users.id | Requesting user | | `importHash` | STRING(255) | UNIQUE, nullable | Import deduplication | --- ## Audio & Media Models ### 10. project_audio_tracks Background audio tracks for projects. | Field | Type | Constraints | Description | |-------|------|-------------|-------------| | `id` | UUID | PK, default: UUIDv4 | Primary identifier | | `environment` | ENUM | NOT NULL, default: 'dev' | Environment: `dev`, `stage`, `production` | | `source_key` | TEXT | nullable | Reference to source version when published | | `name` | TEXT | nullable, len: 0-255 | Track name | | `slug` | TEXT | nullable | URL-safe identifier | | `url` | TEXT | nullable | Audio file URL | | `loop` | BOOLEAN | NOT NULL, default: false | Loop playback | | `volume` | DECIMAL | nullable, min: 0, max: 1 | Volume level (0.0-1.0) | | `sort_order` | INTEGER | nullable | Playback order | | `is_enabled` | BOOLEAN | NOT NULL, default: false | Track enabled | | `projectId` | UUID | FK → projects.id | Parent project | | `importHash` | STRING(255) | UNIQUE, nullable | Import deduplication | **Note:** The `environment` field is NOT NULL with default 'dev' for consistency with `tour_pages`. --- ### 11. project_transition_settings Environment-aware project-level transition settings for CSS-based page transitions. Settings cascade: Element → Project → Global → Hardcoded defaults. | Field | Type | Constraints | Description | |-------|------|-------------|-------------| | `id` | UUID | PK, default: UUIDv4 | Primary identifier | | `environment` | ENUM | NOT NULL | Environment: `dev`, `stage`, `production` | | `source_key` | TEXT | nullable | Reference to source record when published | | `transition_type` | TEXT | NOT NULL, default: 'fade' | CSS transition type (`fade`, `none`) | | `duration_ms` | INTEGER | NOT NULL, default: 700 | Transition duration in milliseconds | | `easing` | TEXT | NOT NULL, default: 'ease-in-out' | CSS easing function | | `overlay_color` | TEXT | NOT NULL, default: '#000000' | Transition overlay color | | `projectId` | UUID | FK → projects.id, NOT NULL | Parent project | | `createdById` | UUID | FK → users.id, nullable | Creator user | | `updatedById` | UUID | FK → users.id, nullable | Last updater | | `importHash` | STRING(255) | UNIQUE, nullable | Import deduplication | **Indexes:** - `project_transition_settings_project_env_unique` - UNIQUE on (projectId, environment) WHERE deletedAt IS NULL **Associations:** - `belongsTo` projects (as `project`) - CASCADE on delete - `belongsTo` users (as `createdBy`, `updatedBy`) **Publishing Integration:** Copied between environments during Save to Stage (dev → stage) and Publish (stage → production). The `source_key` tracks lineage. **Cascade Resolution:** When determining transition settings: 1. Element-level settings (from `ui_schema_json`) 2. Project-level settings (this table, environment-specific) 3. Global defaults (`global_transition_defaults`) 4. Hardcoded fallback (fade, 700ms, ease-in-out, #000000) --- ## Publishing & Caching Models ### 12. publish_events Records of content publishing between environments. | Field | Type | Constraints | Description | |-------|------|-------------|-------------| | `id` | UUID | PK, default: UUIDv4 | Primary identifier | | `title` | STRING | nullable, len: 0-255 | Event title | | `description` | TEXT | nullable, len: 0-5000 | Event description | | `from_environment` | ENUM | NOT NULL | Source: `dev`, `stage`, `production` | | `to_environment` | ENUM | NOT NULL | Target: `dev`, `stage`, `production` | | `started_at` | DATE | nullable | Start timestamp | | `finished_at` | DATE | nullable | Completion timestamp | | `status` | ENUM | NOT NULL, default: 'queued' | Status: `queued`, `running`, `success`, `failed` | | `error_message` | TEXT | nullable | Error details | | `pages_copied` | INTEGER | nullable, min: 0 | Pages copied count | | `audios_copied` | INTEGER | nullable, min: 0 | Audio tracks copied count | | `projectId` | UUID | FK → projects.id | Published project | | `userId` | UUID | FK → users.id | Publishing user | | `importHash` | STRING(255) | UNIQUE, nullable | Import deduplication | **Indexes:** - `projectId` - `userId` - `status` - `started_at` **Cascade Behavior:** `userId` set to NULL when user is deleted (preserves audit trail). --- ### 13. pwa_caches PWA cache configurations for offline support. | Field | Type | Constraints | Description | |-------|------|-------------|-------------| | `id` | UUID | PK, default: UUIDv4 | Primary identifier | | `environment` | ENUM | nullable | Environment: `dev`, `stage`, `production` | | `cache_version` | TEXT | nullable, len: 0-255 | Cache version string | | `manifest_json` | JSON | nullable | PWA manifest configuration | | `asset_list_json` | JSON | nullable | List of cached assets | | `generated_at` | DATE | nullable | Generation timestamp | | `is_active` | BOOLEAN | NOT NULL, default: false | Cache active status | | `projectId` | UUID | FK → projects.id | Parent project | | `importHash` | STRING(255) | UNIQUE, nullable | Import deduplication | --- ## Audit & Logging Models ### 14. access_logs Audit trail for tour access. | Field | Type | Constraints | Description | |-------|------|-------------|-------------| | `id` | UUID | PK, default: UUIDv4 | Primary identifier | | `environment` | ENUM | NOT NULL | Access context: `admin`, `stage`, `production` | | `path` | TEXT | nullable, len: 0-2048 | Accessed path | | `ip_address` | TEXT | nullable, len: 0-45 | Client IP (IPv4/IPv6) | | `user_agent` | TEXT | nullable, len: 0-1024 | Browser user agent | | `accessed_at` | DATE | NOT NULL, default: NOW | Access timestamp | | `projectId` | UUID | FK → projects.id | Accessed project | | `userId` | UUID | FK → users.id | Accessing user (if authenticated) | | `importHash` | STRING(255) | UNIQUE, nullable | Import deduplication | **Indexes:** - `projectId` - `environment` - `userId` - `accessed_at` **Cascade Behavior:** `userId` set to NULL when user is deleted (preserves audit trail). --- ## Element Default Settings Models These models implement a two-tier settings hierarchy for UI elements: 1. **Global defaults** (`element_type_defaults`) - Platform-wide default settings per element type 2. **Project defaults** (`project_element_defaults`) - Project-specific overrides, snapshotted from global on project creation Instance element settings are stored directly in `tour_pages.ui_schema_json` as part of each element's configuration. ### 15. element_type_defaults Global platform-wide default settings for each element type. These serve as templates that are snapshotted to new projects. **Note:** This table was renamed from `ui_elements` to `element_type_defaults` for clarity. | Field | Type | Constraints | Description | |-------|------|-------------|-------------| | `id` | UUID | PK, default: UUIDv4 | Primary identifier | | `element_type` | TEXT | NOT NULL, UNIQUE, len: 1-100 | Element type identifier | | `name` | TEXT | NOT NULL, len: 1-255 | Display name | | `sort_order` | INTEGER | NOT NULL, default: 0 | Display order in UI | | `is_active` | VIRTUAL | getter: true | Virtual active field | | `settings_json` | TEXT | nullable | Default settings JSON | | `importHash` | STRING(255) | UNIQUE, nullable | Import deduplication | **Field Aliasing:** The model exposes `default_settings_json` as a property name, but it maps to the `settings_json` column in the database via `field: 'settings_json'`. This provides a clearer API name while maintaining backward compatibility with the database schema. **Virtual Field:** `is_active` is a VIRTUAL field that always returns `true` (computed, not stored in database). **Indexes:** - `element_type` (unique) - `sort_order` - `deletedAt` **Associations:** - `hasMany` project_element_defaults (as `project_defaults`) **Auto-Initialization:** The API includes an `ensureInitialized()` method that automatically seeds default records if the table is empty. This runs before any CRUD operation. **Seeded Element Types (11 types auto-seeded):** - `navigation_next` - Forward navigation button (sort_order: 1) - `navigation_prev` - Back navigation button (sort_order: 2) - `tooltip` - Hover tooltip (sort_order: 3) - `description` - Text description (sort_order: 4) - `gallery` - Image gallery (sort_order: 5) - `carousel` - Image carousel (sort_order: 6) - `video_player` - Video player (sort_order: 7) - `audio_player` - Audio player (sort_order: 8) - `spot` - Hotspot/clickable area (sort_order: 9) - `logo` - Logo element (sort_order: 10) - `popup` - Popup/modal (sort_order: 11) --- ### 16. project_element_defaults Project-specific element default settings. Created automatically when a project is created by snapshotting all global `element_type_defaults`. Can be customized per-project without affecting global defaults or other projects. | Field | Type | Constraints | Description | |-------|------|-------------|-------------| | `id` | UUID | PK, default: UUIDv4 | Primary identifier | | `element_type` | TEXT | NOT NULL, len: 1-100 | Element type identifier | | `name` | TEXT | nullable, len: 0-255 | Custom display name | | `sort_order` | INTEGER | NOT NULL, default: 0 | Display order in UI | | `settings_json` | TEXT | nullable | Project-specific settings JSON | | `source_element_id` | UUID | FK → element_type_defaults.id, nullable | Reference to global default (for reset/diff) | | `snapshot_version` | INTEGER | NOT NULL, default: 1 | Version counter (incremented on reset) | | `projectId` | UUID | FK → projects.id, NOT NULL | Parent project | | `importHash` | STRING(255) | UNIQUE, nullable | Import deduplication | **Indexes:** - `projectId` - `projectId, element_type` (unique composite) - `element_type` - `source_element_id` - `deletedAt` **Associations:** - `belongsTo` projects (as `project`) - CASCADE on delete - `belongsTo` element_type_defaults (as `source_element`) - SET NULL on delete **Custom findAll with Project Filtering:** The API implements a custom `findAll()` method that supports filtering by project: - Query params: `?projectId=` or `?project=` - Supports multiple values: `?projectId=uuid1|uuid2` - Can filter by project UUID or project name (case-insensitive) - Includes related `project` and `source_element` associations - Default ordering: `sort_order ASC` **Auto-Snapshot Behavior:** When a new project is created, all records from `element_type_defaults` are automatically copied to `project_element_defaults` for that project. This ensures each project starts with consistent defaults while allowing customization. **Reset Functionality:** Projects can reset their element defaults back to the current global defaults via the `/api/project-element-defaults/:id/reset` endpoint. This increments `snapshot_version` and copies current global settings. **Diff Functionality:** The `/api/project-element-defaults/:id/diff` endpoint compares project defaults with global defaults to show customizations. --- ### 16. file (table: `files`) Polymorphic file attachments (e.g., user avatars). **Note:** This model does NOT have `freezeTableName: true`, so the table name is pluralized to `files`. | Field | Type | Constraints | Description | |-------|------|-------------|-------------| | `id` | UUID | PK, default: UUIDv4 | Primary identifier | | `belongsTo` | STRING(255) | nullable | Parent table name | | `belongsToId` | UUID | nullable | Parent record ID | | `belongsToColumn` | STRING(255) | nullable | Column name | | `name` | STRING(2083) | NOT NULL | File name | | `sizeInBytes` | INTEGER | nullable | File size | | `privateUrl` | STRING(2083) | nullable | Private storage URL | | `publicUrl` | STRING(2083) | NOT NULL | Public access URL | **Polymorphic Usage:** Files are attached to records via `belongsTo`/`belongsToId`/`belongsToColumn` columns. Example: User avatar has `belongsTo: 'users'`, `belongsToId: `, `belongsToColumn: 'avatar'` --- ## Junction Tables ### rolesPermissionsPermissions Many-to-many relationship between roles and permissions. | Field | Type | Constraints | |-------|------|-------------| | `roles_permissionsId` | UUID | PK, FK → roles.id | | `permissionId` | UUID | PK, FK → permissions.id | | `createdAt` | TIMESTAMP WITH TIME ZONE | NOT NULL | | `updatedAt` | TIMESTAMP WITH TIME ZONE | NOT NULL | **Indexes:** - `permissionId` ### usersCustom_permissionsPermissions Many-to-many relationship for user custom permissions. | Field | Type | Constraints | |-------|------|-------------| | `users_custom_permissionsId` | UUID | FK → users.id | | `permissionId` | UUID | FK → permissions.id | --- ## Database Indexes Summary | Table | Index | Fields | Type | |-------|-------|--------|------| | users | email | email | unique | | users | app_roleId | app_roleId | - | | users | deletedAt | deletedAt | - | | projects | slug | slug | unique | | projects | deletedAt | deletedAt | - | | production_presentation_access | projectId | projectId | - | | production_presentation_access | userId | userId | - | | production_presentation_access | composite | projectId, userId | unique active rows | | project_memberships | composite | projectId, userId | unique | | tour_pages | composite | projectId, environment, slug | unique | | tour_pages | sort | projectId, environment, sort_order | - | | assets | projectId | projectId | - | | assets | asset_type | asset_type | - | | assets | type | type | - | | assets | is_public | is_public | - | | element_type_defaults | element_type | element_type | unique | | element_type_defaults | sort_order | sort_order | - | | element_type_defaults | deletedAt | deletedAt | - | | project_element_defaults | projectId | projectId | - | | project_element_defaults | composite | projectId, element_type | unique | | project_element_defaults | element_type | element_type | - | | project_element_defaults | source_element_id | source_element_id | - | | project_element_defaults | deletedAt | deletedAt | - | | publish_events | status | status | - | | publish_events | started_at | started_at | - | | access_logs | accessed_at | accessed_at | - | --- ## Foreign Key Constraints All foreign key constraints are enforced at the database level via migration `20260319000001-add-foreign-key-constraints.js`. | Child Table | Column | Parent Table | On Delete | On Update | |-------------|--------|--------------|-----------|-----------| | asset_variants | assetId | assets | CASCADE | CASCADE | | assets | projectId | projects | CASCADE | CASCADE | | tour_pages | projectId | projects | CASCADE | CASCADE | | project_memberships | projectId | projects | CASCADE | CASCADE | | project_memberships | userId | users | CASCADE | CASCADE | | production_presentation_access | projectId | projects | CASCADE | CASCADE | | production_presentation_access | userId | users | CASCADE | CASCADE | | production_presentation_access | createdById | users | SET NULL | CASCADE | | production_presentation_access | updatedById | users | SET NULL | CASCADE | | presigned_url_requests | projectId | projects | CASCADE | CASCADE | | presigned_url_requests | userId | users | CASCADE | CASCADE | | project_audio_tracks | projectId | projects | CASCADE | CASCADE | | project_element_defaults | projectId | projects | CASCADE | CASCADE | | project_element_defaults | source_element_id | element_type_defaults | SET NULL | CASCADE | | publish_events | projectId | projects | CASCADE | CASCADE | | publish_events | userId | users | SET NULL | CASCADE | | pwa_caches | projectId | projects | CASCADE | CASCADE | | access_logs | projectId | projects | CASCADE | CASCADE | | access_logs | userId | users | SET NULL | CASCADE | | users | app_roleId | roles | SET NULL | CASCADE | --- ## Migration History | Migration | Description | |-----------|-------------| | `20260319000001-add-foreign-key-constraints.js` | Adds all FK constraints to enforce referential integrity | | `20260319000002-remove-redundant-deletion-columns.js` | Removes deprecated `is_deleted` and `deleted_at_time` columns from assets and projects | | `20260326000001-rename-ui-elements-to-element-type-defaults.js` | Renames `ui_elements` table to `element_type_defaults` for clarity | | `20260326000002-convert-element-type-enum-to-text.js` | Converts element type from ENUM to TEXT for flexibility | | `20260326000003-create-project-element-defaults.js` | Creates `project_element_defaults` table for project-specific element settings | | `20260326000004-backfill-project-element-defaults.js` | Backfills `project_element_defaults` for existing projects by snapshotting global defaults | | `20260326000005-fix-project-audio-tracks-environment.js` | Fixes `project_audio_tracks.environment` to NOT NULL with default 'dev' | | `20260326000006-copy-dev-to-stage.js` | Copies existing dev content to stage environment for all projects (initializes dev→stage workflow) | | `20260326043002-enforce-environment-not-null.js` | Enforces NOT NULL constraint on environment columns in tour_pages and transitions | | `20260326050442-remove-project-phase-column.js` | Removes redundant `phase` column from projects table (environment is on tour_pages) | | `20260326054410-remove-entry-page-slug-column.js` | Removes `entry_page_slug` from projects (entry page is first by sort_order) | | `20260326060000-convert-targetpageid-to-slug.js` | Converts `targetPageId` to `targetPageSlug` in ui_schema_json for environment-safe navigation | | `20260326060001-drop-page-elements-table.js` | Drops unused `page_elements` table (data stored in ui_schema_json) | | `20260326060002-drop-page-links-table.js` | Drops unused `page_links` table (navigation stored in ui_schema_json) | | `20260326060003-drop-transitions-table.js` | Drops unused `transitions` table (transitionVideoUrl stored in ui_schema_json) | | `20260326171017-add-missing-element-type-defaults.js` | Adds missing element types (spot, logo, popup) to element_type_defaults and backfills project_element_defaults | | `20260327000001-sync-all-element-type-defaults.js` | Syncs all 11 element types with correct sort_order and backfills missing project_element_defaults for all projects | | `20260331024423-remove-unused-theme-columns-from-projects.js` | Removes unused `theme_config_json`, `custom_css_json`, `cdn_base_url` columns from projects table | | `20260331054340-remove-duplicate-element-type-defaults.js` | Removes duplicate element_type_defaults records created during earlier migrations | | `20260331063424-cleanup-invalid-element-type-defaults.js` | Cleans up invalid element_type_defaults entries and ensures data integrity | | `20260403000001-add-background-video-settings.js` | Adds background video playback settings to tour_pages (autoplay, loop, muted, start_time, end_time) | | `20260409000001-add-design-dimensions-to-projects.js` | Adds design_width and design_height columns to projects table for canvas scaling | | `20260409111309-add-design-dimensions-to-tour-pages.js` | Adds design_width and design_height columns to tour_pages table for presentation isolation | | `20260422000001-add-background-video-play-once.js` | Adds background_video_play_once column to tour_pages for session-scoped single playback | | `20260605000001-add-background-audio-settings.js` | Adds background audio playback settings to tour_pages (autoplay, loop, start_time, end_time) | | `20260613000001-add-background-embed-url-to-tour-pages.js` | Adds background_embed_url to tour_pages for 360/embed page backgrounds | | `20260626000001-add-private-production-presentation-access.js` | Adds project production visibility and customer access grants for private production presentations | | `20260626000002-grant-account-manager-create-users.js` | Grants `CREATE_USERS` to Account Manager for customer viewer creation | --- ## Seeders | Seeder | Description | |--------|-------------| | `20200430130759-admin-user.js` | Creates initial admin and test users | | `20200430130760-user-roles.js` | Creates roles, permissions, and role-permission assignments | | `20231127130745-sample-data.js` | Creates sample projects, pages, assets, and other demo data | **Initial Users:** - Admin: `admin@flatlogic.com` (admin password from config) - John Doe: `john@doe.com` (user password from config) - Client: `client@hello.com` (user password from config) --- ## Base DB API Patterns All entity DB APIs extend `GenericDBApi` which provides: ### Configurable Properties - `MODEL` - Sequelize model reference (required, must be defined in subclass) - `TABLE_NAME` - Derived from MODEL.getTableName() - `SEARCHABLE_FIELDS` - Text search fields (ILIKE) - `RANGE_FIELDS` - Date/number range filters - `ENUM_FIELDS` - Exact match filters - `RELATION_FILTERS` - Related entity filters - `CSV_FIELDS` - Export field list (default: `['id', 'createdAt']`) - `AUTOCOMPLETE_FIELD` - Field for autocomplete (default: 'name') - `ASSOCIATIONS` - Many-to-many relationships - `FIND_BY_INCLUDES` - Eager loading for findBy - `FIND_ALL_INCLUDES` - Eager loading for findAll - `getFieldMapping(data)` - Transform input data before save (default: returns data unchanged) ### Standard Methods - `create(data, options)` - Create record with associations - `bulkImport(data, options)` - Bulk create records - `update({ id, data, currentUser, transaction, runtimeContext })` - Update record - `deleteByIds({ ids, currentUser, transaction, runtimeContext })` - Soft delete multiple records - `remove({ id, currentUser, transaction, runtimeContext })` - Soft delete single record - `findBy(where, options)` - Find single record - `findAll(filter, options)` - Paginated list with filters - `findAllAutocomplete({ query, limit, offset }, options)` - Autocomplete search - `toCSV(rows)` - Export to CSV --- ## Environment-Based Content The platform uses an environment-based content model for publishing workflow: 1. **dev** - Development environment (editable in constructor) 2. **stage** - Staging/preview environment (for review before production) 3. **production** - Live production environment (public-facing) ### Content Flow ``` ┌─────────┐ Save to Stage ┌─────────┐ Publish ┌────────────┐ │ dev │ ─────────────────── │ stage │ ───────────── │ production │ └─────────┘ └─────────┘ └────────────┘ ▲ │ Constructor edits ``` ### Tables with Environment Column - `tour_pages` - Pages with `environment` column - `project_audio_tracks` - Audio tracks with `environment` column - `project_transition_settings` - CSS transition settings with `environment` column ### Source Key Tracking When content is copied between environments, the `source_key` field stores the ID of the source record. This enables: - Tracking content lineage - Identifying which stage records came from dev - Rolling back changes if needed ### Environment Access Control Access to content is controlled by environment with strict isolation: | Environment | Authentication | Access | Use Case | |-------------|----------------|--------|----------| | **dev** | Required (JWT) | Admin/Constructor only | Editing in constructor | | **stage** | Required (JWT) | Authenticated users | Review workspace before publish | | **production** | Public (no auth) | Anyone | Published public tours | **Security Layers:** 1. **Backend Middleware** (`requireRuntimeReadOrAuth`): Only `production` environment allows unauthenticated GET requests. Stage and dev require JWT authentication. 2. **Runtime Context API** (`getRuntimeEnvironment`): Blocks `dev` environment from being accessed via `X-Runtime-Environment` header. Only `production` and `stage` are allowed. 3. **Database Filtering** (`applyRuntimeEnvironment`): Applies environment filter to all queries when runtime context is present. 4. **Frontend Routes**: - `/p/[slug]` → production (LayoutGuest) - `/p/[slug]/stage` → stage (LayoutAuthenticated) - `/constructor` → dev (LayoutAuthenticated) **Navigation uses `targetPageSlug`** (not `targetPageId`) in `ui_schema_json` to ensure navigation works correctly across environments since page IDs differ between dev, stage, and production. --- ## Data Types Reference | Sequelize Type | PostgreSQL Type | Usage | |----------------|-----------------|-------| | UUID | uuid | Primary keys, foreign keys | | TEXT | text | Long strings (unlimited) | | STRING(n) | varchar(n) | Limited strings | | INTEGER | integer | Whole numbers | | DECIMAL | numeric | Precise decimals | | BOOLEAN | boolean | True/false | | DATE | timestamp with time zone | Dates and times | | JSON | jsonb | Structured data | | ENUM | enum type | Fixed value sets | | VIRTUAL | (none) | Computed fields | --- ## Common Model Options Most models share these Sequelize options: ```javascript { timestamps: true, // Adds createdAt, updatedAt paranoid: true, // Soft delete via deletedAt freezeTableName: true, // Table name = model name } ``` **Exception:** The `file` model does not set `freezeTableName: true`, so its table name is `files` (pluralized by Sequelize default). All records include audit fields: - `createdAt` - Creation timestamp - `updatedAt` - Last modification timestamp - `deletedAt` - Soft deletion timestamp (null if not deleted) - `createdById` - Creating user's ID - `updatedById` - Last modifying user's ID