39948-vm/backend/docs/database-schema.md
2026-07-03 16:11:24 +02:00

1026 lines
46 KiB
Markdown

# 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_<ENTITY>` - Create records
- `READ_<ENTITY>` - Read records
- `UPDATE_<ENTITY>` - Modify records
- `DELETE_<ENTITY>` - 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=<uuid>` or `?project=<uuid|name>`
- 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: <user_id>`, `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