-
Notifications
You must be signed in to change notification settings - Fork 0
Database Schema
Schema is managed via drizzle-kit push — no SQL migration files are committed to git. It lives in src/db/schema/*.ts and is applied automatically on every container start (the Docker entrypoint runs drizzle-kit push --force).
Postgres runs in a Docker container with no host port mapping (security). To reach it from the VPS host: docker compose exec db psql -U squishybot squishybot (or squishybot db:shell). botpanel reaches it over the shared botpanel-net network at db-squishy:5432.
There are 19 tables. They group into voice, profiles/staff, games, social/roles/archive, reaction roles, reporting, and runtime config.
Every active auto voice channel and its full state.
| Column | Type | Description |
|---|---|---|
id |
uuid | Primary key |
guild_id |
text | Discord guild ID |
voice_channel_id |
text | Discord voice channel ID (unique) |
text_channel_id |
text | Attached text channel ID (unique) |
owner_user_id |
text | Channel owner |
acting_owner_user_id |
text | Temporary driver during an owner-grace window |
owner_grace_expires_at |
timestamp | When the owner-grace window ends |
host_user_ids |
text[] | Host user IDs |
allowed_user_ids / allowed_role_ids
|
text[] | Explicit allow lists |
source_hub_id |
text | Hub that spawned this channel |
is_locked / is_hidden
|
boolean |
@everyone Connect / View state |
user_limit |
integer | Max members (0 = unlimited) |
auto_name_enabled |
boolean | Whether the name tracks rich presence |
name_template |
text | Active template key (auto / counter / squad / …) |
manual_name / fallback_name
|
text | Custom name / fallback when nobody is playing |
control_panel_msg_id |
text | Control-panel message ID |
scheduled_cleanup_at |
timestamp | When the cleanup timer fires |
created_at / last_active_at
|
timestamp |
Backs the control panel's "In channel" list. Written on join (upsert) / leave (delete); the reconciler backfills current members at now() on boot.
| Column | Type | Description |
|---|---|---|
voice_channel_id + user_id
|
text | Composite primary key |
joined_at |
timestamp | Join time (relative <t:N:R> in the panel) |
Registry of managed hub voice channels — DB-authoritative; HUB_CHANNEL_IDS env is a one-time seed. Loaded into a hot-path cache so isHubChannel() is sync.
| Column | Type | Description |
|---|---|---|
id |
uuid | Primary key |
guild_id |
text | |
channel_id |
text | Discord channel ID (unique). Updated in place when the reconciler creates a replacement hub. |
category_id |
text | Parent category |
position |
integer | Position in category |
label |
text | Display name |
default_template_key / default_manual_name / default_user_limit
|
text/text/int | Per-hub defaults for spawned channels |
lockdown_until |
timestamp | Per-hub lockdown expiry |
created_at |
timestamp |
Bot-side member profile. Members edit a subset via /settings → Profile & Birthday; sudo edits all fields via /sudo → Settings → User Profiles or right-click → Manage → Edit Profile. Upserted lazily on first edit.
| Column | Type | Description |
|---|---|---|
id |
uuid | Primary key |
guild_id + user_id
|
text | Unique per user per guild |
display_name |
text | How the bot refers to the user (self-editable) |
real_name |
text | Sudo only |
birthday_month / birthday_day
|
integer | Month/day only |
birthday_pings_enabled |
boolean | Default true; the scheduler skips opted-out members |
birthday_year_visible |
boolean | Default false; reserved for future age display |
staff_category / department / tier / leadership_title
|
text | Sudo only |
created_at / updated_at
|
timestamp |
Staff-role request queue.
| Column | Type | Description |
|---|---|---|
id |
uuid | |
guild_id / user_id
|
text | Who submitted |
requested_data |
jsonb | New flow: { role_key, role_label, real_name, reason }; legacy rows: { category, department, tier, … }
|
approval_msg_id |
text | Card message ID in the approvals thread |
status |
text |
pending / approved / denied
|
reviewed_by |
text | Reviewer |
review_note |
text | Optional |
created_at / reviewed_at
|
timestamp |
Catalog backing /games and /play. Managed via /sudo → Settings → Games; loaded into an in-memory cache at boot.
| Column | Type | Description |
|---|---|---|
id |
uuid | Primary key |
guild_id |
text | |
name |
text | Display name |
role_id |
text | View role (toggled by /games View) |
ping_role_id |
text | LFG ping role (toggled by /games Pings; pinged by /play) |
channel_id |
text | Where /play posts |
category_id |
text | Optional parent category for auto-provisioned channels |
is_archived |
boolean | Hidden from /games, rejected by /play
|
is_visible |
boolean | When false: hidden from /games but resolvable by /play
|
sort_order |
integer | Lower = first |
aliases |
text[] | Autocomplete + findGameByNameOrAlias()
|
created_at |
timestamp |
Per-user opt-in state. Toggling writes here AND syncs the Discord role on the target. Cascade-deletes with the parent game.
| Column | Type | Description |
|---|---|---|
id |
uuid | Primary key |
guild_id + user_id
|
text | Owner of the prefs |
game_id |
uuid | FK → games.id (onDelete: cascade) |
wants_view / wants_ping
|
boolean | Desired role state |
Roles applied to every new member on guildMemberAdd. Gated by feature.auto_role_on_join (default off). Managed via /sudo → Settings → Auto Roles.
| Column | Type | Description |
|---|---|---|
role_id |
text | Primary key |
guild_id |
text | |
added_at / added_by_user_id
|
timestamp/text |
Curated color-only roles for /color. Gated by feature.color_roles (default off).
| Column | Type | Description |
|---|---|---|
role_id |
text | Primary key |
guild_id |
text | |
label |
text | Shown in the /color picker |
sort_order |
integer | Default 0 |
added_at |
timestamp |
RSS-backed feeds the poller fetches and reposts. Loaded into a cache at boot; deduped by last_seen_id.
| Column | Type | Description |
|---|---|---|
id |
uuid | Primary key |
guild_id |
text | |
label |
text | e.g. "ITSupportRI Instagram" |
source_url |
text | rss.app (or any RSS) feed URL |
channel_id |
text | Where new items post |
enabled |
boolean | Default true |
last_seen_id |
text | Dedupe cursor (seeded on add so the backlog isn't replayed) |
last_polled_at / last_error
|
timestamp/text | Diagnostics |
max_items_per_poll |
integer | 0 = latest only; sudo cap 3; bot owner unlimited |
created_by_discord_id / created_at
|
text/timestamp |
Categories sudo has opted into the channel-archive workflow.
| Column | Type | Description |
|---|---|---|
id |
uuid | Primary key |
guild_id |
text | |
category_id |
text | Unique |
added_by_user_id / added_at
|
text/timestamp |
Channels currently archived; holds the data needed to restore them.
| Column | Type | Description |
|---|---|---|
channel_id |
text | Primary key |
guild_id |
text | |
original_category_id / original_name
|
text | Restore target |
archived_at / archived_by_user_id
|
timestamp/text |
One row per watched message. expires_at ⇒ temporary (game-night) mode; a daily check deletes the message + rows at expiry.
| Column | Type | Description |
|---|---|---|
id |
uuid | Primary key |
guild_id / channel_id
|
text | |
message_id |
text | Unique |
anchor_role_id |
text | Optional anchor; temp roles get bumped one position above it |
expires_at |
timestamp | Set ⇒ temporary |
created_by_user_id / created_at
|
text/timestamp |
(emoji, role) pairs per message.
| Column | Type | Description |
|---|---|---|
id |
uuid | Primary key |
message_pk |
uuid | FK → reaction_role_messages.id
|
emoji |
text | Unicode emoji or custom-emoji ID |
role_id |
text | Role to toggle |
Append-only log of every /report submission. The bot-owner triage view reads recent rows; Approve/Reject updates status and github_issue_url.
| Column | Type | Description |
|---|---|---|
id |
uuid | Primary key |
guild_id / user_id
|
text | |
title / report_type / description / steps
|
text | Modal fields |
status |
text | Default pending
|
github_issue_url |
text | Set when filed |
decided_by_user_id / decided_at
|
text/timestamp | |
created_at |
timestamp |
Runtime-overridable key/value config — overrides what would otherwise come from .env. Edited via /sudo → Settings. Reading any key falls back to the env value when no row exists.
| Column | Type | Description |
|---|---|---|
key |
text | Primary key |
value |
text | Channel ID, integer-as-string, or 0/1 for booleans |
updated_by_discord_id |
text | Sudo who set the override |
updated_at |
timestamp |
Representative keys in use today: channel.log, channel.admin, channel.birthday, channel.staff_approval_thread, channel.auto_voice_category, channel.archive_destination, channel.games_category, channel.games_prefix; voice.cleanup_delay_ms, voice.owner_grace_ms, voice.no_voice_chat_messages, voice.guild_lockdown_until; birthday.target_hour, birthday.last_run_date; welcome.enabled / welcome.channel_id / welcome.template, goodbye.enabled / goodbye.channel_id / goodbye.template; archive.stale_days; the feature flags feature.auto_voice, feature.auto_threads, feature.social_poller, feature.presence_renames, feature.birthday_pings, feature.auto_role_on_join, feature.color_roles; and the seven staff.role.* links.
Members granted sudo at runtime, beyond the immutable SUDO_USER_IDS env list. Cached at startup.
| Column | Type | Description |
|---|---|---|
user_id |
text | Primary key |
added_by_discord_id / added_at
|
text/timestamp | |
note |
text | Optional |
Channels where every non-bot, non-system message gets a public thread. Edited via /sudo → Settings → Auto Threads; cached at startup.
| Column | Type | Description |
|---|---|---|
channel_id |
text | Primary key |
guild_id |
text | |
name_template |
text | Optional {author} / {content} template (null → {author} — {content}) |
archive_duration |
integer | Optional auto-archive minutes (60 / 1440 / 4320 / 10080; null → 1440) |
added_by_discord_id / added_at
|
text/timestamp |
Audit trail for bot_settings edits — one row per successful set/clear. Rendered in /sudo → Settings → Debug → Audit log. Manual retention.
| Column | Type | Description |
|---|---|---|
id |
uuid | Primary key |
key |
text | Which setting |
old_value / new_value
|
text | |
changed_by_user_id / changed_at
|
text/timestamp |