Last Updated: 2/11/2026
Database Schema
Complete reference for our PostgreSQL database structure.
Core Tables
users
The users table stores all user account information.
Schema Definition:
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
full_name VARCHAR(255),
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
last_login_at TIMESTAMP WITH TIME ZONE,
is_active BOOLEAN DEFAULT true,
CONSTRAINT email_format CHECK (email ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$')
);
-- Indexes for performance
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_created_at ON users(created_at);
CREATE INDEX idx_users_active ON users(is_active) WHERE is_active = true;Example Queries:
-- Create a new user
INSERT INTO users (email, password_hash, full_name)
VALUES ('user@example.com', '$2b$10$...', 'John Doe')
RETURNING id, email, created_at;
-- Find user by email
SELECT id, email, full_name, created_at, last_login_at
FROM users
WHERE email = 'user@example.com' AND is_active = true;
-- Update last login
UPDATE users
SET last_login_at = CURRENT_TIMESTAMP,
updated_at = CURRENT_TIMESTAMP
WHERE id = '123e4567-e89b-12d3-a456-426614174000';articles
The articles table stores all article content and metadata.
Schema Definition:
CREATE TABLE articles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
title VARCHAR(500) NOT NULL,
slug VARCHAR(500) NOT NULL UNIQUE,
content TEXT,
excerpt TEXT,
author_id UUID NOT NULL,
status VARCHAR(50) DEFAULT 'draft',
published_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
view_count INTEGER DEFAULT 0,
CONSTRAINT fk_author FOREIGN KEY (author_id)
REFERENCES users(id) ON DELETE CASCADE,
CONSTRAINT valid_status CHECK (status IN ('draft', 'published', 'archived'))
);
-- Indexes
CREATE INDEX idx_articles_author_id ON articles(author_id);
CREATE INDEX idx_articles_status ON articles(status);
CREATE INDEX idx_articles_published_at ON articles(published_at) WHERE status = 'published';
CREATE INDEX idx_articles_slug ON articles(slug);
CREATE INDEX idx_articles_full_text ON articles USING gin(to_tsvector('english', title || ' ' || content));Example Queries:
-- Create a new article
INSERT INTO articles (title, slug, content, author_id, status)
VALUES (
'Getting Started with PostgreSQL',
'getting-started-postgresql',
'PostgreSQL is a powerful open-source database...',
'123e4567-e89b-12d3-a456-426614174000',
'draft'
)
RETURNING id, title, created_at;
-- Publish an article
UPDATE articles
SET status = 'published',
published_at = CURRENT_TIMESTAMP,
updated_at = CURRENT_TIMESTAMP
WHERE id = '987fcdeb-51a2-43f7-9abc-123456789012';
-- Get published articles by author with user info
SELECT
a.id,
a.title,
a.slug,
a.excerpt,
a.published_at,
a.view_count,
u.full_name AS author_name,
u.email AS author_email
FROM articles a
INNER JOIN users u ON a.author_id = u.id
WHERE a.author_id = '123e4567-e89b-12d3-a456-426614174000'
AND a.status = 'published'
ORDER BY a.published_at DESC
LIMIT 10;
-- Full-text search
SELECT id, title, excerpt,
ts_rank(to_tsvector('english', title || ' ' || content),
plainto_tsquery('english', 'database schema')) AS rank
FROM articles
WHERE status = 'published'
AND to_tsvector('english', title || ' ' || content) @@ plainto_tsquery('english', 'database schema')
ORDER BY rank DESC
LIMIT 20;folders
The folders table implements a hierarchical folder structure using a self-referencing foreign key.
Schema Definition:
CREATE TABLE folders (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
parent_id UUID,
owner_id UUID NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
position INTEGER DEFAULT 0,
CONSTRAINT fk_parent FOREIGN KEY (parent_id)
REFERENCES folders(id) ON DELETE CASCADE,
CONSTRAINT fk_owner FOREIGN KEY (owner_id)
REFERENCES users(id) ON DELETE CASCADE,
CONSTRAINT no_self_reference CHECK (id != parent_id)
);
-- Indexes
CREATE INDEX idx_folders_parent_id ON folders(parent_id);
CREATE INDEX idx_folders_owner_id ON folders(owner_id);
CREATE INDEX idx_folders_position ON folders(position);Example Queries:
-- Create a root folder
INSERT INTO folders (name, owner_id, parent_id)
VALUES ('Documents', '123e4567-e89b-12d3-a456-426614174000', NULL)
RETURNING id, name, created_at;
-- Create a subfolder
INSERT INTO folders (name, owner_id, parent_id)
VALUES (
'Work Projects',
'123e4567-e89b-12d3-a456-426614174000',
'abc12345-6789-0def-ghij-klmnopqrstuv'
)
RETURNING id, name, parent_id;
-- Get folder hierarchy (recursive query)
WITH RECURSIVE folder_tree AS (
-- Base case: root folders
SELECT id, name, parent_id, owner_id, 0 AS depth,
ARRAY[name] AS path
FROM folders
WHERE parent_id IS NULL
AND owner_id = '123e4567-e89b-12d3-a456-426614174000'
UNION ALL
-- Recursive case: child folders
SELECT f.id, f.name, f.parent_id, f.owner_id, ft.depth + 1,
ft.path || f.name
FROM folders f
INNER JOIN folder_tree ft ON f.parent_id = ft.id
)
SELECT id, name, parent_id, depth,
array_to_string(path, ' > ') AS full_path
FROM folder_tree
ORDER BY path;
-- Move folder to new parent
UPDATE folders
SET parent_id = 'new-parent-uuid',
updated_at = CURRENT_TIMESTAMP
WHERE id = 'folder-to-move-uuid';
-- Get all descendants of a folder
WITH RECURSIVE descendants AS (
SELECT id, name, parent_id
FROM folders
WHERE id = 'parent-folder-uuid'
UNION ALL
SELECT f.id, f.name, f.parent_id
FROM folders f
INNER JOIN descendants d ON f.parent_id = d.id
)
SELECT * FROM descendants;users
- id (uuid, primary key)
- email (varchar, unique)
- createdat (timestamp)
articles
- id (uuid, primary key)
- title (varchar)
- content (text)
- authorid (uuid, foreign key)
- createdat (timestamp)
- updatedat (timestamp)
folders
- id (uuid, primary key)
- name (varchar)
- parentid (uuid, nullable)