diff --git a/Database-Schema.md b/Database-Schema.md index d2725c6..aaeac6c 100644 --- a/Database-Schema.md +++ b/Database-Schema.md @@ -1,123 +1,168 @@ # 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 -``` -users ──< thoughts ──< thought_tags >── tags - │ - ├──< follows - ├──< top_friends - └──< api_keys -``` +| File | Contents | +|---|---| +| `001_initial_schema.sql` | Core tables: users, thoughts, follows, top_friends, tags, thought_tags, api_keys | +| `002_federation_columns.sql` | AP federation columns (ap_id, inbox_url, etc.) | +| `003_new_tables.sql` | likes, boosts, blocks, notifications | +| `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` -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 | -|---|---|---|---| -| `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 | +Federation columns added in migration 002: `ap_id`, `inbox_url`, `is_remote` (boolean). --- ### `thoughts` -Stores the content of each post. - -| Column | Type | Constraints | Description | -|---|---|---|---| -| `id` | UUID | PK, DEFAULT gen_random_uuid() | Unique thought identifier | -| `user_id` | UUID | NOT NULL, FK → users(id) | Author | -| `content` | VARCHAR(128) | NOT NULL | Post text | -| `reply_to` | UUID | NULL, FK → thoughts(id) | Parent thought (if reply) | -| `visibility` | ENUM | NOT NULL, DEFAULT 'public' | `public`, `followers`, `private` | -| `created_at` | TIMESTAMPTZ | NOT NULL, DEFAULT NOW() | Post timestamp | +| Column | Type | Notes | +|---|---|---| +| `id` | UUID PK | | +| `user_id` | UUID | FK → users(id) ON DELETE CASCADE | +| `content` | TEXT | was VARCHAR(128), widened in 007 | +| `reply_to` | UUID | nullable FK → thoughts(id) | +| `visibility` | TEXT | `public` / `followers` / `private` | +| `ap_id` | TEXT | nullable — AP object URL for remote notes | +| `created_at` | TIMESTAMPTZ | | --- ### `follows` -Join table for the follower/following relationship. - -| Column | Type | Constraints | Description | -|---|---|---|---| -| `follower_id` | UUID | NOT NULL, FK → users(id) | User who follows | -| `following_id` | UUID | NOT NULL, FK → users(id) | User being followed | -| | | PK (follower_id, following_id) | Prevents duplicate follows | +| Column | Type | Notes | +|---|---|---| +| `follower_id` | UUID | FK → users(id) ON DELETE CASCADE | +| `following_id` | UUID | FK → users(id) ON DELETE CASCADE | +| `state` | TEXT | `pending` / `accepted` (for AP follow handshake) | +| PK | (follower_id, following_id) | | --- ### `top_friends` -Ordered "Top Friends" list per user (up to 8 entries). - -| Column | Type | Constraints | Description | -|---|---|---|---| -| `user_id` | UUID | NOT NULL, FK → users(id) | Owner of the list | -| `friend_id` | UUID | NOT NULL, FK → users(id) | Friend being displayed | -| `position` | SMALLINT | NOT NULL | Display order (1–8) | -| | | PK (user_id, friend_id) | No duplicates | -| | | UNIQUE (user_id, position) | No duplicate positions | +| Column | Type | Notes | +|---|---|---| +| `user_id` | UUID | FK → users(id) ON DELETE CASCADE | +| `friend_id` | UUID | FK → users(id) ON DELETE CASCADE | +| `position` | SMALLINT | 1–5 | +| PK | (user_id, friend_id) | | +| UNIQUE | (user_id, position) | | --- -### `tags` +### `tags` / `thought_tags` -Unique hashtag names. +**tags**: `id SERIAL PK`, `name VARCHAR(50) UNIQUE NOT NULL` -| Column | Type | Constraints | Description | -|---|---|---|---| -| `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 | +**thought_tags**: join table — `(thought_id UUID, tag_id INTEGER)` both FK with CASCADE. --- ### `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 005–006) + +### `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 -# Run pending migrations -cd thoughts-backend -cargo run -p migration +cargo run -p bootstrap +# or point DATABASE_URL at a fresh DB and run the binary ``` - -Migration files follow the naming pattern `m{YYYYMMDD}_{HHMMSS}_{description}.rs`.