399 lines
17 KiB
Markdown
399 lines
17 KiB
Markdown
# 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 profiles and app-specific account controls.
|
|
- `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` - Better Auth session tokens with application View-as metadata.
|
|
- `a_accounts` - Better Auth credentials and linked identity-provider accounts.
|
|
- `a_verifications` - Better Auth email OTP verification and reset challenges.
|
|
- `a_user_two_factors` - Better Auth authenticator secrets, backup codes, and per-user verification lock state.
|
|
- `a_user_passkeys` - Better Auth passkey credentials and authenticator metadata.
|
|
- `a_login_attempts` - Better Auth database-backed rate-limit counters.
|
|
|
|
### `a_users`
|
|
|
|
- `id`, `name`, `username`, `email`, `email_verified`, `image`, `pending_email`, `must_change_password`, `account_locked`, `created_at`, `created_by`, `modified_at`, `modified_by`
|
|
- `username` is unique.
|
|
- `email` is Better Auth's unique, non-null email identity; synthetic addresses are used when the app email is absent.
|
|
|
|
### `a_accounts`
|
|
|
|
- `id`, `account_id`, `provider_id`, `user_id`, `access_token`, `refresh_token`, `id_token`, `access_token_expires_at`, `refresh_token_expires_at`, `scope`, `password`, `created_at`, `updated_at`
|
|
- `(provider_id, account_id)` is unique.
|
|
- Credential passwords are stored in `password` on the `credential` provider account using Better Auth's native scrypt format; legacy PBKDF2 hashes upgrade after successful sign-in. Password hashes are not stored on `a_users`.
|
|
- Foreign key:
|
|
- `user_id` -> `a_users.id` with `ON DELETE CASCADE`
|
|
|
|
### `a_verifications`
|
|
|
|
- `id`, `identifier`, `value`, `expires_at`, `created_at`, `updated_at`
|
|
- Indexed by `identifier`; Better Auth stores one-time email codes and their expiry here.
|
|
|
|
### `a_user_two_factors`
|
|
|
|
- `id`, `user_id`, `secret`, `backup_codes`, `verified`, `failed_verification_count`, `locked_until`
|
|
- `user_id` is unique; each user has at most one authenticator record.
|
|
- Foreign key: `user_id` -> `a_users.id` with `ON DELETE CASCADE`.
|
|
|
|
### `a_user_passkeys`
|
|
|
|
- `id`, `name`, `public_key`, `user_id`, `credential_id`, `counter`, `device_type`, `backed_up`, `transports`, `created_at`, `aaguid`
|
|
- `credential_id` is unique; `user_id` is indexed.
|
|
- Foreign key: `user_id` -> `a_users.id` with `ON DELETE CASCADE`.
|
|
|
|
### `a_login_attempts`
|
|
|
|
- `id`, `rate_key`, `failed_count`, `last_request`
|
|
- `rate_key` is unique.
|
|
|
|
### `a_roles`
|
|
|
|
- `id`, `role_key`, `name`, `description`, `require_two_factor`, `created_at`, `created_by`, `modified_at`, `modified_by`
|
|
- `role_key` is unique.
|
|
- `name` is unique.
|
|
- `require_two_factor` defaults to false; users must enroll when any assigned role requires two-factor authentication.
|
|
|
|
### `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_ACCOUNTS {
|
|
}
|
|
A_VERIFICATIONS {
|
|
}
|
|
A_USER_TWO_FACTORS {
|
|
}
|
|
A_USER_PASSKEYS {
|
|
}
|
|
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_ACCOUNTS : has
|
|
A_USERS ||--o| A_USER_TWO_FACTORS : secures_with
|
|
A_USERS ||--o{ A_USER_PASSKEYS : registers
|
|
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
|
|
``` |