# Database Schema This app creates and maintains its schema at startup through `src/db/index.js`. The sections below summarize the current tables and their purpose. Tables generally use a numeric auto-increment `id` primary key. The relationship table `d_announcement_screens` intentionally uses the composite `(announcement_id, screen_id)` primary key instead. Natural and relationship keys remain as unique constraints where needed. Timestamps are stored as `created_at` and `modified_at` when a table supports auditing. ## Admin - `a_users` - admin user accounts and password hashes. - `a_roles` - named role definitions. - `a_permissions` - permission catalog seeded from the application constants. - `a_role_permissions` - many-to-many mapping between roles and permissions. - `a_user_roles` - many-to-many mapping between users and roles. - `a_sessions` - persisted admin session tokens. - `a_account_tokens` - short-lived account verification and password-reset tokens. - `a_user_invitations` - pending user invitations and assigned role ids. - `a_login_attempts` - login rate-limit and lockout state. ### `a_users` - `id`, `name`, `username`, `email`, `email_verified_at`, `pending_email`, `pending_email_token_hash`, `pending_email_expires_at`, `password_hash`, `password_salt`, `password_iterations`, `must_change_password`, `account_locked`, `last_login_at`, `last_login_ip`, `created_at`, `created_by`, `modified_at`, `modified_by` - `username` is unique. ### `a_account_tokens` - `id`, `user_id`, `token_type`, `token_hash`, `expires_at`, `used_at`, `created_at` - `token_hash` is unique. - Foreign key: - `user_id` -> `a_users.id` with `ON DELETE CASCADE` - Indexed by `(token_type, token_hash, expires_at)` and `(user_id, token_type)`. ### `a_user_invitations` - `id`, `email`, `name`, `role_ids_json`, `token_hash`, `expires_at`, `used_at`, `created_at`, `created_by` - `token_hash` is unique. - Indexed by `(email, used_at, expires_at)` and `(created_by, created_at)`. ### `a_login_attempts` - `id`, `rate_key`, `failed_count`, `last_failed_at`, `locked_until`, `created_at`, `modified_at` - `rate_key` is unique. ### `a_roles` - `id`, `role_key`, `name`, `description`, `created_at`, `created_by`, `modified_at`, `modified_by` - `role_key` is unique. - `name` is unique. ### `a_permissions` - `id`, `permission_key`, `name`, `section_name`, `description`, `created_at`, `modified_at` - `permission_key` is unique. ### `a_role_permissions` - `id`, `role_id`, `permission_id`, `created_at`, `modified_at` - Foreign keys: - `role_id` -> `a_roles.id` - `permission_id` -> `a_permissions.id` - Unique key: `(role_id, permission_id)` ### `a_user_roles` - `id`, `user_id`, `role_id`, `created_at`, `modified_at` - Foreign keys: - `user_id` -> `a_users.id` - `role_id` -> `a_roles.id` - Unique key: `(user_id, role_id)` ### `a_sessions` - `id`, `session_hash`, `user_id`, `ip_address`, `user_agent`, `expires_at`, `created_at`, `created_by`, `last_used_at`, `modified_by` - `session_hash` is unique. - Foreign key: - `user_id` -> `a_users.id` ## Content - `c_canvas_sizes` - reusable canvas presets for templates. - `c_playlists` - playlist definitions, playback options, and canvas assignment. - `c_templates` - slide templates with canvas and background settings. - `c_template_regions` - template region layout and metadata. - `c_slides` - slide records with template binding, JSON content, and thumbnail path. - `c_media_assets` - reusable uploaded media files referenced by slides and templates. - `c_playlist_slides` - ordered playlist items and timing. - `c_playlist_slide_schedule_rules` - rule rows attached to playlist slides. ### `c_canvas_sizes` - `id`, `name`, `width`, `height`, `created_at`, `created_by`, `modified_at`, `modified_by` - `(width, height)` is unique. ### `c_playlists` - `id`, `name`, `fade_between_slides`, `skip_unavailable_rtmp`, `canvas_id`, `created_at`, `created_by`, `modified_at`, `modified_by` - Foreign key: - `canvas_id` -> `c_canvas_sizes.id` with `ON DELETE SET NULL` ### `c_templates` - `id`, `name`, `canvas_size_id`, `background_image_path`, `background_color`, `background_gradient`, `created_at`, `created_by`, `modified_at`, `modified_by` - `background_gradient` stores normalized linear gradient settings as JSON text, including angle and colour stops. - Foreign key: - `canvas_size_id` -> `c_canvas_sizes.id` with `ON DELETE SET NULL` ### `c_template_regions` - `id`, `template_id`, `region_key`, `region_type`, `label`, `font_family`, `lock_ratio`, `animation_json`, `default_content_json`, `read_only`, `x`, `y`, `width`, `height`, `z_index`, `created_at`, `created_by`, `modified_at`, `modified_by` - Foreign key: - `template_id` -> `c_templates.id` with `ON DELETE CASCADE` ### `c_slides` - `id`, `title`, `template_id`, `content_json`, `thumbnail_path`, `created_at`, `created_by`, `modified_at`, `modified_by` - Foreign key: - `template_id` -> `c_templates.id` with `ON DELETE SET NULL` ### `c_media_assets` - `id`, `media_path`, `original_name`, `media_type`, `mime_type`, `file_size`, `is_published`, `created_at`, `created_by`, `modified_at`, `modified_by` - `media_path` is unique. - Indexed by `media_type` and `created_at`. ### `c_playlist_slides` - `id`, `playlist_id`, `slide_id`, `position`, `duration_seconds`, `use_video_duration`, `disable_audio`, `created_at`, `created_by`, `modified_at`, `modified_by` - Foreign keys: - `playlist_id` -> `c_playlists.id` with `ON DELETE CASCADE` - `slide_id` -> `c_slides.id` with `ON DELETE CASCADE` ### `c_playlist_slide_schedule_rules` - `id`, `playlist_slide_id`, `position`, `start_datetime`, `end_datetime`, `start_time`, `end_time`, `schedule_days_json`, `created_at`, `created_by`, `modified_at`, `modified_by` - Foreign key: - `playlist_slide_id` -> `c_playlist_slides.id` with `ON DELETE CASCADE` - Index: - `(playlist_slide_id, position)` - Each playlist slide can have zero or more schedule rules. - Rules are evaluated with OR across rows and with AND across the fields inside a single rule. ## Devices - `d_players` - player registry and connection metadata. - `d_screens` - screen records and playlist assignment. - `d_onboarding_devices` - device-to-screen bindings and onboarded client names. ### `d_players` - `id`, `identifier`, `public_base_url`, `internal_base_url`, `last_seen_at`, `created_at`, `modified_at` - `id` is the primary key. - `identifier` is unique and is the stable player identity used by the app. ### `d_screens` - `id`, `name`, `slug`, `playlist_id`, `created_at`, `created_by`, `modified_at`, `modified_by` - `slug` is unique. - Foreign keys: - `playlist_id` -> `c_playlists.id` with `ON DELETE SET NULL` - Screens are no longer tied to a player foreign key directly; player registration and live connection metadata are tracked separately in `d_players`. ### `d_onboarding_devices` - `id`, `device_id`, `client_name`, `screen_id`, `created_at`, `created_by`, `modified_at`, `modified_by`, `last_seen_at` - `device_id` is unique. - Foreign key: - `screen_id` -> `d_screens.id` with `ON DELETE SET NULL` ## Onboarding - The onboarding flow uses `d_onboarding_devices` to bind a device to a screen and persist the client name. ## Announcements - `d_announcements` - announcement content and display metadata. - `d_announcement_screens` - announcement-to-screen assignments. ### `d_announcements` - `id`, `message`, `short_label`, `announcement_type`, `color_key`, `icon_key`, `duration_seconds`, `expires_at`, `created_at`, `created_by`, `modified_at`, `modified_by` - `announcement_type` defaults to `lower-third`. ### `d_announcement_screens` - `announcement_id`, `screen_id`, `created_at`, `created_by`, `modified_at`, `modified_by` - Foreign keys: - `announcement_id` -> `d_announcements.id` with `ON DELETE CASCADE` - `screen_id` -> `d_screens.id` with `ON DELETE CASCADE` - Unique key: `(announcement_id, screen_id)` ## Integrations - `i_rss_feeds` - RSS feed definitions and refresh cadence. - `i_rss_feed_items` - cached RSS feed items. - `i_api_source_groups` - API source names and shared authentication settings. - `i_api_sources` - named requests, independent refresh settings, and last response snapshots. - `i_weather_locations` - configured weather locations and last response snapshot. - `i_timetable_groups` - grouped timetable definitions used by the timetable region. - `i_timetable_entries` - dated entries that belong to a timetable group. ### `i_rss_feeds` - `id`, `name`, `feed_url`, `update_interval_value`, `update_interval_unit`, `item_limit`, `enabled`, `last_pulled_at`, `created_at`, `created_by`, `modified_at`, `modified_by` ### `i_rss_feed_items` - `id`, `rss_feed_id`, `position`, `item_json`, `created_at`, `created_by`, `modified_at`, `modified_by` - Foreign key: - `rss_feed_id` -> `i_rss_feeds.id` with `ON DELETE CASCADE` - Unique key: - `(rss_feed_id, position)` ### `i_api_source_groups` - `id`, `name`, `enabled`, `auth_method`, `auth_username`, `auth_password`, `auth_bearer_token`, `auth_header_name`, `auth_header_value`, `token_url`, `token_request_body_json`, `token_response_path`, `token_refresh_url`, `token_refresh_request_body_json`, `token_refresh_response_path`, `token_header_name`, `token_header_prefix`, `created_at`, `created_by`, `modified_at`, `modified_by` - Unique group name; authentication applies to every child request. `auth_method = 'none'` supports public endpoints. Token caches are keyed by group and authentication configuration. - `enabled` is the source-wide refresh gate, added by migration 2.16.2 with a default of `1`. Disabling a group leaves child enabled flags unchanged. Refresh lookups combine group/request flags, and queued work checks effective enabled state before making an outbound request. ### `i_api_sources` - `id`, `auth_source_id`, `name`, `api_url`, `request_method`, `request_body_json`, `items_path`, `update_interval_value`, `update_interval_unit`, `enabled`, `last_pulled_at`, `last_pull_error`, `last_response_status`, `last_response_content_type`, `last_response_json`, `created_at`, `created_by`, `modified_at`, `modified_by` - `auth_source_id` references `i_api_source_groups.id` with restricted parent deletion. The web application requires at least one request per group and unique request names within each group. - Request IDs remain the `source_id` stored in slide regions and the keys used by scheduled refreshes. Removing a request used by a slide is blocked. - The 2.16.1 migration copies each existing source's authentication into a same-ID parent group and links the original request without changing its ID or response snapshot. Legacy authentication columns remain on request rows for schema compatibility but runtime authentication is loaded from the parent. - Editor/player request lists exclude authentication credentials. Refreshes resolve current parent authentication at execution time. ### `i_weather_locations` - `id`, `name`, `location_label`, `latitude`, `longitude`, `timezone`, `provider`, `temperature_unit`, `wind_unit`, `precipitation_unit`, `update_interval_value`, `update_interval_unit`, `enabled`, `last_pulled_at`, `last_pull_error`, `last_response_json`, `created_at`, `created_by`, `modified_at`, `modified_by` - Stores configured weather locations and the most recent provider response used for forecast previews and weather regions. ### `i_timetable_groups` - `id`, `name`, `short_description`, `timezone`, `created_at`, `created_by`, `modified_at`, `modified_by` - `timezone` defaults to `Europe/London`. ### `i_timetable_entries` - `id`, `schedule_group_id`, `title`, `short_description`, `start_datetime`, `end_datetime`, `created_at`, `created_by`, `modified_at`, `modified_by` - Foreign key: - `schedule_group_id` -> `i_timetable_groups.id` with `ON DELETE CASCADE` - Index: - `(schedule_group_id, start_datetime)` ## Operations - `o_background_tasks` - queue and history for background jobs. - `o_app_state` - generic app state and version markers stored as key/value pairs. - `o_app_settings` - administrator-configurable application settings stored by key. - `o_audit_events` - retained audit events for administrator activity and system changes. ### `o_background_tasks` - `id`, `task_key`, `task_type`, `title`, `category`, `status`, `payload_json`, `metadata_json`, `attempts`, `created_at`, `created_by`, `started_at`, `finished_at`, `error_message` - Indexed by `status`, `task_key`, and `task_type`. ### `o_app_state` - `id`, `state_key`, `state_value`, `created_at`, `modified_at` - `state_key` is unique. - `schema_version` is stored here so startup can detect the previously recorded schema version before deciding whether migrations need to run. ### `o_app_settings` - `id`, `setting_key`, `setting_value`, `created_at`, `created_by`, `modified_at`, `modified_by` - `setting_key` is unique. - Values are stored as JSON and validated against the application setting definitions in `src/data/app-settings.js`. ### `o_audit_events` - `id`, `occurred_at`, `category`, `event_type`, `actor_user_id`, `target_type`, `target_id`, `target_label`, `ip_address`, `user_agent`, `details_json` - Indexed by occurrence time, category and event type, actor, and target. ## Notes - The schema is initialized with `CREATE TABLE IF NOT EXISTS`, so new installs can start from an empty database. - `src/db/bootstrap.js` seeds the canvas size defaults, default permissions, and the default administrator role. - `src/db/migrations.js` records the current schema version in `o_app_state` during startup so later launches can tell whether an update is happening. - The ER diagram shows declared foreign keys and the logical audit actor association; player registry and JSON-based references are intentionally not shown as foreign-key relationships. ```mermaid erDiagram A_USERS { } A_ROLES { } A_PERMISSIONS { } A_ROLE_PERMISSIONS { } A_USER_ROLES { } A_SESSIONS { } A_ACCOUNT_TOKENS { } A_USER_INVITATIONS { } A_LOGIN_ATTEMPTS { } C_CANVAS_SIZES { } C_PLAYLISTS { } C_TEMPLATES { } C_TEMPLATE_REGIONS { } C_SLIDES { } C_MEDIA_ASSETS { } C_PLAYLIST_SLIDES { } C_PLAYLIST_SLIDE_SCHEDULE_RULES { } D_PLAYERS { } D_SCREENS { } D_ONBOARDING_DEVICES { } D_ANNOUNCEMENTS { } D_ANNOUNCEMENT_SCREENS { } I_RSS_FEEDS { } I_RSS_FEED_ITEMS { } I_API_SOURCES { } I_API_SOURCE_GROUPS { } I_WEATHER_LOCATIONS { } I_TIMETABLE_GROUPS { } I_TIMETABLE_ENTRIES { } O_APP_STATE { } O_APP_SETTINGS { } O_BACKGROUND_TASKS { } O_AUDIT_EVENTS { } A_USERS ||--o{ A_USER_ROLES : has A_ROLES ||--o{ A_USER_ROLES : assigned_to A_ROLES ||--o{ A_ROLE_PERMISSIONS : has A_PERMISSIONS ||--o{ A_ROLE_PERMISSIONS : granted_to A_USERS ||--o{ A_SESSIONS : owns A_USERS ||--o{ A_ACCOUNT_TOKENS : has A_USERS o|--o{ O_AUDIT_EVENTS : acts I_API_SOURCE_GROUPS ||--o{ I_API_SOURCES : contains C_CANVAS_SIZES o|--o{ C_TEMPLATES : used_by C_CANVAS_SIZES o|--o{ C_PLAYLISTS : used_by C_TEMPLATES ||--o{ C_TEMPLATE_REGIONS : contains C_TEMPLATES o|--o{ C_SLIDES : used_by C_MEDIA_ASSETS }o..o{ C_SLIDES : referenced_by C_MEDIA_ASSETS o{..o| C_TEMPLATES : background_for C_PLAYLISTS ||--o{ C_PLAYLIST_SLIDES : contains C_SLIDES ||--o{ C_PLAYLIST_SLIDES : included_in C_PLAYLIST_SLIDES ||--o{ C_PLAYLIST_SLIDE_SCHEDULE_RULES : has_rules C_PLAYLISTS o|--o{ D_SCREENS : uses D_SCREENS o|--o{ D_ONBOARDING_DEVICES : binds D_ANNOUNCEMENTS ||--o{ D_ANNOUNCEMENT_SCREENS : targets D_SCREENS ||--o{ D_ANNOUNCEMENT_SCREENS : receives I_RSS_FEEDS ||--o{ I_RSS_FEED_ITEMS : caches I_TIMETABLE_GROUPS ||--o{ I_TIMETABLE_ENTRIES : contains ```