Mohammed Rami Benhamida
  • NEW!
    NEW!  Connect Ideas Across Notes
    Save time and share insights. With Paragraph Citation, you can quote others’ work with source info built in. If someone cites your note, you’ll see a card showing where it’s used—bringing notes closer together.
    Got it
      • Create new note
      • Create a note from template
        • Sharing URL Link copied
        • /edit
        • View mode
          • Edit mode
          • View mode
          • Book mode
          • Slide mode
          Edit mode View mode Book mode Slide mode
        • Customize slides
        • Note Permission
        • Read
          • Only me
          • Signed-in users
          • Everyone
          Only me Signed-in users Everyone
        • Write
          • Only me
          • Signed-in users
          • Everyone
          Only me Signed-in users Everyone
        • Engagement control Commenting, Suggest edit, Emoji Reply
      • Invite by email
        Invitee

        This note has no invitees

      • Publish Note

        Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note No publishing access yet

        Your note will be visible on your profile and discoverable by anyone.
        Your note is now live.
        This note is visible on your profile and discoverable online.
        Everyone on the web can find and read all notes of this public team.

        Your account was recently created. Publishing will be available soon, allowing you to share notes on your public page and in search results.

        Your team account was recently created. Publishing will be available soon, allowing you to share notes on your public page and in search results.

        Explore these features while you wait
        Complete general settings
        Bookmark and like published notes
        Write a few more notes
        Complete general settings
        Write a few more notes
        See published notes
        Unpublish note
        Please check the box to agree to the Community Guidelines.
        View profile
      • Commenting
        Permission
        Disabled Forbidden Owners Signed-in users Everyone
      • Enable
      • Permission
        • Forbidden
        • Owners
        • Signed-in users
        • Everyone
      • Suggest edit
        Permission
        Disabled Forbidden Owners Signed-in users Everyone
      • Enable
      • Permission
        • Forbidden
        • Owners
        • Signed-in users
      • Emoji Reply
      • Enable
      • Versions and GitHub Sync
      • Note settings
      • Note Insights New
      • Engagement control
      • Make a copy
      • Transfer ownership
      • Delete this note
      • Save as template
      • Insert from template
      • Import from
        • Dropbox
        • Google Drive
        • Gist
        • Clipboard
      • Export to
        • Dropbox
        • Google Drive
        • Gist
      • Download
        • Markdown
        • HTML
        • Raw HTML
    Menu Note settings Note Insights Versions and GitHub Sync Sharing URL Create Help
    Create Create new note Create a note from template
    Menu
    Options
    Engagement control Make a copy Transfer ownership Delete this note
    Import from
    Dropbox Google Drive Gist Clipboard
    Export to
    Dropbox Google Drive Gist
    Download
    Markdown HTML Raw HTML
    Back
    Sharing URL Link copied
    /edit
    View mode
    • Edit mode
    • View mode
    • Book mode
    • Slide mode
    Edit mode View mode Book mode Slide mode
    Customize slides
    Note Permission
    Read
    Only me
    • Only me
    • Signed-in users
    • Everyone
    Only me Signed-in users Everyone
    Write
    Only me
    • Only me
    • Signed-in users
    • Everyone
    Only me Signed-in users Everyone
    Engagement control Commenting, Suggest edit, Emoji Reply
  • Invite by email
    Invitee

    This note has no invitees

  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note No publishing access yet

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.

    Your account was recently created. Publishing will be available soon, allowing you to share notes on your public page and in search results.

    Your team account was recently created. Publishing will be available soon, allowing you to share notes on your public page and in search results.

    Explore these features while you wait
    Complete general settings
    Bookmark and like published notes
    Write a few more notes
    Complete general settings
    Write a few more notes
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       Owned this note    Owned this note      
    Published Linked with GitHub
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    # 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

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password
    or
    Sign in via Google Sign in via Facebook Sign in via X(Twitter) Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    By signing in, you agree to our terms of service.

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully