---
# System prepended metadata

title: Social Media Platform - Database Architecture Documentation

---

# Social Media Platform - Database Architecture Documentation

**Version:** 2.0  
**Database:** PostgreSQL 14+  
**Date:** January 2026  
**Status:** Ready for Review

---

## Table of Contents

1. [Overview](#overview)
2. [Architecture Principles](#architecture-principles)
3. [Naming Conventions](#naming-conventions)
4. [Database Schema](#database-schema)
   - [System & Configuration](#1-system--configuration-sys_)
   - [Users & Authentication](#2-users--authentication-usr_)
   - [Content Creators](#3-content-creators-crt_)
   - [File Storage](#4-file-storage-fs_)
   - [Posts](#5-posts-cnt_)
   - [Reels](#6-reels-cnt_)
   - [Stories](#7-stories-cnt_)
   - [Hashtags](#8-hashtags-cnt_)
   - [Social Features](#9-social-features-usr_)
5. [Entity Relationships](#entity-relationships)
6. [Key Design Decisions](#key-design-decisions)
7. [Security & Access Control](#security--access-control)
8. [Scalability Considerations](#scalability-considerations)
9. [Data Integrity](#data-integrity)
10. [Future Extensions](#future-extensions)

---

## Overview

This database schema supports a comprehensive social media platform with three distinct user roles: **Users**, **Content Creators**, and **Admins**. The platform enables content creation (posts, reels, and stories), user engagement (likes, comments, follows), hashtag discovery, and comprehensive admin moderation capabilities.

### Key Features

- ✅ Role-based access control (RBAC) with granular permissions
- ✅ Individual and company content creator profiles
- ✅ Mixed-content posts (text, images, videos, or combinations)
- ✅ Instagram-style reels with video content
- ✅ 24-hour ephemeral stories
- ✅ Hashtag system for content discovery
- ✅ Follow/following social graph
- ✅ Engagement tracking (likes, comments, views)
- ✅ Storage-agnostic file system (MinIO, S3, local)
- ✅ Admin moderation and audit logging
- ✅ Soft deletes for content recovery

### External Dependencies

- **Analytics**: Firebase/PostHog (view tracking, watch time, engagement metrics)
- **File Storage**: MinIO (primary), S3, or local filesystem

---

## Architecture Principles

### 1. **Separation of Concerns**
Tables are grouped by functional domain using prefixes:
- `sys_` → System configuration and security
- `usr_` → User accounts and social features
- `crt_` → Content creator profiles
- `cnt_` → Content and interactions
- `fs_` → File storage

### 2. **Referential Integrity**
All relationships use foreign keys with explicit cascade behaviors. No polymorphic relationships—each content type has dedicated tables.

### 3. **Performance Optimization**
- Denormalized engagement counts (likes, comments, views) on content tables
- Denormalized follower/following counts on user profiles
- Strategic indexes on frequently queried columns
- Soft deletes to preserve data integrity

### 4. **Audit Trail**
- `created_at` and `updated_at` timestamps on all tables
- Admin action logging in `sys_audit_log`
- Creator status history tracking

### 5. **Extensibility**
- JSONB columns for flexible metadata
- Storage-agnostic file system design
- Permission-based RBAC for easy role expansion

---

## Naming Conventions

### Tables
- **Prefix-based grouping**: `<domain>_<entity_name>`
- **snake_case**: All lowercase with underscores
- **Plural or singular**: Context-dependent (e.g., `usr_users`, `fs_files`)

### Columns
- **snake_case**: All lowercase with underscores
- **Descriptive names**: `creator_id`, `video_file_id`, `like_count`
- **Timestamp suffix**: `created_at`, `updated_at`, `deleted_at`, `expires_at`

### Indexes
- **Pattern**: `idx_<table>_<column(s)>`
- **Example**: `idx_cnt_posts_creator`

### Foreign Keys
- **Pattern**: `fk_<table>_<reference>`
- **Example**: `fk_usr_users_profile_picture`

---

## Database Schema

### 1. System & Configuration (`sys_`)

#### `sys_parameters`
**Purpose**: Global system configuration key-value store.

| Column | Type | Constraints | Description |
|--------|------|-------------|-------------|
| `id` | `SERIAL` | `PRIMARY KEY` | Auto-increment ID |
| `key` | `VARCHAR(100)` | `UNIQUE NOT NULL` | Configuration key |
| `value` | `TEXT` | `NOT NULL` | Configuration value |
| `description` | `TEXT` | `NULL` | Human-readable description |
| `created_at` | `TIMESTAMP` | `DEFAULT NOW()` | Creation timestamp |
| `updated_at` | `TIMESTAMP` | `DEFAULT NOW()` | Auto-updated on change |

**Example Data**:
```
key: "max_file_size_mb", value: "500"
key: "story_duration_hours", value: "24"
key: "max_hashtags_per_post", value: "30"
```

---

#### `sys_permissions`
**Purpose**: Defines granular permissions for RBAC system.

| Column | Type | Constraints | Description |
|--------|------|-------------|-------------|
| `id` | `SERIAL` | `PRIMARY KEY` | Permission ID |
| `name` | `VARCHAR(100)` | `UNIQUE NOT NULL` | Permission name (e.g., "create_post") |
| `resource` | `VARCHAR(50)` | `NOT NULL` | Resource type (e.g., "posts", "users") |
| `action` | `VARCHAR(50)` | `NOT NULL` | Action type (e.g., "create", "moderate") |
| `description` | `TEXT` | `NULL` | Permission description |
| `created_at` | `TIMESTAMP` | `DEFAULT NOW()` | Creation timestamp |

**Index**: `idx_sys_permissions_resource` on `(resource, action)`

**Example Permissions**:
- `view_content` (resource: content, action: read)
- `create_story` (resource: stories, action: create)
- `moderate_users` (resource: users, action: moderate)

---

#### `sys_role_permissions`
**Purpose**: Maps permissions to user roles (user, content_creator, admin).

| Column | Type | Constraints | Description |
|--------|------|-------------|-------------|
| `role` | `VARCHAR(20)` | `CHECK IN (...)` | Role: user, content_creator, admin |
| `permission_id` | `INTEGER` | `FK → sys_permissions(id)` | Permission reference |
| `created_at` | `TIMESTAMP` | `DEFAULT NOW()` | Mapping creation time |

**Primary Key**: `(role, permission_id)`

**Permission Inheritance**:
- **User**: View, like, comment on content, follow users
- **Content Creator**: User permissions + create/edit posts/reels/stories
- **Admin**: All permissions (full platform access)

---

#### `sys_audit_log`
**Purpose**: Tracks admin actions and critical system operations.

| Column | Type | Constraints | Description |
|--------|------|-------------|-------------|
| `id` | `BIGSERIAL` | `PRIMARY KEY` | Log entry ID |
| `user_id` | `BIGINT` | `FK → usr_users(id)` | User who performed action |
| `action` | `VARCHAR(100)` | `NOT NULL` | Action name (e.g., "ban_user") |
| `resource_type` | `VARCHAR(50)` | `NOT NULL` | Resource affected (e.g., "creators") |
| `resource_id` | `BIGINT` | `NULL` | ID of affected resource |
| `changes` | `JSONB` | `NULL` | Before/after values |
| `ip_address` | `INET` | `NULL` | User's IP address |
| `user_agent` | `TEXT` | `NULL` | Browser/client info |
| `created_at` | `TIMESTAMP` | `DEFAULT NOW()` | Action timestamp |

**Indexes**:
- `idx_sys_audit_log_user` on `(user_id, created_at DESC)`
- `idx_sys_audit_log_resource` on `(resource_type, resource_id, created_at DESC)`

---

### 2. Users & Authentication (`usr_`)

#### `usr_users`
**Purpose**: Core user accounts for all platform members.

| Column | Type | Constraints | Description |
|--------|------|-------------|-------------|
| `id` | `BIGSERIAL` | `PRIMARY KEY` | User ID |
| `username` | `VARCHAR(50)` | `UNIQUE NOT NULL` | Unique username |
| `email` | `VARCHAR(255)` | `UNIQUE NOT NULL` | Email address |
| `password_hash` | `VARCHAR(255)` | `NOT NULL` | Hashed password |
| `role` | `VARCHAR(20)` | `CHECK IN (...)` | Role: user, content_creator, admin |
| `is_active` | `BOOLEAN` | `DEFAULT TRUE` | Account active status |
| `profile_picture_id` | `BIGINT` | `FK → fs_files(id)` | Profile picture reference |
| `bio` | `TEXT` | `NULL` | User biography |
| `follower_count` | `INTEGER` | `DEFAULT 0` | Total followers (denormalized) |
| `following_count` | `INTEGER` | `DEFAULT 0` | Total following (denormalized) |
| `last_login_at` | `TIMESTAMP` | `NULL` | Last login timestamp |
| `last_login_ip` | `INET` | `NULL` | Last login IP address |
| `created_at` | `TIMESTAMP` | `DEFAULT NOW()` | Account creation date |
| `updated_at` | `TIMESTAMP` | `DEFAULT NOW()` | Last update timestamp |
| `deleted_at` | `TIMESTAMP` | `NULL` | Soft delete timestamp |
| `deleted_by` | `BIGINT` | `FK → usr_users(id)` | Admin who deleted account |

**Indexes**:
- `idx_usr_users_email` on `(email)` where `deleted_at IS NULL`
- `idx_usr_users_username` on `(username)` where `deleted_at IS NULL`
- `idx_usr_users_role` on `(role)` where `is_active = TRUE`
- `idx_usr_users_followers` on `(follower_count DESC)` where `deleted_at IS NULL`

---

### 3. Content Creators (`crt_`)

#### `crt_creators`
**Purpose**: Extended profile for users who create content.

| Column | Type | Constraints | Description |
|--------|------|-------------|-------------|
| `id` | `BIGSERIAL` | `PRIMARY KEY` | Creator ID |
| `user_id` | `BIGINT` | `UNIQUE FK → usr_users(id)` | Associated user account |
| `creator_type` | `VARCHAR(20)` | `CHECK IN (...)` | Type: individual or company |
| `display_name` | `VARCHAR(100)` | `NOT NULL` | Public display name |
| `verified` | `BOOLEAN` | `DEFAULT FALSE` | Verification badge status |
| `status` | `VARCHAR(20)` | `CHECK IN (...)` | Status: active, suspended, banned, under_review |
| `status_changed_at` | `TIMESTAMP` | `NULL` | Last status change time |
| `status_changed_by` | `BIGINT` | `FK → usr_users(id)` | Admin who changed status |
| `created_at` | `TIMESTAMP` | `DEFAULT NOW()` | Creator profile creation |
| `updated_at` | `TIMESTAMP` | `DEFAULT NOW()` | Last update timestamp |

**Indexes**:
- `idx_crt_creators_user` on `(user_id)`
- `idx_crt_creators_type` on `(creator_type)`
- `idx_crt_creators_status` on `(status)` where `status != 'banned'`

---

#### `crt_companies`
**Purpose**: Company-specific information for corporate creators.

| Column | Type | Constraints | Description |
|--------|------|-------------|-------------|
| `id` | `BIGSERIAL` | `PRIMARY KEY` | Company ID |
| `creator_id` | `BIGINT` | `UNIQUE FK → crt_creators(id)` | Associated creator profile |
| `company_name` | `VARCHAR(255)` | `NOT NULL` | Legal company name |
| `registration_number` | `VARCHAR(100)` | `NULL` | Business registration number |
| `industry` | `VARCHAR(100)` | `NULL` | Industry/sector |
| `website` | `VARCHAR(255)` | `NULL` | Company website URL |
| `contact_email` | `VARCHAR(255)` | `NULL` | Business contact email |
| `contact_phone` | `VARCHAR(50)` | `NULL` | Business phone number |
| `address` | `TEXT` | `NULL` | Business address |
| `created_at` | `TIMESTAMP` | `DEFAULT NOW()` | Record creation time |
| `updated_at` | `TIMESTAMP` | `DEFAULT NOW()` | Last update timestamp |

---

#### `crt_creator_status_history`
**Purpose**: Audit trail for creator moderation actions.

| Column | Type | Constraints | Description |
|--------|------|-------------|-------------|
| `id` | `BIGSERIAL` | `PRIMARY KEY` | History entry ID |
| `creator_id` | `BIGINT` | `FK → crt_creators(id)` | Creator being moderated |
| `status` | `VARCHAR(20)` | `CHECK IN (...)` | New status applied |
| `reason` | `TEXT` | `NULL` | Reason for status change |
| `actioned_by` | `BIGINT` | `FK → usr_users(id)` | Admin who made the change |
| `created_at` | `TIMESTAMP` | `DEFAULT NOW()` | Change timestamp |

**Index**: `idx_crt_creator_status_history_creator` on `(creator_id, created_at DESC)`

---

### 4. File Storage (`fs_`)

#### `fs_files`
**Purpose**: Storage-agnostic file metadata for all uploaded media.

| Column | Type | Constraints | Description |
|--------|------|-------------|-------------|
| `id` | `BIGSERIAL` | `PRIMARY KEY` | File ID |
| `uploader_id` | `BIGINT` | `FK → usr_users(id)` | User who uploaded file |
| `storage_provider` | `VARCHAR(50)` | `NOT NULL` | Provider: minio, s3, local |
| `bucket_name` | `VARCHAR(255)` | `NOT NULL` | Bucket/container name |
| `storage_key` | `VARCHAR(500)` | `NOT NULL` | Full path in storage |
| `file_name` | `VARCHAR(255)` | `NOT NULL` | Original file name |
| `mime_type` | `VARCHAR(100)` | `NOT NULL` | MIME type (e.g., "image/jpeg") |
| `file_size_bytes` | `BIGINT` | `NOT NULL` | File size in bytes |
| `file_hash` | `VARCHAR(64)` | `NOT NULL` | SHA-256 hash (deduplication) |
| `width` | `INTEGER` | `NULL` | Image/video width in pixels |
| `height` | `INTEGER` | `NULL` | Image/video height in pixels |
| `duration_seconds` | `INTEGER` | `NULL` | Video duration |
| `metadata` | `JSONB` | `NULL` | Additional metadata (EXIF, etc.) |
| `is_public` | `BOOLEAN` | `DEFAULT FALSE` | Public access flag |
| `upload_status` | `VARCHAR(20)` | `CHECK IN (...)` | Status: pending, processing, completed, failed |
| `created_at` | `TIMESTAMP` | `DEFAULT NOW()` | Upload timestamp |
| `updated_at` | `TIMESTAMP` | `DEFAULT NOW()` | Last update timestamp |
| `deleted_at` | `TIMESTAMP` | `NULL` | Soft delete timestamp |

**Indexes**:
- `idx_fs_files_uploader` on `(uploader_id)`
- `idx_fs_files_hash` on `(file_hash)` where `deleted_at IS NULL`
- `idx_fs_files_provider` on `(storage_provider, bucket_name)`

---

### 5. Posts (`cnt_`)

#### `cnt_posts`
**Purpose**: User-generated posts with text and/or media content.

| Column | Type | Constraints | Description |
|--------|------|-------------|-------------|
| `id` | `BIGSERIAL` | `PRIMARY KEY` | Post ID |
| `creator_id` | `BIGINT` | `FK → crt_creators(id)` | Post author |
| `content_text` | `TEXT` | `NULL` | Post text content |
| `is_published` | `BOOLEAN` | `DEFAULT TRUE` | Published status |
| `view_count` | `BIGINT` | `DEFAULT 0` | Total views (from analytics) |
| `like_count` | `INTEGER` | `DEFAULT 0` | Total likes (auto-updated) |
| `comment_count` | `INTEGER` | `DEFAULT 0` | Total comments (auto-updated) |
| `created_at` | `TIMESTAMP` | `DEFAULT NOW()` | Post creation time |
| `updated_at` | `TIMESTAMP` | `DEFAULT NOW()` | Last edit time |
| `deleted_at` | `TIMESTAMP` | `NULL` | Soft delete timestamp |

**Indexes**:
- `idx_cnt_posts_creator` on `(creator_id, created_at DESC)`
- `idx_cnt_posts_created` on `(created_at DESC)` where `deleted_at IS NULL`
- `idx_cnt_posts_published` on `(is_published, created_at DESC)` where `deleted_at IS NULL`

---

#### `cnt_post_media`
**Purpose**: Junction table linking posts to multiple media files.

| Column | Type | Constraints | Description |
|--------|------|-------------|-------------|
| `id` | `BIGSERIAL` | `PRIMARY KEY` | Junction ID |
| `post_id` | `BIGINT` | `FK → cnt_posts(id)` | Post reference |
| `file_id` | `BIGINT` | `FK → fs_files(id)` | File reference |
| `display_order` | `INTEGER` | `DEFAULT 0` | Order of media in post |
| `created_at` | `TIMESTAMP` | `DEFAULT NOW()` | Link creation time |

**Indexes**:
- `idx_cnt_post_media_unique` (UNIQUE) on `(post_id, file_id)`
- `idx_cnt_post_media_post` on `(post_id, display_order)`

---

#### `cnt_post_likes`
**Purpose**: Tracks user likes on posts.

| Column | Type | Constraints | Description |
|--------|------|-------------|-------------|
| `id` | `BIGSERIAL` | `PRIMARY KEY` | Like ID |
| `user_id` | `BIGINT` | `FK → usr_users(id)` | User who liked |
| `post_id` | `BIGINT` | `FK → cnt_posts(id)` | Post being liked |
| `created_at` | `TIMESTAMP` | `DEFAULT NOW()` | Like timestamp |

**Indexes**:
- `idx_cnt_post_likes_unique` (UNIQUE) on `(user_id, post_id)`
- `idx_cnt_post_likes_post` on `(post_id, created_at DESC)`

---

#### `cnt_post_comments`
**Purpose**: User comments on posts with threading support.

| Column | Type | Constraints | Description |
|--------|------|-------------|-------------|
| `id` | `BIGSERIAL` | `PRIMARY KEY` | Comment ID |
| `user_id` | `BIGINT` | `FK → usr_users(id)` | Commenter |
| `post_id` | `BIGINT` | `FK → cnt_posts(id)` | Post being commented on |
| `parent_comment_id` | `BIGINT` | `FK → cnt_post_comments(id)` | Parent comment (for replies) |
| `content` | `TEXT` | `NOT NULL` | Comment text |
| `created_at` | `TIMESTAMP` | `DEFAULT NOW()` | Comment timestamp |
| `updated_at` | `TIMESTAMP` | `DEFAULT NOW()` | Edit timestamp |
| `deleted_at` | `TIMESTAMP` | `NULL` | Soft delete timestamp |

**Indexes**:
- `idx_cnt_post_comments_post` on `(post_id, created_at DESC)` where `deleted_at IS NULL`
- `idx_cnt_post_comments_parent` on `(parent_comment_id)` where `parent_comment_id IS NOT NULL`

---

### 6. Reels (`cnt_`)

#### `cnt_reels`
**Purpose**: Short-form video content (Instagram-style reels).

| Column | Type | Constraints | Description |
|--------|------|-------------|-------------|
| `id` | `BIGSERIAL` | `PRIMARY KEY` | Reel ID |
| `creator_id` | `BIGINT` | `FK → crt_creators(id)` | Reel author |
| `title` | `VARCHAR(255)` | `NULL` | Reel title |
| `description` | `TEXT` | `NULL` | Reel description |
| `video_file_id` | `BIGINT` | `FK → fs_files(id)` | Video file reference |
| `thumbnail_file_id` | `BIGINT` | `FK → fs_files(id)` | Thumbnail image reference |
| `duration_seconds` | `INTEGER` | `NOT NULL` | Video duration |
| `is_published` | `BOOLEAN` | `DEFAULT TRUE` | Published status |
| `view_count` | `BIGINT` | `DEFAULT 0` | Total views (from analytics) |
| `like_count` | `INTEGER` | `DEFAULT 0` | Total likes (auto-updated) |
| `comment_count` | `INTEGER` | `DEFAULT 0` | Total comments (auto-updated) |
| `created_at` | `TIMESTAMP` | `DEFAULT NOW()` | Reel creation time |
| `updated_at` | `TIMESTAMP` | `DEFAULT NOW()` | Last edit time |
| `deleted_at` | `TIMESTAMP` | `NULL` | Soft delete timestamp |

**Indexes**:
- `idx_cnt_reels_creator` on `(creator_id, created_at DESC)`
- `idx_cnt_reels_created` on `(created_at DESC)` where `deleted_at IS NULL`
- `idx_cnt_reels_published` on `(is_published, created_at DESC)` where `deleted_at IS NULL`

---

#### `cnt_reel_likes`
**Purpose**: Tracks user likes on reels.

| Column | Type | Constraints | Description |
|--------|------|-------------|-------------|
| `id` | `BIGSERIAL` | `PRIMARY KEY` | Like ID |
| `user_id` | `BIGINT` | `FK → usr_users(id)` | User who liked |
| `reel_id` | `BIGINT` | `FK → cnt_reels(id)` | Reel being liked |
| `created_at` | `TIMESTAMP` | `DEFAULT NOW()` | Like timestamp |

**Indexes**:
- `idx_cnt_reel_likes_unique` (UNIQUE) on `(user_id, reel_id)`
- `idx_cnt_reel_likes_reel` on `(reel_id, created_at DESC)`

---

#### `cnt_reel_comments`
**Purpose**: User comments on reels with threading support.

| Column | Type | Constraints | Description |
|--------|------|-------------|-------------|
| `id` | `BIGSERIAL` | `PRIMARY KEY` | Comment ID |
| `user_id` | `BIGINT` | `FK → usr_users(id)` | Commenter |
| `reel_id` | `BIGINT` | `FK → cnt_reels(id)` | Reel being commented on |
| `parent_comment_id` | `BIGINT` | `FK → cnt_reel_comments(id)` | Parent comment (for replies) |
| `content` | `TEXT` | `NOT NULL` | Comment text |
| `created_at` | `TIMESTAMP` | `DEFAULT NOW()` | Comment timestamp |
| `updated_at` | `TIMESTAMP` | `DEFAULT NOW()` | Edit timestamp |
| `deleted_at` | `TIMESTAMP` | `NULL` | Soft delete timestamp |

**Indexes**:
- `idx_cnt_reel_comments_reel` on `(reel_id, created_at DESC)` where `deleted_at IS NULL`
- `idx_cnt_reel_comments_parent` on `(parent_comment_id)` where `parent_comment_id IS NOT NULL`

---

### 7. Stories (`cnt_`)

#### `cnt_stories`
**Purpose**: 24-hour ephemeral content (Instagram-style stories).

| Column | Type | Constraints | Description |
|--------|------|-------------|-------------|
| `id` | `BIGSERIAL` | `PRIMARY KEY` | Story ID |
| `creator_id` | `BIGINT` | `FK → crt_creators(id)` | Story author |
| `media_file_id` | `BIGINT` | `FK → fs_files(id)` | Media file (image or video) |
| `content_text` | `TEXT` | `NULL` | Optional text overlay |
| `view_count` | `BIGINT` | `DEFAULT 0` | Total views (from analytics) |
| `is_published` | `BOOLEAN` | `DEFAULT TRUE` | Published status |
| `created_at` | `TIMESTAMP` | `DEFAULT NOW()` | Story creation time |
| `expires_at` | `TIMESTAMP` | `NOT NULL` | Expiration time (24h from creation) |
| `deleted_at` | `TIMESTAMP` | `NULL` | Soft delete timestamp |

**Indexes**:
- `idx_cnt_stories_creator` on `(creator_id, created_at DESC)`
- `idx_cnt_stories_expires` on `(expires_at)` where `deleted_at IS NULL`
- `idx_cnt_stories_active` on `(created_at DESC)` where `expires_at > NOW() AND deleted_at IS NULL`

**Cleanup Strategy**: 
- Background job periodically deletes expired stories (`WHERE expires_at < NOW()`)
- Expired stories can be archived before deletion for analytics

---

#### `cnt_story_views`
**Purpose**: Tracks who viewed each story (for creator insights).

| Column | Type | Constraints | Description |
|--------|------|-------------|-------------|
| `id` | `BIGSERIAL` | `PRIMARY KEY` | View ID |
| `story_id` | `BIGINT` | `FK → cnt_stories(id)` | Story being viewed |
| `viewer_id` | `BIGINT` | `FK → usr_users(id)` | User who viewed |
| `created_at` | `TIMESTAMP` | `DEFAULT NOW()` | View timestamp |

**Indexes**:
- `idx_cnt_story_views_unique` (UNIQUE) on `(story_id, viewer_id)`
- `idx_cnt_story_views_story` on `(story_id, created_at DESC)`
- `idx_cnt_story_views_viewer` on `(viewer_id, created_at DESC)`

**Note**: Each user can view a story multiple times, but only the first view is recorded.

---

### 8. Hashtags (`cnt_`)

#### `cnt_hashtags`
**Purpose**: Master list of all hashtags used on the platform.

| Column | Type | Constraints | Description |
|--------|------|-------------|-------------|
| `id` | `BIGSERIAL` | `PRIMARY KEY` | Hashtag ID |
| `tag` | `VARCHAR(100)` | `UNIQUE NOT NULL` | Hashtag text (without #) |
| `usage_count` | `BIGINT` | `DEFAULT 0` | Total times used (denormalized) |
| `created_at` | `TIMESTAMP` | `DEFAULT NOW()` | First usage timestamp |
| `updated_at` | `TIMESTAMP` | `DEFAULT NOW()` | Last usage timestamp |

**Indexes**:
- `idx_cnt_hashtags_tag` (UNIQUE) on `(LOWER(tag))`
- `idx_cnt_hashtags_usage` on `(usage_count DESC)`

**Normalization**: Tags stored in lowercase for consistency (e.g., "travel", not "Travel" or "TRAVEL")

---

#### `cnt_post_hashtags`
**Purpose**: Junction table linking posts to hashtags.

| Column | Type | Constraints | Description |
|--------|------|-------------|-------------|
| `id` | `BIGSERIAL` | `PRIMARY KEY` | Junction ID |
| `post_id` | `BIGINT` | `FK → cnt_posts(id)` | Post reference |
| `hashtag_id` | `BIGINT` | `FK → cnt_hashtags(id)` | Hashtag reference |
| `created_at` | `TIMESTAMP` | `DEFAULT NOW()` | Link creation time |

**Indexes**:
- `idx_cnt_post_hashtags_unique` (UNIQUE) on `(post_id, hashtag_id)`
- `idx_cnt_post_hashtags_post` on `(post_id)`
- `idx_cnt_post_hashtags_hashtag` on `(hashtag_id, created_at DESC)`

---

#### `cnt_reel_hashtags`
**Purpose**: Junction table linking reels to hashtags.

| Column | Type | Constraints | Description |
|--------|------|-------------|-------------|
| `id` | `BIGSERIAL` | `PRIMARY KEY` | Junction ID |
| `reel_id` | `BIGINT` | `FK → cnt_reels(id)` | Reel reference |
| `hashtag_id` | `BIGINT` | `FK → cnt_hashtags(id)` | Hashtag reference |
| `created_at` | `TIMESTAMP` | `DEFAULT NOW()` | Link creation time |

**Indexes**:
- `idx_cnt_reel_hashtags_unique` (UNIQUE) on `(reel_id, hashtag_id)`
- `idx_cnt_reel_hashtags_reel` on `(reel_id)`
- `idx_cnt_reel_hashtags_hashtag` on `(hashtag_id, created_at DESC)`

---

### 9. Social Features (`usr_`)

#### `usr_follows`
**Purpose**: Tracks follower/following relationships between users.

| Column | Type | Constraints | Description |
|--------|------|-------------|-------------|
| `id` | `BIGSERIAL` | `PRIMARY KEY` | Follow ID |
| `follower_id` | `BIGINT` | `FK → usr_users(id)` | User who is following |
| `following_id` | `BIGINT` | `FK → usr_users(id)` | User being followed |
| `created_at` | `TIMESTAMP` | `DEFAULT NOW()` | Follow timestamp |

**Indexes**:
- `idx_usr_follows_unique` (UNIQUE) on `(follower_id, following_id)`
- `idx_usr_follows_follower` on `(follower_id, created_at DESC)`
- `idx_usr_follows_following` on `(following_id, created_at DESC)`

**Constraints**:
- Check constraint: `follower_id != following_id` (cannot follow yourself)

**Behavior**:
- Insert triggers increment `usr_users.following_count` (for follower) and `usr_users.follower_count` (for following)
- Delete triggers decrement both counts

**Use Cases**:
- Get followers: `SELECT follower_id FROM usr_follows WHERE following_id = ?`
- Get following: `SELECT following_id FROM usr_follows WHERE follower_id = ?`
- Check if following: `SELECT 1 FROM usr_follows WHERE follower_id = ? AND following_id = ?`

---

## Entity Relationships

### Relationship Diagram

```
usr_users (1)
    ↓
    ├──> (0-1) crt_creators
    │         ↓
    │         ├──> (0-1) crt_companies [if creator_type='company']
    │         │
    │         ├──> (0-N) cnt_posts
    │         │         ↓
    │         │         ├──> (0-N) cnt_post_media ──> (N) fs_files
    │         │         ├──> (0-N) cnt_post_likes
    │         │         ├──> (0-N) cnt_post_comments
    │         │         └──> (0-N) cnt_post_hashtags ──> (N) cnt_hashtags
    │         │
    │         ├──> (0-N) cnt_reels
    │         │         ↓
    │         │         ├──> (1) fs_files [video]
    │         │         ├──> (0-1) fs_files [thumbnail]
    │         │         ├──> (0-N) cnt_reel_likes
    │         │         ├──> (0-N) cnt_reel_comments
    │         │         └──> (0-N) cnt_reel_hashtags ──> (N) cnt_hashtags
    │         │
    │         └──> (0-N) cnt_stories
    │                   ↓
    │                   ├──> (1) fs_files [media]
    │                   └──> (0-N) cnt_story_views
    │
    ├──> (0-N) usr_follows [as follower]
    ├──> (0-N) usr_follows [as following]
    ├──> (0-N) cnt_post_likes
    ├──> (0-N) cnt_post_comments
    ├──> (0-N) cnt_reel_likes
    ├──> (0-N) cnt_reel_comments
    ├──> (0-N) cnt_story_views
    ├──> (0-N) fs_files [uploads]
    └──> (0-1) fs_files [profile_picture]

sys_permissions (N) ←──> (N) sys_role_permissions ──> (3) roles
```

### Key Relationships

| Parent | Child | Type | Cascade Behavior |
|--------|-------|------|------------------|
| `usr_users` | `crt_creators` | 1:1 | `ON DELETE CASCADE` |
| `usr_users` | `usr_follows` (follower) | 1:N | `ON DELETE CASCADE` |
| `usr_users` | `usr_follows` (following) | 1:N | `ON DELETE CASCADE` |
| `crt_creators` | `crt_companies` | 1:0..1 | `ON DELETE CASCADE` |
| `crt_creators` | `cnt_posts` | 1:N | `ON DELETE CASCADE` |
| `crt_creators` | `cnt_reels` | 1:N | `ON DELETE CASCADE` |
| `crt_creators` | `cnt_stories` | 1:N | `ON DELETE CASCADE` |
| `cnt_posts` | `cnt_post_hashtags` | N:M | `ON DELETE CASCADE` |
| `cnt_reels` | `cnt_reel_hashtags` | N:M | `ON DELETE CASCADE` |
| `cnt_hashtags` | `cnt_post_hashtags` | 1:N | `ON DELETE CASCADE` |
| `cnt_hashtags` | `cnt_reel_hashtags` | 1:N | `ON DELETE CASCADE` |
| `cnt_stories` | `cnt_story_views` | 1:N | `ON DELETE CASCADE` |

---

## Key Design Decisions

### 1. **Stories as Separate Table (Not Polymorphic)**
**Decision**: Dedicated `cnt_stories` table instead of merging with posts/reels.

**Rationale**:
- **Different lifecycle**: Stories expire after 24 hours (auto-cleanup required)
- **Different features**: No likes/comments, only views
- **Performance**: Separate table allows efficient expiration queries
- **Simpler queries**: No type filtering needed

**Cleanup**: Background job runs hourly to delete expired stories:
```sql
DELETE FROM cnt_stories WHERE expires_at < NOW() - INTERVAL '7 days';
```

---

### 2. **Hashtag Normalization**
**Decision**: Master hashtag table with junction tables for posts/reels.

**Rationale**:
- **Consistency**: Single source of truth for each hashtag
- **Trending calculation**: `usage_count` enables trending hashtags
- **Search optimization**: Index on hashtag name for fast lookups
- **Typo prevention**: Application normalizes to lowercase before insert

**Discovery Features**:
- Search by hashtag: `SELECT * FROM cnt_posts WHERE id IN (SELECT post_id FROM cnt_post_hashtags WHERE hashtag_id = ?)`
- Trending hashtags: `SELECT * FROM cnt_hashtags ORDER BY usage_count DESC LIMIT 50`
- Related hashtags: Query co-occurrence in same posts

---

### 3. **Follow Relationship as Self-Referencing Table**
**Decision**: Single `usr_follows` table with `follower_id` and `following_id`.

**Rationale**:
- **Bidirectional queries**: Easy to query both followers and following
- **Denormalized counts**: `follower_count` and `following_count` in `usr_users` for fast profile display
- **No redundancy**: Single row represents the relationship
- **Mutual follows**: Requires two rows (A→B and B→A)

**Performance**: Triggers automatically maintain counts on insert/delete.

---

### 4. **Story Views Tracking**
**Decision**: Separate `cnt_story_views` table instead of using analytics.

**Rationale**:
- **Creator insights**: Creators need to see who viewed their story
- **Privacy consideration**: Unlike posts/reels, story views are visible to creator
- **Real-time**: Immediate feedback without analytics delay
- **Ephemeral**: Auto-deleted when story expires

**Alternative considered**: Store in analytics (Firebase/PostHog)
- **Rejected**: Cannot query individual viewers from external analytics

---

### 5. **Hashtag Usage Count Denormalization**
**Decision**: Store `usage_count` in `cnt_hashtags` table.

**Rationale**:
- **Trending discovery**: Fast query for trending hashtags without COUNT aggregation
- **Caching friendly**: Single value instead of expensive count query
- **Real-time updates**: Triggers update on hashtag link insert/delete

**Update mechanism**:
```sql
-- On insert to cnt_post_hashtags
UPDATE cnt_hashtags SET usage_count = usage_count + 1 WHERE id = NEW.hashtag_id;
```

---

### 6. **Follower/Following Count Denormalization**
**Decision**: Store counts in `usr_users` instead of counting on-demand.

**Rationale**:
- **Profile performance**: Display counts without JOIN or COUNT query
- **Popular on social platforms**: Users expect instant count display
- **Sort by popularity**: Easy to find top users by followers
- **Consistency**: Triggers ensure accuracy

**Trade-off**: Slight write overhead (acceptable for read-heavy social platform)

---

### 7. **Stories Expiration Strategy**
**Decision**: `expires_at` timestamp with background cleanup job.

**Rationale**:
- **Automatic expiration**: No manual intervention needed
- **Grace period**: Keep for 7 days after expiration (analytics/compliance)
- **Performance**: Index on `expires_at` for efficient cleanup
- **Flexibility**: Can extend expiration for premium users

**Implementation**:
```sql
-- Cron job runs every hour
DELETE FROM cnt_stories 
WHERE expires_at < NOW() - INTERVAL '7 days'
  AND deleted_at IS NULL;
```

---

## Security & Access Control

### Role-Based Access Control (RBAC)

#### Permission Model
1. **Permissions defined in** `sys_permissions`:
   - Resource (e.g., "posts", "stories", "users")
   - Action (e.g., "create", "moderate")
   - Name (e.g., "create_story", "follow_user")

2. **Roles mapped in** `sys_role_permissions`:
   - Each role assigned specific permissions
   - Inheritance: content_creator includes all user permissions
   - Admin has all permissions

3. **Enforcement**:
   - Application checks `sys_role_permissions` before allowing actions
   - Database enforces via foreign key constraints
   - Admin actions logged in `sys_audit_log`

#### Permission Examples

| Role | Can Do | Cannot Do |
|------|--------|-----------|
| **User** | View content, like, comment, follow users | Create posts/reels/stories, moderate |
| **Content Creator** | Everything User can + create/edit own posts/reels/stories | Moderate others' content, ban users |
| **Admin** | Everything + moderate all content, ban users, view analytics | (No restrictions) |

---

### Social Graph Privacy

#### Follow Relationships
- **Public by default**: Anyone can see followers/following lists
- **Application-level privacy**: Can implement private accounts
- **Block feature**: Application prevents follow if user blocked

#### Story Views
- **Creator visibility**: Creator can see who viewed their stories
- **Viewer privacy**: Other users cannot see who viewed stories
- **Anonymous views**: Optional feature to hide viewer from creator

---

### Data Ownership

#### Creator → Content
- Creators can only edit/delete **their own** posts, reels, and stories
- Enforced by application checking `creator_id` against current user
- Admins can moderate any content (logged in `sys_audit_log`)

#### User → Engagement
- Users can only delete **their own** likes, comments, and follows
- Edit comment: Check `user_id` matches current user
- Admins can delete any comment (moderation)

---

## Scalability Considerations

### Indexed Queries
All frequently queried patterns have dedicated indexes:
- User lookup by email/username
- Follower/following relationships (bidirectional)
- Hashtag search and trending
- Active stories (not expired)
- Content by creator ordered by date
- Post/reel by hashtag with time sorting

### Write Optimization
- **Engagement counts**: Triggers update in single transaction
- **Hashtag usage**: Triggers update hashtag counts
- **Follow counts**: Triggers update user counts
- **Story cleanup**: Scheduled batch deletion

### Read Optimization
- **Denormalized counts**: No COUNT(*) on large tables
- **Filtered indexes**: Exclude deleted/expired records
- **Hashtag caching**: Cache trending hashtags (1 hour TTL)
- **Follow status**: Cache follow relationships (5 min TTL)

### Hot Hashtags Strategy
**Problem**: Popular hashtags (e.g., #love) could have millions of posts.

**Solution**:
1. **Pagination**: Always use cursor-based pagination on hashtag queries
2. **Time filters**: Default to "last 7 days" for popular hashtags
3. **Relevance scoring**: Use external search (Elasticsearch) for ranking
4. **Caching**: Cache first page of trending hashtags

---

### Story Scaling
**Problem**: High write volume (millions of stories/day), most expire.

**Solution**:
1. **Partitioning**: Partition `cnt_stories` by `created_at` (daily)
2. **Auto-drop old partitions**: Drop partitions older than 7 days
3. **Separate analytics**: Send story views to Firebase/PostHog
4. **CDN delivery**: Serve story media from CDN (not database)

---

### Follower Feed Generation
**Problem**: Generate feed for user following 1000+ accounts.

**Solution**:
1. **Fan-out on write**: Pre-compute feed for active users (Redis)
2. **Hybrid approach**: Mix cached feed + recent posts query
3. **Pagination**: Never load entire feed, always paginate
4. **Background job**: Async feed computation for large follower counts

**Query example** (simple approach):
```sql
SELECT p.* FROM cnt_posts p
JOIN usr_follows f ON f.following_id = p.creator_id
WHERE f.follower_id = ?
  AND p.deleted_at IS NULL
  AND p.is_published = TRUE
ORDER BY p.created_at DESC
LIMIT 20;
```

---

## Data Integrity

### Foreign Key Constraints
All relationships enforced at database level:
- ✅ Cannot follow deleted user (CASCADE delete)
- ✅ Cannot tag post with non-existent hashtag
- ✅ Cannot view deleted story (CASCADE delete)
- ✅ Cannot create story without valid media file

### Unique Constraints
Prevent duplicate data:
- ✅ User cannot follow same person twice
- ✅ User cannot like same post twice
- ✅ User cannot view same story twice (for tracking)
- ✅ Post cannot use same hashtag twice
- ✅ Hashtag text must be unique (case-insensitive)

### Check Constraints
Validate data at insert/update:
- ✅ Cannot follow yourself (`follower_id != following_id`)
- ✅ Story `expires_at` must be after `created_at`
- ✅ Hashtag tag cannot be empty string
- ✅ Usage counts cannot be negative

### Cascading Deletes Strategy

| When Deleted | What Happens | Why |
|--------------|--------------|-----|
| User account | All follows CASCADE deleted | Clean up relationships |
| Creator profile | All posts/reels/stories CASCADE deleted | Remove all content |
| Post | All likes/comments/hashtags CASCADE deleted | Clean up engagement |
| Story (expired) | All views CASCADE deleted | No orphaned data |
| Hashtag | All post/reel links CASCADE deleted | Remove broken references |

---

## Future Extensions

### Potential Features (Not Currently Implemented)

#### 1. **Saved Posts/Collections**
```sql
CREATE TABLE usr_saved_posts (
  id BIGSERIAL PRIMARY KEY,
  user_id BIGINT REFERENCES usr_users(id) ON DELETE CASCADE,
  post_id BIGINT REFERENCES cnt_posts(id) ON DELETE CASCADE,
  collection_name VARCHAR(100), -- Optional grouping
  created_at TIMESTAMP DEFAULT NOW(),
  UNIQUE (user_id, post_id)
);

CREATE TABLE usr_saved_reels (
  id BIGSERIAL PRIMARY KEY,
  user_id BIGINT REFERENCES usr_users(id) ON DELETE CASCADE,
  reel_id BIGINT REFERENCES cnt_reels(id) ON DELETE CASCADE,
  collection_name VARCHAR(100),
  created_at TIMESTAMP DEFAULT NOW(),
  UNIQUE (user_id, reel_id)
);
```

---

#### 2. **Direct Messaging**
```sql
CREATE TABLE msg_conversations (
  id BIGSERIAL PRIMARY KEY,
  is_group BOOLEAN DEFAULT FALSE,
  name VARCHAR(255), -- For group chats
  created_at TIMESTAMP DEFAULT NOW()
);

CREATE TABLE msg_participants (
  conversation_id BIGINT REFERENCES msg_conversations(id) ON DELETE CASCADE,
  user_id BIGINT REFERENCES usr_users(id) ON DELETE CASCADE,
  joined_at TIMESTAMP DEFAULT NOW(),
  last_read_at TIMESTAMP,
  PRIMARY KEY (conversation_id, user_id)
);

CREATE TABLE msg_messages (
  id BIGSERIAL PRIMARY KEY,
  conversation_id BIGINT REFERENCES msg_conversations(id) ON DELETE CASCADE,
  sender_id BIGINT REFERENCES usr_users(id) ON DELETE SET NULL,
  content TEXT,
  media_file_id BIGINT REFERENCES fs_files(id) ON DELETE SET NULL,
  created_at TIMESTAMP DEFAULT NOW(),
  deleted_at TIMESTAMP
);
```

---

#### 3. **User Blocking**
```sql
CREATE TABLE usr_blocks (
  id BIGSERIAL PRIMARY KEY,
  blocker_id BIGINT REFERENCES usr_users(id) ON DELETE CASCADE,
  blocked_id BIGINT REFERENCES usr_users(id) ON DELETE CASCADE,
  created_at TIMESTAMP DEFAULT NOW(),
  UNIQUE (blocker_id, blocked_id),
  CHECK (blocker_id != blocked_id)
);
```

**Enforcement**: Application prevents:
- Blocked users from seeing blocker's content
- Blocked users from following/messaging blocker
- Blocker from seeing blocked user's content

---

#### 4. **Notifications**
```sql
CREATE TABLE usr_notifications (
  id BIGSERIAL PRIMARY KEY,
  user_id BIGINT REFERENCES usr_users(id) ON DELETE CASCADE,
  actor_id BIGINT REFERENCES usr_users(id) ON DELETE CASCADE,
  notification_type VARCHAR(50) NOT NULL, -- 'follow', 'like', 'comment', 'mention'
  resource_type VARCHAR(50), -- 'post', 'reel', 'comment'
  resource_id BIGINT,
  is_read BOOLEAN DEFAULT FALSE,
  created_at TIMESTAMP DEFAULT NOW()
);

CREATE INDEX idx_usr_notifications_user ON usr_notifications(user_id, is_read, created_at DESC);
```

---

#### 5. **Content Sharing (Cross-platform)**
```sql
CREATE TABLE cnt_shares (
  id BIGSERIAL PRIMARY KEY,
  user_id BIGINT REFERENCES usr_users(id) ON DELETE CASCADE,
  content_type VARCHAR(20) CHECK (content_type IN ('post', 'reel', 'story')),
  content_id BIGINT NOT NULL,
  platform VARCHAR(50), -- 'facebook', 'twitter', 'whatsapp', 'internal'
  created_at TIMESTAMP DEFAULT NOW()
);
```

---

#### 6. **Mentions in Content**
```sql
CREATE TABLE cnt_post_mentions (
  id BIGSERIAL PRIMARY KEY,
  post_id BIGINT REFERENCES cnt_posts(id) ON DELETE CASCADE,
  mentioned_user_id BIGINT REFERENCES usr_users(id) ON DELETE CASCADE,
  created_at TIMESTAMP DEFAULT NOW(),
  UNIQUE (post_id, mentioned_user_id)
);

CREATE TABLE cnt_reel_mentions (
  id BIGSERIAL PRIMARY KEY,
  reel_id BIGINT REFERENCES cnt_reels(id) ON DELETE CASCADE,
  mentioned_user_id BIGINT REFERENCES usr_users(id) ON DELETE CASCADE,
  created_at TIMESTAMP DEFAULT NOW(),
  UNIQUE (reel_id, mentioned_user_id)
);
```

---

#### 7. **Story Replies**
```sql
CREATE TABLE cnt_story_replies (
  id BIGSERIAL PRIMARY KEY,
  story_id BIGINT REFERENCES cnt_stories(id) ON DELETE CASCADE,
  user_id BIGINT REFERENCES usr_users(id) ON DELETE CASCADE,
  content TEXT NOT NULL,
  created_at TIMESTAMP DEFAULT NOW()
);
```

**Note**: Replies could also trigger direct message conversation.

---

#### 8. **Hashtag Following**
```sql
CREATE TABLE usr_followed_hashtags (
  id BIGSERIAL PRIMARY KEY,
  user_id BIGINT REFERENCES usr_users(id) ON DELETE CASCADE,
  hashtag_id BIGINT REFERENCES cnt_hashtags(id) ON DELETE CASCADE,
  created_at TIMESTAMP DEFAULT NOW(),
  UNIQUE (user_id, hashtag_id)
);
```

**Use Case**: Users can follow hashtags to see posts/reels in their feed.

---

## Appendix: Quick Reference

### Table Count: **25 Tables**

| Prefix | Count | Tables |
|--------|-------|--------|
| `sys_` | 4 | parameters, permissions, role_permissions, audit_log |
| `usr_` | 2 | users, follows |
| `crt_` | 3 | creators, companies, creator_status_history |
| `fs_` | 1 | files |
| `cnt_` | 15 | posts, post_media, post_likes, post_comments, post_hashtags, reels, reel_likes, reel_comments, reel_hashtags, stories, story_views, hashtags |

### New Features Summary

| Feature | Tables Added | Key Capabilities |
|---------|--------------|------------------|
| **Followers/Following** | `usr_follows` | Follow users, follower counts, social graph |
| **Hashtags** | `cnt_hashtags`, `cnt_post_hashtags`, `cnt_reel_hashtags` | Tag content, discover by hashtag, trending tags |
| **Stories** | `cnt_stories`, `cnt_story_views` | 24-hour ephemeral content, view tracking |

### Index Count: **68 Indexes**

### Foreign Key Count: **38 Relationships**

### Supported Content Types
- **Posts**: Text + multiple images/videos
- **Reels**: Short-form video (max 3 minutes)
- **Stories**: Single image/video (expires in 24 hours)

### Default Configuration
- Max file size: 500 MB
- Max post media: 10 files
- Max reel duration: 180 seconds
- Story duration: 24 hours
- Max hashtags per post: 30

---

## Review Checklist

### Core Features
- [x] Does the schema satisfy all three user roles?
- [x] Can companies register as content creators?
- [x] Can posts contain text only, media only, or both?
- [x] Can posts have multiple media files?
- [x] Are reels properly structured with video + thumbnail?
- [x] Can users like and comment on content?
- [x] Is the file storage system provider-agnostic?
- [x] Can admins moderate creators and content?
- [x] Are all admin actions logged?

### New Features
- [x] Can users follow/unfollow other users?
- [x] Are follower/following counts maintained?
- [x] Can posts and reels be tagged with hashtags?
- [x] Can users discover content by hashtag?
- [x] Are trending hashtags trackable?
- [x] Can creators post 24-hour stories?
- [x] Can creators see who viewed their stories?
- [x] Do stories automatically expire?

### Technical
- [x] Is the schema scalable for high traffic?
- [x] Are foreign keys and constraints properly enforced?
- [x] Is soft delete implemented where needed?
- [x] Are indexes optimized for common queries?
- [x] Are denormalized counts auto-updated via triggers?

---

**Document Version:** 2.0  
**Last Updated:** January 29, 2026  
**Changes from v1.0**: Added followers/following, hashtags, and stories features  
**Prepared For:** Architecture Review