| 1 | -- Klonkt — initial schema
|
|---|
| 2 | -- Forked from a v9 PHP, file-based CMS → SQLite/Node
|
|---|
| 3 | -- Complete schema with the v9 features (Forum, Audio, Themes) + v10 CRDT
|
|---|
| 4 |
|
|---|
| 5 | -- ==================== USERS ====================
|
|---|
| 6 | CREATE TABLE IF NOT EXISTS users (
|
|---|
| 7 | id TEXT PRIMARY KEY,
|
|---|
| 8 | username TEXT UNIQUE NOT NULL,
|
|---|
| 9 | email TEXT UNIQUE NOT NULL,
|
|---|
| 10 | password_hash TEXT NOT NULL,
|
|---|
| 11 | role TEXT DEFAULT 'member', -- member, admin, god
|
|---|
| 12 | avatar_url TEXT,
|
|---|
| 13 | bio TEXT,
|
|---|
| 14 | theme TEXT DEFAULT 'dark', -- dark, light
|
|---|
| 15 | palette TEXT DEFAULT 'sage', -- sage, paper, ocean, forest, stone, midnight, sunset, cream
|
|---|
| 16 | created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|---|
| 17 | updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|---|
| 18 | );
|
|---|
| 19 |
|
|---|
| 20 | -- ==================== SITES ====================
|
|---|
| 21 | CREATE TABLE IF NOT EXISTS sites (
|
|---|
| 22 | id TEXT PRIMARY KEY,
|
|---|
| 23 | slug TEXT UNIQUE NOT NULL,
|
|---|
| 24 | title TEXT NOT NULL,
|
|---|
| 25 | description TEXT,
|
|---|
| 26 | tagline TEXT,
|
|---|
| 27 | owner_id TEXT NOT NULL,
|
|---|
| 28 | parent_site_id TEXT,
|
|---|
| 29 |
|
|---|
| 30 | -- v9 config fields
|
|---|
| 31 | language TEXT DEFAULT 'nl',
|
|---|
| 32 | author TEXT,
|
|---|
| 33 | palette TEXT DEFAULT 'sage',
|
|---|
| 34 | accent TEXT DEFAULT '#c2410c',
|
|---|
| 35 | theme_override TEXT,
|
|---|
| 36 |
|
|---|
| 37 | -- Social/SEO
|
|---|
| 38 | social_title TEXT,
|
|---|
| 39 | social_description TEXT,
|
|---|
| 40 | social_image TEXT,
|
|---|
| 41 | og_image_default TEXT,
|
|---|
| 42 | default_cover TEXT,
|
|---|
| 43 | default_description TEXT,
|
|---|
| 44 | canonical TEXT,
|
|---|
| 45 |
|
|---|
| 46 | -- Verification metas
|
|---|
| 47 | google_verification TEXT,
|
|---|
| 48 | bing_verification TEXT,
|
|---|
| 49 | pinterest_verification TEXT,
|
|---|
| 50 | yandex_verification TEXT,
|
|---|
| 51 | og_locale TEXT,
|
|---|
| 52 | facebook_app_id TEXT,
|
|---|
| 53 |
|
|---|
| 54 | -- Customization
|
|---|
| 55 | custom_css TEXT,
|
|---|
| 56 | custom_head_html TEXT,
|
|---|
| 57 | custom_foot_html TEXT,
|
|---|
| 58 | title_template TEXT DEFAULT '{title} — {site}',
|
|---|
| 59 |
|
|---|
| 60 | -- Settings
|
|---|
| 61 | is_public INTEGER DEFAULT 1,
|
|---|
| 62 | closed_circle_mode INTEGER DEFAULT 0,
|
|---|
| 63 | robots_index INTEGER DEFAULT 1,
|
|---|
| 64 | require_login_to_comment INTEGER DEFAULT 1,
|
|---|
| 65 | enable_audio_player INTEGER DEFAULT 1,
|
|---|
| 66 | profile_photo TEXT,
|
|---|
| 67 |
|
|---|
| 68 | origin_server TEXT DEFAULT 'local',
|
|---|
| 69 | created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|---|
| 70 | updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|---|
| 71 | FOREIGN KEY (owner_id) REFERENCES users(id),
|
|---|
| 72 | FOREIGN KEY (parent_site_id) REFERENCES sites(id)
|
|---|
| 73 | );
|
|---|
| 74 |
|
|---|
| 75 | -- ==================== SITE MEMBERS ====================
|
|---|
| 76 | CREATE TABLE IF NOT EXISTS site_members (
|
|---|
| 77 | site_id TEXT NOT NULL,
|
|---|
| 78 | user_id TEXT NOT NULL,
|
|---|
| 79 | role TEXT DEFAULT 'member',
|
|---|
| 80 | PRIMARY KEY (site_id, user_id),
|
|---|
| 81 | FOREIGN KEY (site_id) REFERENCES sites(id),
|
|---|
| 82 | FOREIGN KEY (user_id) REFERENCES users(id)
|
|---|
| 83 | );
|
|---|
| 84 |
|
|---|
| 85 | -- ==================== POSTS ====================
|
|---|
| 86 | CREATE TABLE IF NOT EXISTS posts (
|
|---|
| 87 | id TEXT PRIMARY KEY,
|
|---|
| 88 | site_id TEXT NOT NULL,
|
|---|
| 89 | slug TEXT NOT NULL,
|
|---|
| 90 | author_id TEXT NOT NULL,
|
|---|
| 91 | title TEXT,
|
|---|
| 92 | content TEXT,
|
|---|
| 93 | excerpt TEXT,
|
|---|
| 94 | status TEXT DEFAULT 'draft',
|
|---|
| 95 | cover_image_url TEXT,
|
|---|
| 96 | pinned INTEGER DEFAULT 0,
|
|---|
| 97 | type TEXT DEFAULT 'post',
|
|---|
| 98 | tags TEXT,
|
|---|
| 99 | published_at DATETIME,
|
|---|
| 100 | created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|---|
| 101 | updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|---|
| 102 | yjs_binary BLOB,
|
|---|
| 103 | origin_server TEXT DEFAULT 'local',
|
|---|
| 104 | FOREIGN KEY (site_id) REFERENCES sites(id),
|
|---|
| 105 | FOREIGN KEY (author_id) REFERENCES users(id),
|
|---|
| 106 | UNIQUE (site_id, slug)
|
|---|
| 107 | );
|
|---|
| 108 |
|
|---|
| 109 | -- ==================== COMMENTS (FORUM) ====================
|
|---|
| 110 | CREATE TABLE IF NOT EXISTS comments (
|
|---|
| 111 | id TEXT PRIMARY KEY,
|
|---|
| 112 | post_id TEXT NOT NULL,
|
|---|
| 113 | author_id TEXT NOT NULL,
|
|---|
| 114 | parent_comment_id TEXT,
|
|---|
| 115 | content TEXT NOT NULL,
|
|---|
| 116 | status TEXT DEFAULT 'pending',
|
|---|
| 117 | yjs_binary BLOB,
|
|---|
| 118 | created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|---|
| 119 | updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|---|
| 120 | FOREIGN KEY (post_id) REFERENCES posts(id),
|
|---|
| 121 | FOREIGN KEY (author_id) REFERENCES users(id),
|
|---|
| 122 | FOREIGN KEY (parent_comment_id) REFERENCES comments(id)
|
|---|
| 123 | );
|
|---|
| 124 |
|
|---|
| 125 | -- ==================== PRUTTER (DM SYSTEM) ====================
|
|---|
| 126 | CREATE TABLE IF NOT EXISTS conversations (
|
|---|
| 127 | id TEXT PRIMARY KEY,
|
|---|
| 128 | user_a_id TEXT NOT NULL,
|
|---|
| 129 | user_b_id TEXT NOT NULL,
|
|---|
| 130 | site_id TEXT,
|
|---|
| 131 | last_message_at DATETIME,
|
|---|
| 132 | created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|---|
| 133 | FOREIGN KEY (user_a_id) REFERENCES users(id),
|
|---|
| 134 | FOREIGN KEY (user_b_id) REFERENCES users(id),
|
|---|
| 135 | FOREIGN KEY (site_id) REFERENCES sites(id),
|
|---|
| 136 | UNIQUE (user_a_id, user_b_id, site_id)
|
|---|
| 137 | );
|
|---|
| 138 |
|
|---|
| 139 | CREATE TABLE IF NOT EXISTS messages (
|
|---|
| 140 | id TEXT PRIMARY KEY,
|
|---|
| 141 | conversation_id TEXT NOT NULL,
|
|---|
| 142 | author_id TEXT NOT NULL,
|
|---|
| 143 | content TEXT NOT NULL,
|
|---|
| 144 | read_at DATETIME,
|
|---|
| 145 | created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|---|
| 146 | FOREIGN KEY (conversation_id) REFERENCES conversations(id),
|
|---|
| 147 | FOREIGN KEY (author_id) REFERENCES users(id)
|
|---|
| 148 | );
|
|---|
| 149 |
|
|---|
| 150 | -- ==================== MEDIA ====================
|
|---|
| 151 | CREATE TABLE IF NOT EXISTS media (
|
|---|
| 152 | id TEXT PRIMARY KEY,
|
|---|
| 153 | site_id TEXT NOT NULL,
|
|---|
| 154 | filename TEXT NOT NULL,
|
|---|
| 155 | mime_type TEXT NOT NULL,
|
|---|
| 156 | size INTEGER NOT NULL,
|
|---|
| 157 | storage_path TEXT NOT NULL,
|
|---|
| 158 | created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|---|
| 159 | FOREIGN KEY (site_id) REFERENCES sites(id)
|
|---|
| 160 | );
|
|---|
| 161 |
|
|---|
| 162 | -- ==================== AUDIO TRACKS ====================
|
|---|
| 163 | CREATE TABLE IF NOT EXISTS audio_tracks (
|
|---|
| 164 | id TEXT PRIMARY KEY,
|
|---|
| 165 | site_id TEXT NOT NULL,
|
|---|
| 166 | title TEXT NOT NULL,
|
|---|
| 167 | artist TEXT,
|
|---|
| 168 | duration INTEGER,
|
|---|
| 169 | media_id TEXT,
|
|---|
| 170 | position INTEGER DEFAULT 0,
|
|---|
| 171 | created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|---|
| 172 | FOREIGN KEY (site_id) REFERENCES sites(id),
|
|---|
| 173 | FOREIGN KEY (media_id) REFERENCES media(id)
|
|---|
| 174 | );
|
|---|
| 175 |
|
|---|
| 176 | -- ==================== PLAYLISTS ====================
|
|---|
| 177 | -- First-class playlist entity (from v9). Posts reference playlists by id;
|
|---|
| 178 | -- editing a playlist propagates to every post that embeds it via [[playlist:id]].
|
|---|
| 179 | -- `kind` distinguishes display style: 'album' (numbered list) vs 'playlist' (per-track thumbnails).
|
|---|
| 180 | CREATE TABLE IF NOT EXISTS playlists (
|
|---|
| 181 | id TEXT PRIMARY KEY, -- slug-style id, e.g. "ai-covers"
|
|---|
| 182 | site_id TEXT NOT NULL,
|
|---|
| 183 | title TEXT NOT NULL,
|
|---|
| 184 | artist TEXT,
|
|---|
| 185 | year INTEGER,
|
|---|
| 186 | cover_url TEXT,
|
|---|
| 187 | kind TEXT DEFAULT 'album', -- 'album' | 'playlist'
|
|---|
| 188 | created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|---|
| 189 | updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|---|
| 190 | FOREIGN KEY (site_id) REFERENCES sites(id)
|
|---|
| 191 | );
|
|---|
| 192 |
|
|---|
| 193 | -- Junction table — preserves track ordering via `position`.
|
|---|
| 194 | -- Composite PK prevents the same track being listed twice in the same playlist
|
|---|
| 195 | -- but allows the same track to appear in many playlists.
|
|---|
| 196 | CREATE TABLE IF NOT EXISTS playlist_tracks (
|
|---|
| 197 | playlist_id TEXT NOT NULL,
|
|---|
| 198 | track_id TEXT NOT NULL,
|
|---|
| 199 | position INTEGER NOT NULL DEFAULT 0,
|
|---|
| 200 | PRIMARY KEY (playlist_id, track_id),
|
|---|
| 201 | FOREIGN KEY (playlist_id) REFERENCES playlists(id) ON DELETE CASCADE,
|
|---|
| 202 | FOREIGN KEY (track_id) REFERENCES audio_tracks(id) ON DELETE CASCADE
|
|---|
| 203 | );
|
|---|
| 204 | CREATE INDEX IF NOT EXISTS idx_playlist_tracks_pos
|
|---|
| 205 | ON playlist_tracks(playlist_id, position);
|
|---|
| 206 |
|
|---|
| 207 | -- ==================== NOTIFICATIONS ====================
|
|---|
| 208 | CREATE TABLE IF NOT EXISTS notifications (
|
|---|
| 209 | id TEXT PRIMARY KEY,
|
|---|
| 210 | user_id TEXT NOT NULL,
|
|---|
| 211 | type TEXT,
|
|---|
| 212 | title TEXT,
|
|---|
| 213 | message TEXT,
|
|---|
| 214 | link TEXT,
|
|---|
| 215 | read_at DATETIME,
|
|---|
| 216 | created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|---|
| 217 | FOREIGN KEY (user_id) REFERENCES users(id)
|
|---|
| 218 | );
|
|---|
| 219 |
|
|---|
| 220 | -- ==================== SEARCH (FTS5) ====================
|
|---|
| 221 | CREATE VIRTUAL TABLE IF NOT EXISTS posts_fts USING fts5(
|
|---|
| 222 | content,
|
|---|
| 223 | title,
|
|---|
| 224 | author,
|
|---|
| 225 | post_id UNINDEXED
|
|---|
| 226 | );
|
|---|
| 227 |
|
|---|
| 228 | -- ==================== FEDERATION ====================
|
|---|
| 229 | CREATE TABLE IF NOT EXISTS federation_log (
|
|---|
| 230 | id TEXT PRIMARY KEY,
|
|---|
| 231 | entity_type TEXT,
|
|---|
| 232 | entity_id TEXT,
|
|---|
| 233 | origin_server TEXT,
|
|---|
| 234 | action TEXT,
|
|---|
| 235 | yjs_update BLOB,
|
|---|
| 236 | created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|---|
| 237 | );
|
|---|
| 238 |
|
|---|
| 239 | -- ==================== SESSIONS ====================
|
|---|
| 240 | CREATE TABLE IF NOT EXISTS sessions (
|
|---|
| 241 | sid TEXT PRIMARY KEY,
|
|---|
| 242 | data TEXT,
|
|---|
| 243 | expiresAt DATETIME
|
|---|
| 244 | );
|
|---|
| 245 |
|
|---|
| 246 | -- ==================== INDEXES ====================
|
|---|
| 247 | CREATE INDEX IF NOT EXISTS idx_posts_site ON posts(site_id);
|
|---|
| 248 | CREATE INDEX IF NOT EXISTS idx_posts_status ON posts(status);
|
|---|
| 249 | CREATE INDEX IF NOT EXISTS idx_comments_post ON comments(post_id);
|
|---|
| 250 | CREATE INDEX IF NOT EXISTS idx_messages_conv ON messages(conversation_id);
|
|---|
| 251 | CREATE INDEX IF NOT EXISTS idx_notifications_user ON notifications(user_id);
|
|---|