Update wiki page 'Database-Schema'

2026-05-15 15:04:59 +00:00
parent e78c9db6e0
commit 50e7260816

@@ -1,123 +1,168 @@
# Database Schema # Database Schema
PostgreSQL. UUIDs as primary keys to prevent enumeration and ease future federation. All timestamps include timezone (`TIMESTAMPTZ`). PostgreSQL 15+. UUIDs as primary keys. All timestamps are `TIMESTAMPTZ`. Migrations are plain SQL files in `crates/adapters/postgres/migrations/` — run automatically on API server startup.
## ERD (simplified) ## Migration Files
``` | File | Contents |
users ──< thoughts ──< thought_tags >── tags |---|---|
| `001_initial_schema.sql` | Core tables: users, thoughts, follows, top_friends, tags, thought_tags, api_keys |
├──< follows | `002_federation_columns.sql` | AP federation columns (ap_id, inbox_url, etc.) |
├──< top_friends | `003_new_tables.sql` | likes, boosts, blocks, notifications |
└──< api_keys | `004_search_indexes.sql` | PostgreSQL trigram indexes for full-text search |
``` | `005_federation_tables.sql` | remote_actors, federation-specific tables |
| `006_remote_actor_connections.sql` | remote_actor_connections (follower/following cache) |
| `007_content_text.sql` | Widens content column to TEXT |
| `008_rename_notifications_type.sql` | Notification type column rename |
| `009_failed_events.sql` | failed_events table for DLQ |
| `010_fix_reply_fk_on_delete.sql` | Fix FK cascade on reply_to |
## Tables ## Core Tables
### `users` ### `users`
Stores user account and profile data. | Column | Type | Notes |
|---|---|---|
| `id` | UUID PK | gen_random_uuid() |
| `username` | VARCHAR(32) | UNIQUE NOT NULL |
| `email` | VARCHAR(255) | UNIQUE NOT NULL |
| `password_hash` | TEXT | Argon2 |
| `display_name` | VARCHAR(50) | nullable |
| `bio` | VARCHAR(160) | nullable |
| `avatar_url` | TEXT | nullable |
| `header_url` | TEXT | nullable |
| `custom_css` | TEXT | nullable, sanitized before write |
| `created_at` | TIMESTAMPTZ | DEFAULT NOW() |
| `updated_at` | TIMESTAMPTZ | DEFAULT NOW() |
| Column | Type | Constraints | Description | Federation columns added in migration 002: `ap_id`, `inbox_url`, `is_remote` (boolean).
|---|---|---|---|
| `id` | UUID | PK, DEFAULT gen_random_uuid() | Unique user identifier |
| `username` | VARCHAR(32) | NOT NULL, UNIQUE | User handle |
| `email` | VARCHAR(255) | NOT NULL, UNIQUE | Email address |
| `password_hash` | TEXT | NOT NULL | Argon2 or bcrypt hash |
| `display_name` | VARCHAR(50) | NULL | Public display name |
| `bio` | VARCHAR(160) | NULL | Public biography |
| `avatar_url` | TEXT | NULL | URL to avatar image |
| `header_url` | TEXT | NULL | URL to header/banner image |
| `custom_css` | TEXT | NULL | Custom profile CSS (sanitized) |
| `created_at` | TIMESTAMPTZ | NOT NULL, DEFAULT NOW() | Account creation time |
| `updated_at` | TIMESTAMPTZ | NOT NULL, DEFAULT NOW() | Last profile update |
--- ---
### `thoughts` ### `thoughts`
Stores the content of each post. | Column | Type | Notes |
|---|---|---|
| Column | Type | Constraints | Description | | `id` | UUID PK | |
|---|---|---|---| | `user_id` | UUID | FK → users(id) ON DELETE CASCADE |
| `id` | UUID | PK, DEFAULT gen_random_uuid() | Unique thought identifier | | `content` | TEXT | was VARCHAR(128), widened in 007 |
| `user_id` | UUID | NOT NULL, FK → users(id) | Author | | `reply_to` | UUID | nullable FK → thoughts(id) |
| `content` | VARCHAR(128) | NOT NULL | Post text | | `visibility` | TEXT | `public` / `followers` / `private` |
| `reply_to` | UUID | NULL, FK → thoughts(id) | Parent thought (if reply) | | `ap_id` | TEXT | nullable — AP object URL for remote notes |
| `visibility` | ENUM | NOT NULL, DEFAULT 'public' | `public`, `followers`, `private` | | `created_at` | TIMESTAMPTZ | |
| `created_at` | TIMESTAMPTZ | NOT NULL, DEFAULT NOW() | Post timestamp |
--- ---
### `follows` ### `follows`
Join table for the follower/following relationship. | Column | Type | Notes |
|---|---|---|
| Column | Type | Constraints | Description | | `follower_id` | UUID | FK → users(id) ON DELETE CASCADE |
|---|---|---|---| | `following_id` | UUID | FK → users(id) ON DELETE CASCADE |
| `follower_id` | UUID | NOT NULL, FK → users(id) | User who follows | | `state` | TEXT | `pending` / `accepted` (for AP follow handshake) |
| `following_id` | UUID | NOT NULL, FK → users(id) | User being followed | | PK | (follower_id, following_id) | |
| | | PK (follower_id, following_id) | Prevents duplicate follows |
--- ---
### `top_friends` ### `top_friends`
Ordered "Top Friends" list per user (up to 8 entries). | Column | Type | Notes |
|---|---|---|
| Column | Type | Constraints | Description | | `user_id` | UUID | FK → users(id) ON DELETE CASCADE |
|---|---|---|---| | `friend_id` | UUID | FK → users(id) ON DELETE CASCADE |
| `user_id` | UUID | NOT NULL, FK → users(id) | Owner of the list | | `position` | SMALLINT | 15 |
| `friend_id` | UUID | NOT NULL, FK → users(id) | Friend being displayed | | PK | (user_id, friend_id) | |
| `position` | SMALLINT | NOT NULL | Display order (18) | | UNIQUE | (user_id, position) | |
| | | PK (user_id, friend_id) | No duplicates |
| | | UNIQUE (user_id, position) | No duplicate positions |
--- ---
### `tags` ### `tags` / `thought_tags`
Unique hashtag names. **tags**: `id SERIAL PK`, `name VARCHAR(50) UNIQUE NOT NULL`
| Column | Type | Constraints | Description | **thought_tags**: join table — `(thought_id UUID, tag_id INTEGER)` both FK with CASCADE.
|---|---|---|---|
| `id` | SERIAL | PK | Tag identifier |
| `name` | VARCHAR(50) | NOT NULL, UNIQUE | Tag name (e.g. `welcome`) |
---
### `thought_tags`
Many-to-many join between thoughts and tags.
| Column | Type | Constraints | Description |
|---|---|---|---|
| `thought_id` | UUID | NOT NULL, FK → thoughts(id) | The thought |
| `tag_id` | INTEGER | NOT NULL, FK → tags(id) | The tag |
| | | PK (thought_id, tag_id) | No duplicate tags per post |
--- ---
### `api_keys` ### `api_keys`
Hashed API keys for third-party access. | Column | Type | Notes |
|---|---|---|
| `id` | UUID PK | |
| `user_id` | UUID | FK → users(id) ON DELETE CASCADE |
| `key_hash` | TEXT | UNIQUE |
| `name` | VARCHAR(50) | user-provided label |
| `created_at` | TIMESTAMPTZ | |
| Column | Type | Constraints | Description | ---
|---|---|---|---|
| `id` | UUID | PK, DEFAULT gen_random_uuid() | Key identifier |
| `user_id` | UUID | NOT NULL, FK → users(id) | Owning user |
| `key_hash` | TEXT | NOT NULL, UNIQUE | Hashed key value |
| `name` | VARCHAR(50) | NOT NULL | User-provided label |
| `created_at` | TIMESTAMPTZ | NOT NULL, DEFAULT NOW() | Creation timestamp |
## Migrations ## Social Tables (migration 003)
Migrations are managed by **SeaORM** in the `thoughts-backend/migration/` crate. ### `likes`
| Column | Type | Notes |
|---|---|---|
| `user_id` | UUID | FK → users(id) ON DELETE CASCADE |
| `thought_id` | UUID | FK → thoughts(id) ON DELETE CASCADE |
| `created_at` | TIMESTAMPTZ | |
| PK | (user_id, thought_id) | |
### `boosts`
Same structure as `likes`.
### `blocks`
| Column | Type | Notes |
|---|---|---|
| `blocker_id` | UUID | FK → users(id) |
| `blocked_id` | UUID | FK → users(id) |
| `created_at` | TIMESTAMPTZ | |
| PK | (blocker_id, blocked_id) | |
### `notifications`
| Column | Type | Notes |
|---|---|---|
| `id` | UUID PK | |
| `user_id` | UUID | FK → users(id) — recipient |
| `type` | TEXT | `like`, `boost`, `follow`, `reply`, etc. |
| `actor_id` | UUID | nullable — who triggered it |
| `thought_id` | UUID | nullable — related thought |
| `read` | BOOLEAN | DEFAULT false |
| `created_at` | TIMESTAMPTZ | |
---
## Federation Tables (migrations 005006)
### `remote_actors`
Caches resolved remote AP actor profiles.
| Column | Type | Notes |
|---|---|---|
| `id` | UUID PK | |
| `ap_url` | TEXT | UNIQUE — the actor's AP ID |
| `username` | TEXT | |
| `display_name` | TEXT | nullable |
| `avatar_url` | TEXT | nullable |
| `inbox_url` | TEXT | |
| `fetched_at` | TIMESTAMPTZ | |
### `remote_actor_connections`
Caches follower/following pages fetched from remote outboxes.
### `failed_events`
DLQ for events that failed delivery after retries (migration 009).
## Running Migrations
Migrations run automatically when the API server starts. To run manually:
```bash ```bash
# Run pending migrations cargo run -p bootstrap
cd thoughts-backend # or point DATABASE_URL at a fresh DB and run the binary
cargo run -p migration
``` ```
Migration files follow the naming pattern `m{YYYYMMDD}_{HHMMSS}_{description}.rs`.