Initial commit: The Collective Hub planning documentation

This commit is contained in:
2026-06-05 22:49:03 -04:00
commit 293415f349
29 changed files with 4551 additions and 0 deletions
+313
View File
@@ -0,0 +1,313 @@
# Database Planning Document
## Design Principles
- **siteId on every site-owned table.** Non-negotiable.
- **Prefer normalized tables over JSON columns** — except for theme/branding settings where flexibility is valuable.
- **Timestamps on every table** (`createdAt`, `updatedAt`).
- **Use UUIDs for primary keys** — avoids sequential ID enumeration and works well distributed.
- **Index `siteId` on every table that has it** — it's the most common query filter.
- **Soft deletes where appropriate** — prefer `deletedAt` over hard deletes for content that might be needed later.
---
## Tables
### `sites`
The core tenant table. One row per deployed site.
| Column | Type | Notes |
|--------|------|-------|
| `id` | `uuid` (PK) | |
| `slug` | `text` (UNIQUE, NOT NULL) | Matches `SITE_SLUG` env var |
| `name` | `text` (NOT NULL) | Display name |
| `isActive` | `boolean` (default true) | Soft disable a site |
| `createdAt` | `timestamptz` | |
| `updatedAt` | `timestamptz` | |
**Indexes:** UNIQUE on `slug`.
---
### `users`
Auth users. Created automatically on first Discord login.
| Column | Type | Notes |
|--------|------|-------|
| `id` | `uuid` (PK) | |
| `discordId` | `text` (UNIQUE, NOT NULL) | Discord user ID |
| `discordUsername` | `text` | Display name from Discord |
| `discordAvatar` | `text` | Avatar hash/URL from Discord |
| `email` | `text` | If available from Discord scope |
| `createdAt` | `timestamptz` | |
| `updatedAt` | `timestamptz` | |
| `lastLoginAt` | `timestamptz` | |
**Indexes:** UNIQUE on `discordId`.
**Note:** Better Auth manages its own session/account tables. The `users` table here is the application-level user profile. Better Auth tables are separate and managed by the library.
---
### `memberships`
Links users to sites with a role. A user can be a member of multiple sites with different roles.
| Column | Type | Notes |
|--------|------|-------|
| `id` | `uuid` (PK) | |
| `siteId` | `uuid``sites.id` (NOT NULL) | |
| `userId` | `uuid``users.id` (NOT NULL) | |
| `role` | `enum('owner', 'admin', 'editor')` | See role definitions below |
| `createdAt` | `timestamptz` | |
| `updatedAt` | `timestamptz` | |
**Indexes:** UNIQUE on `(siteId, userId)`. INDEX on `siteId`. INDEX on `userId`.
**Role Definitions (V1):**
- **owner** — Full control. Bootstrap via `OWNER_DISCORD_ID`. Can manage admins. One per site initially.
- **admin** — Can edit all site settings and content. Cannot delete the site or manage the owner.
- **editor** — Can edit content (events, pages) but not site settings or branding.
Future roles: `viewer`, `moderator` — not needed in V1.
---
### `siteSettings`
Key-value or JSON settings for a site. Two approaches are viable; recommendation below.
**Recommended approach: Single JSON column per site**
| Column | Type | Notes |
|--------|------|-------|
| `id` | `uuid` (PK) | |
| `siteId` | `uuid``sites.id` (UNIQUE, NOT NULL) | One settings row per site |
| `settings` | `jsonb` (NOT NULL, default `{}`) | All site settings as JSON |
| `createdAt` | `timestamptz` | |
| `updatedAt` | `timestamptz` | |
**Indexes:** UNIQUE on `siteId`.
The `settings` JSON would contain:
```json
{
"branding": {
"siteName": "Bad Movies Theater",
"tagline": "Terrible movies, great company",
"logoCdnKey": "sites/bad-movies-theater/logo.webp",
"backgroundCdnKey": "sites/bad-movies-theater/background.webp",
"faviconCdnKey": null
},
"theme": {
"preset": "dark",
"accentColor": "#e63946",
"backgroundColor": "#1a1a2e",
"textColor": "#eaeaea"
},
"homepage": {
"heroTitle": "Welcome to Bad Movies Theater",
"heroSubtitle": "We watch bad movies so you don't have to",
"aboutText": "A community of bad movie enthusiasts...",
"primaryButtonText": "Join us on Discord",
"primaryButtonLink": "https://discord.gg/example",
"showNextEvent": true,
"showSchedule": true
},
"layout": {
"preset": "standard"
}
}
```
**Why JSON for settings?** Settings are read as a batch, rarely queried individually, and benefit from schema flexibility. If you add a new setting, no migration is needed.
**Alternative (not recommended for V1):** Key-value table with `siteId`, `key`, `value` columns. More queryable but more complex for nested settings.
---
### `assets`
Records of uploaded or referenced media files stored in the CDN.
| Column | Type | Notes |
|--------|------|-------|
| `id` | `uuid` (PK) | |
| `siteId` | `uuid``sites.id` (NOT NULL) | |
| `uploadedByUserId` | `uuid``users.id` | Nullable for system assets |
| `type` | `text` (NOT NULL) | e.g., `image`, `document` |
| `filename` | `text` (NOT NULL) | Original filename |
| `mimeType` | `text` | e.g., `image/webp` |
| `size` | `integer` | Bytes |
| `cdnKey` | `text` (NOT NULL) | Path within CDN bucket |
| `altText` | `text` | Accessibility description |
| `createdAt` | `timestamptz` | |
| `updatedAt` | `timestamptz` | |
**Indexes:** INDEX on `siteId`. INDEX on `cdnKey`.
**V1 approach:** Assets table may start as a manual-reference table (paste CDN URLs) before automatic upload flow is built. This is fine — the table structure supports both.
---
### `navLinks`
Custom navigation links for a site's header/footer.
| Column | Type | Notes |
|--------|------|-------|
| `id` | `uuid` (PK) | |
| `siteId` | `uuid``sites.id` (NOT NULL) | |
| `label` | `text` (NOT NULL) | Display text |
| `url` | `text` (NOT NULL) | Link target |
| `position` | `text` (default `'header'`) | `header` or `footer` |
| `sortOrder` | `integer` (default 0) | Ordering within position |
| `isExternal` | `boolean` (default true) | Open in new tab? |
| `createdAt` | `timestamptz` | |
| `updatedAt` | `timestamptz` | |
**Indexes:** INDEX on `(siteId, position, sortOrder)`.
---
### `socialLinks`
Social media / external platform links.
| Column | Type | Notes |
|--------|------|-------|
| `id` | `uuid` (PK) | |
| `siteId` | `uuid``sites.id` (NOT NULL) | |
| `platform` | `text` (NOT NULL) | e.g., `discord`, `twitter`, `youtube`, `twitch` |
| `label` | `text` | Display label, defaults to platform name |
| `url` | `text` (NOT NULL) | |
| `icon` | `text` | Icon identifier if custom |
| `sortOrder` | `integer` (default 0) | |
| `createdAt` | `timestamptz` | |
| `updatedAt` | `timestamptz` | |
**Indexes:** INDEX on `(siteId, sortOrder)`.
---
### `events`
Scheduled events / watch parties / screenings.
| Column | Type | Notes |
|--------|------|-------|
| `id` | `uuid` (PK) | |
| `siteId` | `uuid``sites.id` (NOT NULL) | |
| `title` | `text` (NOT NULL) | |
| `description` | `text` | |
| `eventType` | `text` (default `'screening'`) | `screening`, `watch_party`, `meetup`, `other` |
| `startTime` | `timestamptz` (NOT NULL) | |
| `endTime` | `timestamptz` | Optional, for duration |
| `timezone` | `text` (default `'America/New_York'`) | IANA timezone |
| `location` | `text` | e.g., "Discord Stage", "VR Chat", "Online" |
| `externalLink` | `text` | Link to event page, stream, etc. |
| `imageCdnKey` | `text` | Optional event image |
| `isPublished` | `boolean` (default false) | Draft mode |
| `isRecurring` | `boolean` (default false) | Placeholder for future recurring support |
| `createdAt` | `timestamptz` | |
| `updatedAt` | `timestamptz` | |
**Indexes:** INDEX on `(siteId, startTime)`. INDEX on `(siteId, isPublished)`.
**Recurring events:** V1 treats all events as one-off. The `isRecurring` flag is a placeholder. A future phase could add a `recurrenceRule` field (JSON or a separate table) for repeat patterns. Don't build recurring logic in V1.
---
### `homepageSections` (Optional V1)
If the homepage needs more structure than a single text block, sections allow ordered content blocks.
| Column | Type | Notes |
|--------|------|-------|
| `id` | `uuid` (PK) | |
| `siteId` | `uuid``sites.id` (NOT NULL) | |
| `type` | `text` (NOT NULL) | `hero`, `about`, `events`, `links`, `custom` |
| `title` | `text` | Section heading |
| `content` | `text` | Markdown or plain text |
| `settings` | `jsonb` | Section-specific config |
| `sortOrder` | `integer` (default 0) | |
| `isVisible` | `boolean` (default true) | |
| `createdAt` | `timestamptz` | |
| `updatedAt` | `timestamptz` | |
**Indexes:** INDEX on `(siteId, sortOrder)`.
**V1 recommendation:** Start without this table. Use the `homepage` JSON in `siteSettings` for the first version. Add sections in Phase 2+ if sites need more flexible page building.
---
## Entity Relationship Summary
```mermaid
erDiagram
sites ||--o{ memberships : "has"
sites ||--|| siteSettings : "has"
sites ||--o{ assets : "owns"
sites ||--o{ navLinks : "has"
sites ||--o{ socialLinks : "has"
sites ||--o{ events : "hosts"
users ||--o{ memberships : "has"
users ||--o{ assets : "uploads"
sites {
uuid id PK
text slug UK
text name
boolean isActive
}
users {
uuid id PK
text discordId UK
text discordUsername
text discordAvatar
}
memberships {
uuid id PK
uuid siteId FK
uuid userId FK
enum role
}
siteSettings {
uuid id PK
uuid siteId FK UK
jsonb settings
}
events {
uuid id PK
uuid siteId FK
text title
timestamptz startTime
boolean isPublished
}
```
---
## Migration Strategy
1. **All migrations run manually** — not on app startup. David runs `drizzle-kit migrate` locally or via a primary deployment.
2. **Additive changes only in production** — new columns, new tables. Avoid renames or destructive changes without a plan.
3. **JSON columns for settings** reduce migration frequency for feature additions.
4. **Seed data** — a seed script can populate the initial site record for a new deployment, or David creates the site row manually.
## What This Schema Intentionally Avoids
- **No `accounts` or `sessions` tables** — Better Auth manages those.
- **No `pages` table for V1** — homepage content lives in `siteSettings.homepage` JSON.
- **No `reviews`, `comments`, `posts` tables** — future phases.
- **No `featureFlags` table** — use env vars or settings JSON for now.
- **No `domains` table** — single `SITE_SLUG` resolution in V1.
- **No `auditLog` table** — nice to have later, not needed for V1.
- **No `invitations` table** — owner adds admins directly in V1, no invite flow.