Real-world SQL designs from the apps you use every day. Explore ERD diagrams, study the CREATE TABLE statements, and compare them with your own schema.
Users, tweets, follows, likes, retweets, and bookmarks. Designed for 500M+ tweets per day.
Riders, drivers, vehicles, trips, payments, ratings, and real-time event tracking.
Short links, click analytics, custom aliases, and geo/device tracking like bit.ly.
Products, categories, carts, orders, payments, reviews, and inventory management.
Posts, authors, categories, tags, comments, media library, and newsletter subscriptions.
Workspaces, channels, messages, threads, reactions, roles, and permissions.
A simplified but production-grade schema for a Twitter-like microblogging platform. Handles users, tweets, follow relationships, likes, and bookmarks. Designed for read-heavy workloads with partial indexes for non-deleted content.
is_deleted flag and partial index — preserves reply chains and analyticsreply_to_tweet_id and retweet_of_tweet_idfollows table prevents duplicate relationshipsfollower_id != following_idCREATE TABLE users (
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
username VARCHAR(15) NOT NULL UNIQUE,
display_name VARCHAR(50) NOT NULL,
bio TEXT,
avatar_url VARCHAR(500),
email VARCHAR(255) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
is_verified BOOLEAN NOT NULL DEFAULT FALSE,
follower_count INT NOT NULL DEFAULT 0,
following_count INT NOT NULL DEFAULT 0,
tweet_count INT NOT NULL DEFAULT 0
);
CREATE TABLE tweets (
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
content VARCHAR(280) NOT NULL,
reply_to_tweet_id BIGINT REFERENCES tweets(id) ON DELETE SET NULL,
retweet_of_tweet_id BIGINT REFERENCES tweets(id) ON DELETE SET NULL,
media_urls TEXT[],
like_count INT NOT NULL DEFAULT 0,
retweet_count INT NOT NULL DEFAULT 0,
reply_count INT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
is_deleted BOOLEAN NOT NULL DEFAULT FALSE,
deleted_at TIMESTAMPTZ
);
CREATE TABLE follows (
follower_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
following_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (follower_id, following_id),
CONSTRAINT no_self_follow CHECK (follower_id != following_id)
);
CREATE TABLE likes (
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
tweet_id BIGINT NOT NULL REFERENCES tweets(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (user_id, tweet_id)
);
CREATE TABLE bookmarks (
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
tweet_id BIGINT NOT NULL REFERENCES tweets(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (user_id, tweet_id)
);
CREATE INDEX idx_tweets_user_created ON tweets(user_id, created_at DESC) WHERE is_deleted = FALSE;
CREATE INDEX idx_tweets_reply_to ON tweets(reply_to_tweet_id) WHERE reply_to_tweet_id IS NOT NULL;
CREATE INDEX idx_tweets_retweet_of ON tweets(retweet_of_tweet_id) WHERE retweet_of_tweet_id IS NOT NULL;
CREATE INDEX idx_follows_following ON follows(following_id, created_at DESC);
CREATE INDEX idx_likes_tweet ON likes(tweet_id, created_at DESC);
A ride-hailing platform schema covering riders, drivers, vehicles, trips, payments, and ratings. Built for 20M+ trips per day with immutable financial records and event-sourced trip tracking.
trip_events table captures every status change for debugging and disputesratings(trip_id, from_user_id) prevents duplicate ratingsCREATE TABLE users (
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
phone VARCHAR(20) NOT NULL UNIQUE,
email VARCHAR(255) UNIQUE,
full_name VARCHAR(100) NOT NULL,
password_hash VARCHAR(255),
avatar_url VARCHAR(500),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
is_active BOOLEAN NOT NULL DEFAULT TRUE,
default_payment_method_id BIGINT
);
CREATE TABLE drivers (
user_id BIGINT PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
license_number VARCHAR(50) NOT NULL UNIQUE,
license_expiry DATE NOT NULL,
background_check_status VARCHAR(20) NOT NULL DEFAULT 'pending',
onboarding_status VARCHAR(20) NOT NULL DEFAULT 'started',
rating DECIMAL(2,1) NOT NULL DEFAULT 5.0,
total_trips INT NOT NULL DEFAULT 0,
lifetime_earnings DECIMAL(12,2) NOT NULL DEFAULT 0.00,
is_online BOOLEAN NOT NULL DEFAULT FALSE,
current_vehicle_id BIGINT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE vehicles (
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
driver_id BIGINT NOT NULL REFERENCES drivers(user_id) ON DELETE CASCADE,
make VARCHAR(50) NOT NULL,
model VARCHAR(50) NOT NULL,
year INT NOT NULL,
color VARCHAR(30) NOT NULL,
license_plate VARCHAR(20) NOT NULL UNIQUE,
vehicle_type VARCHAR(20) NOT NULL DEFAULT 'economy',
seat_capacity INT NOT NULL DEFAULT 4,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE vehicle_documents (
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
vehicle_id BIGINT NOT NULL REFERENCES vehicles(id) ON DELETE CASCADE,
document_type VARCHAR(30) NOT NULL,
document_url VARCHAR(500) NOT NULL,
verification_status VARCHAR(20) NOT NULL DEFAULT 'pending',
uploaded_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
verified_at TIMESTAMPTZ,
expires_at DATE
);
CREATE TABLE trips (
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
rider_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
driver_id BIGINT REFERENCES drivers(user_id) ON DELETE SET NULL,
vehicle_id BIGINT REFERENCES vehicles(id) ON DELETE SET NULL,
status VARCHAR(20) NOT NULL DEFAULT 'requested',
pickup_lat DECIMAL(10,8) NOT NULL,
pickup_lng DECIMAL(11,8) NOT NULL,
pickup_address VARCHAR(300) NOT NULL,
dropoff_lat DECIMAL(10,8) NOT NULL,
dropoff_lng DECIMAL(11,8) NOT NULL,
dropoff_address VARCHAR(300) NOT NULL,
requested_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
accepted_at TIMESTAMPTZ,
started_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ,
cancelled_at TIMESTAMPTZ,
cancelled_by VARCHAR(10),
cancellation_reason TEXT,
distance_km DECIMAL(8,2),
duration_minutes INT,
base_fare DECIMAL(8,2) NOT NULL,
distance_fare DECIMAL(8,2) NOT NULL DEFAULT 0.00,
time_fare DECIMAL(8,2) NOT NULL DEFAULT 0.00,
surge_multiplier DECIMAL(3,2) NOT NULL DEFAULT 1.00,
promo_discount DECIMAL(8,2) NOT NULL DEFAULT 0.00,
total_fare DECIMAL(8,2) NOT NULL,
currency VARCHAR(3) NOT NULL DEFAULT 'USD',
payment_status VARCHAR(20) NOT NULL DEFAULT 'pending'
);
CREATE TABLE trip_events (
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
trip_id BIGINT NOT NULL REFERENCES trips(id) ON DELETE CASCADE,
event_type VARCHAR(30) NOT NULL,
lat DECIMAL(10,8),
lng DECIMAL(11,8),
metadata JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE payments (
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
trip_id BIGINT NOT NULL REFERENCES trips(id) ON DELETE CASCADE,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
amount DECIMAL(10,2) NOT NULL,
currency VARCHAR(3) NOT NULL DEFAULT 'USD',
payment_method VARCHAR(20) NOT NULL,
payment_status VARCHAR(20) NOT NULL DEFAULT 'pending',
transaction_id VARCHAR(100) UNIQUE,
processed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE ratings (
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
trip_id BIGINT NOT NULL REFERENCES trips(id) ON DELETE CASCADE,
from_user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
to_user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
rating INT NOT NULL CHECK (rating BETWEEN 1 AND 5),
comment TEXT,
category VARCHAR(20),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(trip_id, from_user_id)
);
CREATE INDEX idx_trips_rider ON trips(rider_id, requested_at DESC);
CREATE INDEX idx_trips_driver ON trips(driver_id, requested_at DESC);
CREATE INDEX idx_trips_status ON trips(status, requested_at DESC) WHERE status IN ('requested','accepted','started');
CREATE INDEX idx_trip_events_trip ON trip_events(trip_id, created_at DESC);
CREATE INDEX idx_payments_trip ON payments(trip_id);
CREATE INDEX idx_ratings_to_user ON ratings(to_user_id, created_at DESC);
A bit.ly-style URL shortening service supporting custom aliases, click analytics, expiration, and user accounts. Optimized for 100:1 read-to-write ratio with aggregate tables for fast analytics.
CREATE TABLE users (
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
email VARCHAR(255) UNIQUE,
password_hash VARCHAR(255),
display_name VARCHAR(50),
api_key VARCHAR(64) UNIQUE,
tier VARCHAR(20) NOT NULL DEFAULT 'free',
monthly_link_limit INT NOT NULL DEFAULT 100,
custom_domain VARCHAR(255),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
last_login_at TIMESTAMPTZ
);
CREATE TABLE short_links (
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
user_id BIGINT REFERENCES users(id) ON DELETE SET NULL,
short_code VARCHAR(30) NOT NULL UNIQUE,
original_url TEXT NOT NULL,
is_custom_alias BOOLEAN NOT NULL DEFAULT FALSE,
title VARCHAR(200),
description TEXT,
status VARCHAR(20) NOT NULL DEFAULT 'active',
expires_at TIMESTAMPTZ,
password_hash VARCHAR(255),
total_clicks INT NOT NULL DEFAULT 0,
unique_clicks INT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
CONSTRAINT valid_short_code CHECK (short_code ~ '^[a-zA-Z0-9_-]+$'),
CONSTRAINT valid_alias_length CHECK (
is_custom_alias = FALSE OR LENGTH(short_code) BETWEEN 3 AND 30
)
);
CREATE TABLE link_tags (
link_id BIGINT NOT NULL REFERENCES short_links(id) ON DELETE CASCADE,
tag VARCHAR(30) NOT NULL,
PRIMARY KEY (link_id, tag)
);
CREATE TABLE click_events (
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
link_id BIGINT NOT NULL REFERENCES short_links(id) ON DELETE CASCADE,
clicked_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
ip_hash VARCHAR(64) NOT NULL,
user_agent TEXT,
referrer TEXT,
country_code CHAR(2),
city VARCHAR(50),
device_type VARCHAR(20),
browser VARCHAR(30),
os VARCHAR(30),
language VARCHAR(10)
);
CREATE TABLE click_aggregates (
link_id BIGINT NOT NULL REFERENCES short_links(id) ON DELETE CASCADE,
date DATE NOT NULL,
total_clicks INT NOT NULL DEFAULT 0,
unique_clicks INT NOT NULL DEFAULT 0,
country_breakdown JSONB,
device_breakdown JSONB,
referrer_breakdown JSONB,
PRIMARY KEY (link_id, date)
);
CREATE TABLE link_rules (
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
link_id BIGINT NOT NULL REFERENCES short_links(id) ON DELETE CASCADE,
rule_type VARCHAR(30) NOT NULL,
condition_value VARCHAR(255) NOT NULL,
redirect_url TEXT,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
priority INT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_short_links_code ON short_links(short_code) WHERE deleted_at IS NULL;
CREATE INDEX idx_short_links_user ON short_links(user_id, created_at DESC);
CREATE INDEX idx_click_events_link ON click_events(link_id, clicked_at DESC);
CREATE INDEX idx_click_events_ip ON click_events(link_id, ip_hash, clicked_at);
CREATE INDEX idx_click_aggregates_date ON click_aggregates(date);
A production e-commerce schema covering users, products, categories, shopping carts, orders, payments, reviews, and inventory. Supports multi-currency, coupon codes, and verified purchase reviews.
parent_idCREATE TABLE users (
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
email VARCHAR(255) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
full_name VARCHAR(100) NOT NULL,
phone VARCHAR(20),
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE addresses (
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
type VARCHAR(20) NOT NULL DEFAULT 'shipping',
street VARCHAR(255) NOT NULL,
city VARCHAR(100) NOT NULL,
state VARCHAR(100),
postal_code VARCHAR(20) NOT NULL,
country VARCHAR(2) NOT NULL DEFAULT 'US',
is_default BOOLEAN NOT NULL DEFAULT FALSE
);
CREATE TABLE categories (
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
parent_id BIGINT REFERENCES categories(id) ON DELETE SET NULL,
name VARCHAR(100) NOT NULL,
slug VARCHAR(100) NOT NULL UNIQUE,
description TEXT,
image_url VARCHAR(500),
sort_order INT NOT NULL DEFAULT 0
);
CREATE TABLE products (
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
sku VARCHAR(50) NOT NULL UNIQUE,
name VARCHAR(255) NOT NULL,
slug VARCHAR(255) NOT NULL UNIQUE,
description TEXT,
short_description TEXT,
base_price DECIMAL(12,2) NOT NULL,
compare_at_price DECIMAL(12,2),
cost_price DECIMAL(12,2),
quantity_in_stock INT NOT NULL DEFAULT 0,
weight_kg DECIMAL(8,3),
status VARCHAR(20) NOT NULL DEFAULT 'draft',
is_featured BOOLEAN NOT NULL DEFAULT FALSE,
category_id BIGINT REFERENCES categories(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE product_images (
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
product_id BIGINT NOT NULL REFERENCES products(id) ON DELETE CASCADE,
image_url VARCHAR(500) NOT NULL,
alt_text VARCHAR(255),
sort_order INT NOT NULL DEFAULT 0,
is_primary BOOLEAN NOT NULL DEFAULT FALSE
);
CREATE TABLE carts (
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
user_id BIGINT REFERENCES users(id) ON DELETE CASCADE,
session_id VARCHAR(100),
status VARCHAR(20) NOT NULL DEFAULT 'active',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT cart_owner CHECK (user_id IS NOT NULL OR session_id IS NOT NULL)
);
CREATE TABLE cart_items (
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
cart_id BIGINT NOT NULL REFERENCES carts(id) ON DELETE CASCADE,
product_id BIGINT NOT NULL REFERENCES products(id) ON DELETE CASCADE,
quantity INT NOT NULL DEFAULT 1 CHECK (quantity > 0),
unit_price DECIMAL(12,2) NOT NULL,
added_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(cart_id, product_id)
);
CREATE TABLE orders (
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
order_number VARCHAR(50) NOT NULL UNIQUE,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
shipping_address_id BIGINT NOT NULL REFERENCES addresses(id),
billing_address_id BIGINT NOT NULL REFERENCES addresses(id),
status VARCHAR(20) NOT NULL DEFAULT 'pending',
payment_status VARCHAR(20) NOT NULL DEFAULT 'pending',
fulfillment_status VARCHAR(20) NOT NULL DEFAULT 'unfulfilled',
subtotal DECIMAL(12,2) NOT NULL,
tax_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00,
shipping_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00,
discount_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00,
total_amount DECIMAL(12,2) NOT NULL,
currency VARCHAR(3) NOT NULL DEFAULT 'USD',
notes TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE order_items (
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
order_id BIGINT NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
product_id BIGINT NOT NULL REFERENCES products(id),
product_name VARCHAR(255) NOT NULL,
product_sku VARCHAR(50) NOT NULL,
quantity INT NOT NULL CHECK (quantity > 0),
unit_price DECIMAL(12,2) NOT NULL,
total_price DECIMAL(12,2) NOT NULL
);
CREATE TABLE payments (
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
order_id BIGINT NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
amount DECIMAL(12,2) NOT NULL,
currency VARCHAR(3) NOT NULL DEFAULT 'USD',
provider VARCHAR(30) NOT NULL,
provider_transaction_id VARCHAR(255),
status VARCHAR(20) NOT NULL DEFAULT 'pending',
failure_reason TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE reviews (
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
product_id BIGINT NOT NULL REFERENCES products(id) ON DELETE CASCADE,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
order_id BIGINT REFERENCES orders(id) ON DELETE SET NULL,
rating INT NOT NULL CHECK (rating BETWEEN 1 AND 5),
title VARCHAR(255),
body TEXT,
is_verified_purchase BOOLEAN NOT NULL DEFAULT FALSE,
helpful_count INT NOT NULL DEFAULT 0,
status VARCHAR(20) NOT NULL DEFAULT 'approved',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(product_id, user_id, order_id)
);
CREATE INDEX idx_products_category ON products(category_id, status) WHERE status = 'active';
CREATE INDEX idx_products_featured ON products(is_featured, created_at DESC) WHERE status = 'active';
CREATE INDEX idx_orders_user ON orders(user_id, created_at DESC);
CREATE INDEX idx_orders_status ON orders(status, created_at DESC);
CREATE INDEX idx_reviews_product ON reviews(product_id, created_at DESC) WHERE status = 'approved';
CREATE INDEX idx_cart_items_cart ON cart_items(cart_id);
CREATE INDEX idx_addresses_user ON addresses(user_id, type);
A modern content management system schema inspired by Ghost and WordPress. Handles posts, authors, categories, tags, nested comments, media library, and newsletter subscriptions.
parent_idpost_categories has is_primary flag for main categorypost_media junction table — enables media reusemeta_title, meta_description) for headless CMS flexibilityCREATE TABLE users (
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
email VARCHAR(255) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
display_name VARCHAR(100) NOT NULL,
bio TEXT,
avatar_url VARCHAR(500),
role VARCHAR(20) NOT NULL DEFAULT 'author',
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE posts (
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
author_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
title VARCHAR(255) NOT NULL,
slug VARCHAR(255) NOT NULL UNIQUE,
excerpt TEXT,
content TEXT NOT NULL,
featured_image_url VARCHAR(500),
status VARCHAR(20) NOT NULL DEFAULT 'draft',
visibility VARCHAR(20) NOT NULL DEFAULT 'public',
published_at TIMESTAMPTZ,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
meta_title VARCHAR(255),
meta_description TEXT,
reading_time_minutes INT,
view_count INT NOT NULL DEFAULT 0
);
CREATE TABLE categories (
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
name VARCHAR(100) NOT NULL,
slug VARCHAR(100) NOT NULL UNIQUE,
description TEXT,
parent_id BIGINT REFERENCES categories(id) ON DELETE SET NULL,
sort_order INT NOT NULL DEFAULT 0
);
CREATE TABLE post_categories (
post_id BIGINT NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
category_id BIGINT NOT NULL REFERENCES categories(id) ON DELETE CASCADE,
is_primary BOOLEAN NOT NULL DEFAULT FALSE,
PRIMARY KEY (post_id, category_id)
);
CREATE TABLE tags (
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
name VARCHAR(50) NOT NULL UNIQUE,
slug VARCHAR(50) NOT NULL UNIQUE,
description TEXT
);
CREATE TABLE post_tags (
post_id BIGINT NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
tag_id BIGINT NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
PRIMARY KEY (post_id, tag_id)
);
CREATE TABLE comments (
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
post_id BIGINT NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
parent_id BIGINT REFERENCES comments(id) ON DELETE CASCADE,
author_name VARCHAR(100) NOT NULL,
author_email VARCHAR(255) NOT NULL,
author_url VARCHAR(500),
content TEXT NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'pending',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE media (
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
filename VARCHAR(255) NOT NULL,
original_name VARCHAR(255) NOT NULL,
mime_type VARCHAR(100) NOT NULL,
file_size_bytes INT NOT NULL,
url VARCHAR(500) NOT NULL,
alt_text VARCHAR(255),
uploaded_by BIGINT REFERENCES users(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE post_media (
post_id BIGINT NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
media_id BIGINT NOT NULL REFERENCES media(id) ON DELETE CASCADE,
sort_order INT NOT NULL DEFAULT 0,
PRIMARY KEY (post_id, media_id)
);
CREATE TABLE newsletters (
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
name VARCHAR(100) NOT NULL,
slug VARCHAR(100) NOT NULL UNIQUE,
description TEXT,
status VARCHAR(20) NOT NULL DEFAULT 'active'
);
CREATE TABLE subscriptions (
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
email VARCHAR(255) NOT NULL,
newsletter_id BIGINT NOT NULL REFERENCES newsletters(id) ON DELETE CASCADE,
status VARCHAR(20) NOT NULL DEFAULT 'subscribed',
subscribed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
unsubscribed_at TIMESTAMPTZ,
UNIQUE(email, newsletter_id)
);
CREATE INDEX idx_posts_author ON posts(author_id, published_at DESC) WHERE status = 'published';
CREATE INDEX idx_posts_slug ON posts(slug) WHERE status = 'published';
CREATE INDEX idx_comments_post ON comments(post_id, created_at DESC) WHERE status = 'approved';
CREATE INDEX idx_post_categories_primary ON post_categories(category_id) WHERE is_primary = TRUE;
CREATE INDEX idx_subscriptions_newsletter ON subscriptions(newsletter_id, status);
A real-time chat platform schema inspired by Discord and Slack. Covers workspaces, channels, messages, threads, reactions, roles, permissions, and invite links.
parent_idchannel_members — supports private channelsCREATE TABLE users (
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
username VARCHAR(32) NOT NULL UNIQUE,
display_name VARCHAR(100),
email VARCHAR(255) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
avatar_url VARCHAR(500),
status VARCHAR(20) NOT NULL DEFAULT 'offline',
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE workspaces (
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
name VARCHAR(100) NOT NULL,
slug VARCHAR(100) NOT NULL UNIQUE,
description TEXT,
icon_url VARCHAR(500),
owner_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
is_public BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE workspace_members (
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
workspace_id BIGINT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
role VARCHAR(20) NOT NULL DEFAULT 'member',
joined_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(workspace_id, user_id)
);
CREATE TABLE channels (
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
workspace_id BIGINT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
parent_id BIGINT REFERENCES channels(id) ON DELETE CASCADE,
name VARCHAR(100) NOT NULL,
type VARCHAR(20) NOT NULL DEFAULT 'text',
topic TEXT,
is_private BOOLEAN NOT NULL DEFAULT FALSE,
sort_order INT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(workspace_id, name)
);
CREATE TABLE channel_members (
channel_id BIGINT NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
added_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
added_by BIGINT REFERENCES users(id) ON DELETE SET NULL,
PRIMARY KEY (channel_id, user_id)
);
CREATE TABLE messages (
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
channel_id BIGINT NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
author_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
parent_id BIGINT REFERENCES messages(id) ON DELETE CASCADE,
content TEXT NOT NULL,
is_edited BOOLEAN NOT NULL DEFAULT FALSE,
edited_at TIMESTAMPTZ,
is_pinned BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE message_reactions (
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
message_id BIGINT NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
emoji VARCHAR(50) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(message_id, user_id, emoji)
);
CREATE TABLE attachments (
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
message_id BIGINT REFERENCES messages(id) ON DELETE CASCADE,
filename VARCHAR(255) NOT NULL,
file_size_bytes INT NOT NULL,
mime_type VARCHAR(100) NOT NULL,
url VARCHAR(500) NOT NULL,
width INT,
height INT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE roles (
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
workspace_id BIGINT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
name VARCHAR(50) NOT NULL,
color VARCHAR(7) DEFAULT '#99AAB5',
permissions BIGINT NOT NULL DEFAULT 0,
is_default BOOLEAN NOT NULL DEFAULT FALSE,
sort_order INT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE workspace_invites (
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
workspace_id BIGINT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
inviter_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
email VARCHAR(255),
code VARCHAR(32) NOT NULL UNIQUE,
max_uses INT,
uses_count INT NOT NULL DEFAULT 0,
expires_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_messages_channel_created ON messages(channel_id, created_at DESC);
CREATE INDEX idx_messages_parent ON messages(parent_id) WHERE parent_id IS NOT NULL;
CREATE INDEX idx_workspace_members_user ON workspace_members(user_id);
CREATE INDEX idx_channel_members_channel ON channel_members(channel_id);
CREATE INDEX idx_channels_workspace ON channels(workspace_id, sort_order);
Paste two SQL schemas and get a visual diff + migration script in seconds. No signup, no install, no data leaves your browser.
Try SchemaLens Free →54+ free developer tools. Built in public over 165 days.