Update wiki page 'Database-Schema'
@@ -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`.
|
||||
|
||||
Reference in New Issue
Block a user