# Database tables

Generated from a current schema. Column counts and the leading columns are shown; the authoritative definition is `database/migrations`.

Every table is InnoDB with `utf8mb4` / `utf8mb4_unicode_ci`.

| Table | Columns | Leading columns |
|---|---|---|
| `ad_creatives` | 19 | id, name, placement, code, image_url, link_url, alt, image_width, … |
| `answers` | 9 | id, question_id, user_id, body, status, votes_count, is_best, created_at, … |
| `api_tokens` | 10 | id, user_id, token_prefix, token_hash, device_label, scopes_json, last_used_at, expires_at, … |
| `audit_log` | 9 | id, actor_user_id, action, resource_type, resource_id, before_json, after_json, ip_hash, … |
| `categories` | 13 | id, name, description, content_locale, slug, parent_id, sort_order, seo_title, … |
| `category_follows` | 3 | user_id, category_id, created_at |
| `comments` | 8 | id, commentable_type, commentable_id, user_id, body, status, created_at, updated_at |
| `contact_messages` | 11 | id, name, email, subject, message, locale, status, admin_notes, … |
| `custom_code_snippets` | 11 | id, name, position, code, pages, locales, audience, is_enabled, … |
| `email_tokens` | 7 | id, user_id, type, token_hash, expires_at, used_at, created_at |
| `languages` | 9 | code, name, native_name, direction, fallback_locale, is_enabled, sort_order, created_at, … |
| `layout_sections` | 7 | id, layout_id, section_key, name, sort_order, created_at, updated_at |
| `layouts` | 7 | id, name, slug, description, status, created_at, updated_at |
| `menu_items` | 12 | id, menu_key, label, translation_key, url, audience, locales, is_enabled, … |
| `moderation_actions` | 8 | id, actor_user_id, action, resource_type, resource_id, reason, metadata_json, created_at |
| `notification_preferences` | 5 | user_id, event_type, in_app_enabled, email_enabled, updated_at |
| `notifications` | 8 | id, user_id, type, title, body, url, read_at, created_at |
| `question_follows` | 3 | user_id, question_id, created_at |
| `question_tags` | 3 | question_id, tag_id, created_at |
| `questions` | 25 | id, user_id, category_id, content_locale, title, title_normalized, body, body_normalized, … |
| `rate_limits` | 6 | id, identifier_hash, action, window_start, count, created_at |
| `reports` | 10 | id, reportable_type, reportable_id, user_id, reason, status, notes, moderator_notes, … |
| `schema_migrations` | 3 | id, filename, applied_at |
| `settings` | 7 | id, key, value, type, is_secret, created_at, updated_at |
| `tag_follows` | 3 | user_id, tag_id, created_at |
| `tag_synonyms` | 4 | id, tag_id, synonym, created_at |
| `tags` | 11 | id, name, description, slug, content_locale, seo_title, seo_description, canonical_url, … |
| `theme_profile_versions` | 7 | id, theme_profile_id, version_number, name, tokens_json, created_by, created_at |
| `theme_profiles` | 9 | id, name, slug, description, tokens_json, is_active, created_by, created_at, … |
| `topic_follows` | 3 | user_id, topic_id, created_at |
| `topics` | 16 | id, user_id, category_id, content_locale, title, slug, body, kind, … |
| `translation_overrides` | 6 | locale, translation_key, value, updated_by, created_at, updated_at |
| `user_preferences` | 3 | user_id, admin_locale, updated_at |
| `users` | 19 | id, name, username, bio, avatar_url, email, password_hash, auth_version, … |
| `votes` | 6 | id, user_id, votable_type, votable_id, value, created_at |
| `widget_translations` | 6 | widget_id, locale, title, content_json, created_at, updated_at |
| `widgets` | 9 | id, layout_section_id, type, title, config_json, sort_order, is_enabled, created_at, … |

## Notes

- `schema_migrations` records which migration files have run. Never edit it by hand to "skip" a migration.
- Follow tables (`question_follows`, `topic_follows`, `category_follows`, `tag_follows`) use composite primary keys, so a duplicate follow is impossible by construction.
- `questions.answers_count`, `questions.comments_count`, `topics.comments_count` and `topics.last_activity_at` are denormalised counters maintained inside the same transaction as the row that changes them.
- `users.account_type`, `users.trust_level`, `users.login_disabled` and `users.created_by_admin_id` classify accounts. `login_disabled` is enforced during authentication.
- `theme_profiles` and `theme_profile_versions` store JSON tokens only — never PHP or templates.

See [Database schema](../07-internals/DATABASE-SCHEMA.md).
