Initial commit: The Collective Hub planning documentation
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
# Project Brief — The Collective Hub
|
||||
|
||||
## What This Project Is
|
||||
|
||||
A reusable SvelteKit website template system that lets you launch simple, branded landing pages for online theater hosts, watch-party communities, bad movie groups, VR theater communities, Discord communities, and similar groups — all from one shared codebase.
|
||||
|
||||
One codebase. Multiple deployed websites. One database. One CDN.
|
||||
|
||||
## Who It's For
|
||||
|
||||
- **Primary user — David (system maintainer):** Deploys new sites, maintains the shared codebase, pushes updates that improve all sites at once. Has super admin access across all sites.
|
||||
- **Site owners/admins:** Theater hosts, community organizers, watch-party runners. They log in via Discord, customize their site's branding and content through a simple admin panel.
|
||||
- **Site visitors:** Community members and newcomers who want to see what the community is about, when events happen, and how to join.
|
||||
|
||||
## What Problem It Solves
|
||||
|
||||
Running multiple small community/theater websites usually means one of:
|
||||
- A separate codebase per site (maintenance nightmare)
|
||||
- A heavyweight SaaS platform (overkill for simple landing pages)
|
||||
- A generic Linktree-style page (not customizable enough, doesn't feel owned)
|
||||
|
||||
The Collective Hub gives each community its own branded site without duplicating code or infrastructure.
|
||||
|
||||
## What the First Version Should Do
|
||||
|
||||
- Display a public homepage for a theater/community host
|
||||
- Let the site owner log in via Discord
|
||||
- Let the owner customize basic branding (name, logo, colors, tagline)
|
||||
- Let the owner edit homepage content (intro text, button, links)
|
||||
- Show basic event/schedule information
|
||||
- Support multiple sites from one codebase using `SITE_SLUG`
|
||||
- Share one Postgres database across all sites (data scoped by `siteId`)
|
||||
- Share one CDN/storage bucket across all sites
|
||||
- Full image upload flow with automatic webp conversion and optimization
|
||||
- Asset library for browsing and managing uploaded files
|
||||
- Super admin access for David across all sites via `SUPER_ADMIN_DISCORD_IDS`
|
||||
|
||||
## What It Should NOT Do Yet (Out of Scope for V1)
|
||||
|
||||
- Complex page builder or drag-and-drop editor
|
||||
- User registration beyond admin login
|
||||
- Comments, reviews, or community posts
|
||||
- AI features, recommendations, or semantic search
|
||||
- Per-site custom CSS or advanced theming
|
||||
- Custom domain management UI (manual DNS/Coolify config is fine)
|
||||
- Multi-owner invite system (single owner bootstrapped via env var)
|
||||
- Recurring event schedules with complex timezone logic
|
||||
- Super admin dashboard UI (super admin access exists, but the dedicated dashboard comes in Phase 4)
|
||||
|
||||
## Long-Term Vision
|
||||
|
||||
The system grows into a practical multi-tenant platform where:
|
||||
- Any community host can have a full-featured site
|
||||
- Owners can invite admins and editors
|
||||
- Sites support events, schedules, content collections, and community features
|
||||
- David has a full super admin dashboard for cross-site management
|
||||
- The system remains maintainable by one person (David)
|
||||
- Updates roll out to all sites from one codebase
|
||||
- The architecture supports scaling to many sites without degradation
|
||||
|
||||
But version 1 intentionally starts small. A working, useful product beats an ambitious unfinished one.
|
||||
@@ -0,0 +1,273 @@
|
||||
# Technical Architecture Plan
|
||||
|
||||
## Stack Summary
|
||||
|
||||
| Layer | Choice | Notes |
|
||||
|-------|--------|-------|
|
||||
| Framework | SvelteKit | File-based routing, server-side rendering, API routes |
|
||||
| Language | TypeScript | Strict mode recommended |
|
||||
| Database | PostgreSQL | Single shared instance for all sites |
|
||||
| ORM | Drizzle ORM | Type-safe, lightweight, SQL-first |
|
||||
| Auth | Better Auth | With Discord OAuth provider |
|
||||
| Deployment | Coolify | Multiple deployments from one Git repo |
|
||||
| CDN/Storage | Bunny CDN or S3-compatible | Single bucket, site-scoped paths |
|
||||
| Containerization | Docker | Dockerfile in repo, built by Coolify |
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
subgraph Internet
|
||||
V[Visitors]
|
||||
O[Site Owners/Admins]
|
||||
end
|
||||
|
||||
subgraph Coolify
|
||||
D1[Deployment: bad-movies-theater]
|
||||
D2[Deployment: garbage-day]
|
||||
D3[Deployment: future-site-N]
|
||||
end
|
||||
|
||||
subgraph Shared Infrastructure
|
||||
DB[(PostgreSQL Database)]
|
||||
CDN[(CDN / Object Storage)]
|
||||
end
|
||||
|
||||
subgraph Auth
|
||||
DA[Discord OAuth]
|
||||
end
|
||||
|
||||
V --> D1
|
||||
V --> D2
|
||||
V --> D3
|
||||
O --> D1
|
||||
O --> D2
|
||||
O --> D3
|
||||
|
||||
D1 --> DB
|
||||
D2 --> DB
|
||||
D3 --> DB
|
||||
|
||||
D1 --> CDN
|
||||
D2 --> CDN
|
||||
D3 --> CDN
|
||||
|
||||
D1 --> DA
|
||||
D2 --> DA
|
||||
D3 --> DA
|
||||
```
|
||||
|
||||
Each Coolify deployment runs the same Docker image from the same Git repo. The only difference between deployments is their environment variables — specifically `SITE_SLUG`.
|
||||
|
||||
## Site Resolution Flow
|
||||
|
||||
Every request follows this path:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
R[HTTP Request] --> MW[Site Resolver Middleware]
|
||||
MW --> SLUG[Read SITE_SLUG from env]
|
||||
SLUG --> DBQ[Query: SELECT FROM sites WHERE slug = ?]
|
||||
DBQ --> CTX[Attach site + settings to locals]
|
||||
CTX --> APP[App renders with site context]
|
||||
```
|
||||
|
||||
1. Request hits the SvelteKit server
|
||||
2. A hook or middleware reads `SITE_SLUG` from environment variables
|
||||
3. The site record (and its settings) is loaded from the database
|
||||
4. Site context is attached to `locals` for the lifetime of the request
|
||||
5. All downstream code (pages, API routes, auth checks) uses this site context
|
||||
|
||||
## Data Scoping Rule
|
||||
|
||||
**Every site-owned record MUST include a `siteId` column.**
|
||||
|
||||
This applies to: settings, pages, events, assets, nav links, social links, memberships, and any future content types.
|
||||
|
||||
Queries always filter by `siteId`:
|
||||
|
||||
```sql
|
||||
SELECT * FROM events WHERE siteId = $currentSiteId ORDER BY startTime ASC;
|
||||
```
|
||||
|
||||
This rule means:
|
||||
- No cross-site data leaks
|
||||
- The database is logically multi-tenant
|
||||
- Migrating to a single-deployment/multi-domain model later requires no schema changes
|
||||
|
||||
## Directory Structure (Proposed)
|
||||
|
||||
```text
|
||||
/
|
||||
├── src/
|
||||
│ ├── app.d.ts # App types, locals augmentation
|
||||
│ ├── app.html # HTML shell
|
||||
│ ├── hooks.server.ts # Site resolver, auth handling
|
||||
│ ├── lib/
|
||||
│ │ ├── server/
|
||||
│ │ │ ├── db/
|
||||
│ │ │ │ ├── index.ts # Drizzle + postgres connection
|
||||
│ │ │ │ ├── schema.ts # All table definitions
|
||||
│ │ │ │ └── seed.ts # Optional seed data
|
||||
│ │ │ ├── auth.ts # Better Auth configuration
|
||||
│ │ │ ├── site-resolver.ts # Site loading by SITE_SLUG
|
||||
│ │ │ └── cdn.ts # CDN URL helpers
|
||||
│ │ └── shared/
|
||||
│ │ └── types.ts # Shared TypeScript types
|
||||
│ ├── routes/
|
||||
│ │ ├── +layout.server.ts # Root layout, loads site for all pages
|
||||
│ │ ├── +page.server.ts # Public homepage data loading
|
||||
│ │ ├── +page.svelte # Public homepage
|
||||
│ │ ├── login/
|
||||
│ │ │ └── +page.svelte # Login page (Discord redirect)
|
||||
│ │ ├── admin/
|
||||
│ │ │ ├── +layout.server.ts # Admin auth guard
|
||||
│ │ │ ├── +layout.svelte # Admin shell/nav
|
||||
│ │ │ ├── +page.svelte # Admin dashboard
|
||||
│ │ │ ├── settings/
|
||||
│ │ │ │ └── +page.svelte # Site settings editor
|
||||
│ │ │ ├── branding/
|
||||
│ │ │ │ └── +page.svelte # Logo, colors, theme
|
||||
│ │ │ ├── homepage/
|
||||
│ │ │ │ └── +page.svelte # Homepage content editor
|
||||
│ │ │ ├── links/
|
||||
│ │ │ │ └── +page.svelte # Nav and social links
|
||||
│ │ │ └── events/
|
||||
│ │ │ └── +page.svelte # Events manager
|
||||
│ │ └── api/
|
||||
│ │ └── auth/
|
||||
│ │ └── [...betterAuth] # Better Auth API routes
|
||||
│ └── styles/
|
||||
│ └── app.css # Global styles, CSS custom properties
|
||||
├── static/
|
||||
│ └── favicon.png
|
||||
├── Dockerfile
|
||||
├── docker-compose.yml # Optional, for local dev
|
||||
├── drizzle.config.ts
|
||||
├── package.json
|
||||
├── svelte.config.js
|
||||
├── tsconfig.json
|
||||
└── vite.config.ts
|
||||
```
|
||||
|
||||
## Auth Flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant U as User
|
||||
participant S as SvelteKit App
|
||||
participant DA as Discord OAuth
|
||||
participant DB as PostgreSQL
|
||||
|
||||
U->>S: Visit /login
|
||||
S->>DA: Redirect to Discord OAuth
|
||||
DA->>U: Authorize
|
||||
U->>S: Callback with OAuth code
|
||||
S->>DA: Exchange code for token + user info
|
||||
DA-->>S: Discord user (id, username, avatar)
|
||||
S->>DB: Upsert user record
|
||||
S->>DB: Check if Discord ID matches OWNER_DISCORD_ID
|
||||
S->>DB: Check if Discord ID matches SUPER_ADMIN_DISCORD_IDS
|
||||
DB-->>S: Match found → assign appropriate role for current site
|
||||
S->>S: Create session
|
||||
S-->>U: Redirect to /admin (or show access denied)
|
||||
```
|
||||
|
||||
### Ownership Bootstrap
|
||||
|
||||
On first login, the app compares the user's Discord ID against the `OWNER_DISCORD_ID` environment variable. If they match:
|
||||
1. A membership record is created (or confirmed) with role `owner` for the current site
|
||||
2. The env var acts as a bootstrap — once the owner exists in the database, the env var could theoretically be removed (though keeping it is fine)
|
||||
|
||||
Long-term, existing owners can add other admins/editors through the admin panel, and the database becomes the source of truth for all roles.
|
||||
|
||||
### Super Admin Access
|
||||
|
||||
The app also checks the user's Discord ID against `SUPER_ADMIN_DISCORD_IDS` (a comma-separated list). If a match is found:
|
||||
1. The user is granted cross-site admin access — they bypass site-scoped membership checks
|
||||
2. Super admins can access any site's admin panel regardless of `OWNER_DISCORD_ID`
|
||||
3. This is intended for David (system maintainer) to manage all sites
|
||||
4. Super admin access is checked on every request, not just at login
|
||||
|
||||
## Deployment Model
|
||||
|
||||
### Current: Multiple Coolify Deployments
|
||||
|
||||
```
|
||||
Git Repo (main branch)
|
||||
│
|
||||
├── Coolify Deployment "bad-movies-theater"
|
||||
│ └── Env: SITE_SLUG=bad-movies-theater
|
||||
│
|
||||
├── Coolify Deployment "garbage-day"
|
||||
│ └── Env: SITE_SLUG=garbage-day
|
||||
│
|
||||
└── Coolify Deployment "future-site"
|
||||
└── Env: SITE_SLUG=future-site
|
||||
```
|
||||
|
||||
Each deployment:
|
||||
- Points to the same Git repo + branch
|
||||
- Has its own set of environment variables
|
||||
- Connects to the same database
|
||||
- Uses the same CDN bucket
|
||||
- Is completely isolated at the container level
|
||||
|
||||
### Future: Single Deployment / Multi-Domain (Optional)
|
||||
|
||||
If desired later, the system could switch to a single deployment that resolves the site by domain name instead of `SITE_SLUG`:
|
||||
- Add a `domains` table or JSON field on `sites`
|
||||
- The site resolver checks the request's `Host` header
|
||||
- Falls back to `SITE_SLUG` for local dev
|
||||
|
||||
This is **not** needed for version 1 but the architecture supports it because all data is already scoped by `siteId`.
|
||||
|
||||
## CDN/Asset Architecture
|
||||
|
||||
```
|
||||
CDN Bucket: collective-hub
|
||||
├── sites/
|
||||
│ ├── bad-movies-theater/
|
||||
│ │ ├── logo.webp
|
||||
│ │ ├── background.webp
|
||||
│ │ └── events/
|
||||
│ │ └── movie-night-june.webp
|
||||
│ ├── garbage-day/
|
||||
│ │ ├── logo.webp
|
||||
│ │ └── background.webp
|
||||
│ └── future-site/
|
||||
│ └── logo.webp
|
||||
```
|
||||
|
||||
Key rules:
|
||||
- Database stores asset records with `cdnKey` (the path within the bucket)
|
||||
- Application constructs full CDN URLs using `CDN_BASE_URL` + `cdnKey`
|
||||
- Never hardcode full CDN URLs in the database
|
||||
- Assets are always scoped by `siteId` in the database
|
||||
- Path convention: `sites/{siteSlug}/{type}/{filename}`
|
||||
- All uploaded images are automatically converted to webp and optimized before storage
|
||||
|
||||
## Migration Safety
|
||||
|
||||
Multiple deployments sharing one database means migrations must be handled carefully:
|
||||
|
||||
- **Migrations run automatically on startup**, but only on the deployment with `RUN_MIGRATIONS=true`.
|
||||
- All other deployments (`RUN_MIGRATIONS=false`) skip migrations entirely.
|
||||
- **Exactly one deployment** must be designated the migration runner. Choose a stable, low-traffic deployment.
|
||||
- The migration runner must be deployed first when schema changes are included in a release.
|
||||
- Drizzle's migration tools handle idempotency — running the same migration twice is safe, but concurrent runs from multiple deployments must be avoided.
|
||||
- This is enforced by convention (the `RUN_MIGRATIONS` flag), not by a distributed lock. David must ensure only one deployment has the flag set to `true`.
|
||||
|
||||
## Key Architecture Decisions
|
||||
|
||||
| Decision | Choice | Rationale |
|
||||
|----------|--------|-----------|
|
||||
| Multi-deploy vs single-deploy | Multi-deploy first | Simpler initially, no domain-routing complexity |
|
||||
| siteId on every table | Yes | Multi-tenant from day one, no rewrite needed later |
|
||||
| JSON vs normalized tables | Prefer normalized; JSON for theme settings only | Queryable, type-safe, referential integrity |
|
||||
| CDN URLs in DB | Store keys only, not full URLs | CDN migration is trivial, no data changes needed |
|
||||
| Image processing | Auto webp conversion + optimization on upload | Consistent format, smaller files, better performance |
|
||||
| Auth library | Better Auth | First-class Discord support, SvelteKit integration |
|
||||
| Super admin | `SUPER_ADMIN_DISCORD_IDS` env var | Cross-site access for system maintainer |
|
||||
| Migration strategy | Automated, gated by `RUN_MIGRATIONS` flag | One deployment runs them; others skip |
|
||||
| ORM | Drizzle | Type-safe, lightweight, good Postgres support |
|
||||
@@ -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.
|
||||
@@ -0,0 +1,163 @@
|
||||
# Feature Roadmap
|
||||
|
||||
## How to Read This
|
||||
|
||||
Each phase builds on the previous one. Phases are ordered by dependency, not by importance.
|
||||
|
||||
**The rule:** Phase 1 must be complete and working before starting Phase 2. Later phases can be reordered based on need.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Foundation Minimum Viable Product
|
||||
|
||||
**Goal:** A working SvelteKit app that resolves a site by `SITE_SLUG`, shows a public homepage, lets the owner log in and edit settings, and supports image uploads with CDN storage.
|
||||
|
||||
### Deliverables
|
||||
|
||||
- [ ] SvelteKit project initialized with TypeScript
|
||||
- [ ] Drizzle ORM configured, connected to Postgres
|
||||
- [ ] Core database tables created: `sites`, `users`, `memberships`, `siteSettings`, `assets`
|
||||
- [ ] Better Auth integrated with Discord OAuth provider
|
||||
- [ ] Site resolver: reads `SITE_SLUG`, loads site + settings from DB, attaches to `locals`
|
||||
- [ ] Public homepage renders with site name and basic content
|
||||
- [ ] Login page: "Login with Discord" button
|
||||
- [ ] Owner bootstrap: `OWNER_DISCORD_ID` env var creates owner membership on first login
|
||||
- [ ] Super admin bootstrap: `SUPER_ADMIN_DISCORD_IDS` env var grants cross-site access
|
||||
- [ ] Admin auth guard: `/admin/*` routes redirect unauthenticated users to login
|
||||
- [ ] Basic admin dashboard page (placeholder with site name)
|
||||
- [ ] Admin settings page: edit site name and tagline (saved to `siteSettings` JSON)
|
||||
- [ ] CDN storage integration (Bunny CDN or S3-compatible)
|
||||
- [ ] Image upload endpoint with webp conversion and optimization
|
||||
- [ ] File validation: accepted types, max size
|
||||
- [ ] Asset records created in database on upload
|
||||
- [ ] Asset library page in admin: browse, search, copy CDN URL
|
||||
- [ ] Migration automation: primary deployment runs migrations on startup; others skip via `RUN_MIGRATIONS` env var
|
||||
|
||||
### What's NOT in Phase 1
|
||||
- No branding customization (logo, colors) — Phase 2
|
||||
- No homepage content editing beyond name/tagline — Phase 2
|
||||
- No events, nav links, social links — Phase 2 / Phase 4
|
||||
- No role management UI — Phase 5
|
||||
- No super admin dashboard UI — Phase 5
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Branding & Customization
|
||||
|
||||
**Goal:** Site owners can customize the look and feel of their site. The public site reflects branding settings. Asset upload is already available from Phase 1.
|
||||
|
||||
### Deliverables
|
||||
|
||||
- [ ] Admin branding page: select logo, background image, favicon from asset library
|
||||
- [ ] Admin theme page: preset selector (dark/light/custom), accent color, background color, text color
|
||||
- [ ] CSS custom properties generated from theme settings
|
||||
- [ ] Admin homepage editor: hero title, subtitle, about text, CTA button text/link
|
||||
- [ ] Public site renders all branding and homepage settings
|
||||
- [ ] Admin nav links manager: add, edit, reorder, delete header/footer links
|
||||
- [ ] Admin social links manager: add, edit, reorder, delete social platform links
|
||||
- [ ] Public site renders nav links and social links
|
||||
- [ ] Layout preset support (single configurable layout for V1)
|
||||
|
||||
### What's NOT in Phase 2
|
||||
- Multiple layout options (just one flexible layout)
|
||||
- Custom CSS fields
|
||||
- Per-page theming
|
||||
- Theme marketplace or sharing
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Events & Schedule
|
||||
|
||||
**Goal:** Sites can display upcoming events. Admins can manage events through the admin panel.
|
||||
|
||||
### Deliverables
|
||||
|
||||
- [ ] Events table created (if not already)
|
||||
- [ ] Admin events manager: create, edit, delete, publish/unpublish events
|
||||
- [ ] Event fields: title, description, type, start time, end time, timezone, location, external link, image
|
||||
- [ ] Public homepage: "Next Event" card (shows the next upcoming published event)
|
||||
- [ ] Public homepage: "Upcoming Events" list/schedule section
|
||||
- [ ] Event detail page (optional — can be a modal or external link for V1)
|
||||
- [ ] Timezone display handling (show event time in visitor's local time via JS)
|
||||
|
||||
### What's NOT in Phase 3
|
||||
- Recurring/repeating events
|
||||
- Calendar feed (iCal/RSS)
|
||||
- Event reminders or notifications
|
||||
- Attendee RSVPs
|
||||
- Integration with Discord events
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Super Admin Dashboard
|
||||
|
||||
**Goal:** David (system maintainer) can manage all sites from a central dashboard.
|
||||
|
||||
### Deliverables
|
||||
|
||||
- [ ] Super admin auth: `SUPER_ADMIN_DISCORD_IDS` env var bypasses site-scoped membership checks
|
||||
- [ ] Super admin dashboard: list all sites with status, quick links
|
||||
- [ ] Create new site flow: insert site row, generate setup instructions
|
||||
- [ ] View any site's settings, events, assets (read-only cross-site access)
|
||||
- [ ] Feature flag management across sites
|
||||
- [ ] Site deactivation/reactivation
|
||||
|
||||
### What's NOT in Phase 4
|
||||
- Full site provisioning automation (still manual Coolify setup)
|
||||
- Usage analytics or billing
|
||||
- Impersonation (login as site owner)
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Admin Improvements
|
||||
|
||||
**Goal:** Better admin experience, role management, and operational tooling.
|
||||
|
||||
### Deliverables
|
||||
|
||||
- [ ] Role management: owner can add/remove admins and editors
|
||||
- [ ] Admin list page showing all team members with roles
|
||||
- [ ] Preview mode: admins can preview unpublished changes
|
||||
- [ ] Improved admin dashboard with quick stats
|
||||
- [ ] Site cloning helper (manual or scripted) for creating new sites
|
||||
- [ ] Feature flags via settings JSON or env vars
|
||||
- [ ] Audit-like log of who changed what (basic)
|
||||
- [ ] Enhanced super admin dashboard: cross-site search, bulk operations
|
||||
|
||||
### What's NOT in Phase 5
|
||||
- Full audit trail with rollback
|
||||
- Granular permission system
|
||||
- Multi-owner support per site
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: Future (Optional, Not Planned in Detail)
|
||||
|
||||
These are ideas for later. Do not build any of these until Phases 1-5 are solid.
|
||||
|
||||
- **Community features:** reviews, comments, discussion posts
|
||||
- **Discord integration:** server widget, event sync, role sync
|
||||
- **Calendar feeds:** iCal export, Google Calendar integration
|
||||
- **AI tools:** content suggestions, event descriptions, semantic search (pgvector)
|
||||
- **Advanced theming:** multiple layout presets, custom CSS per site
|
||||
- **Analytics:** page views, event clicks, simple dashboard
|
||||
- **Email notifications:** event reminders, new content alerts
|
||||
- **Single-deployment model:** switch from multi-Coolify-deployment to one deployment resolving sites by domain
|
||||
- **Public API:** read-only API for events and site info
|
||||
|
||||
---
|
||||
|
||||
## Phase Dependency Diagram
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
P1[Phase 1: Foundation + Assets] --> P2[Phase 2: Branding & Customization]
|
||||
P1 --> P3[Phase 3: Events & Schedule]
|
||||
P2 --> P3
|
||||
P1 --> P4[Phase 4: Super Admin Dashboard]
|
||||
P3 --> P5[Phase 5: Admin Improvements]
|
||||
P4 --> P5
|
||||
P5 --> P6[Phase 6: Future Features]
|
||||
```
|
||||
|
||||
**Note:** Phase 1 now includes CDN + asset upload (previously Phase 3). Phase 4 (Super Admin Dashboard) can start as soon as Phase 1 is done — it's independent of branding and events. Phase 5 enhances both regular admin and super admin experiences.
|
||||
@@ -0,0 +1,192 @@
|
||||
# Environment Variables Document — The Collective Hub
|
||||
|
||||
## Overview
|
||||
|
||||
Environment variables are the only difference between deployments. Each Coolify deployment has its own set of env vars, but all deployments share the same database, CDN, and Discord OAuth app.
|
||||
|
||||
---
|
||||
|
||||
## Variable Reference
|
||||
|
||||
### Required for Every Deployment
|
||||
|
||||
| Variable | Example | Description | Shared or Per-Site? |
|
||||
|----------|---------|-------------|---------------------|
|
||||
| `SITE_SLUG` | `bad-movies-theater` | Identifies which site this deployment serves. Must match a `slug` in the `sites` table. | **Per-site** |
|
||||
| `PUBLIC_SITE_URL` | `https://badmovies.example.com` | The public URL of this deployment. Used for auth callbacks, canonical URLs. | **Per-site** |
|
||||
| `DATABASE_URL` | `postgresql://user:pass@host:5432/collective_hub` | Postgres connection string. | **Shared** (same for all) |
|
||||
| `BETTER_AUTH_SECRET` | (generated) | Secret key for Better Auth session signing. | **Shared** (same for all) |
|
||||
| `BETTER_AUTH_URL` | `https://badmovies.example.com` | Base URL for auth callbacks. Must match `PUBLIC_SITE_URL`. | **Per-site** |
|
||||
| `DISCORD_CLIENT_ID` | `123456789012345678` | Discord OAuth application client ID. | **Shared** |
|
||||
| `DISCORD_CLIENT_SECRET` | `abc123...` | Discord OAuth application client secret. | **Shared** |
|
||||
| `OWNER_DISCORD_ID` | `123456789012345678` | Discord user ID of the site owner. Used to bootstrap ownership on first login. | **Per-site** |
|
||||
|
||||
### Required for CDN (Phase 1)
|
||||
|
||||
| Variable | Example | Description | Shared or Per-Site? |
|
||||
|----------|---------|-------------|---------------------|
|
||||
| `CDN_BASE_URL` | `https://cdn.example.com` | Base URL for constructing CDN URLs. | **Shared** |
|
||||
| `CDN_STORAGE_ENDPOINT` | `https://ny.storage.bunnycdn.com` | Storage API endpoint. | **Shared** |
|
||||
| `CDN_ACCESS_KEY` | `abc123...` | Storage access key. | **Shared** |
|
||||
| `CDN_SECRET_KEY` | (S3 only) | Secret key for S3-compatible storage. | **Shared** |
|
||||
| `CDN_BUCKET` | `collective-hub` | Bucket or storage zone name. | **Shared** |
|
||||
| `CDN_REGION` | `us-east-1` | Region for S3-compatible storage. | **Shared** |
|
||||
|
||||
### Super Admin
|
||||
|
||||
| Variable | Example | Description | Shared or Per-Site? |
|
||||
|----------|---------|-------------|---------------------|
|
||||
| `SUPER_ADMIN_DISCORD_IDS` | `123456789,987654321` | Comma-separated Discord user IDs with cross-site super admin access. | **Shared** |
|
||||
|
||||
### Migration Control
|
||||
|
||||
| Variable | Example | Description | Shared or Per-Site? |
|
||||
|----------|---------|-------------|---------------------|
|
||||
| `RUN_MIGRATIONS` | `true` | Whether this deployment should run database migrations on startup. Only one deployment should have this set to `true`. | **Per-site** (only one `true`) |
|
||||
|
||||
### Optional
|
||||
|
||||
| Variable | Example | Description | Shared or Per-Site? |
|
||||
|----------|---------|-------------|---------------------|
|
||||
| `NODE_ENV` | `production` | Node environment. | **Per-site** (usually `production`) |
|
||||
| `LOG_LEVEL` | `info` | Logging verbosity. | **Per-site** |
|
||||
| `FEATURE_FLAGS` | `events:on` | Comma-separated feature flags (future). | **Per-site** |
|
||||
|
||||
---
|
||||
|
||||
## Example: Full Deployment Env File
|
||||
|
||||
### Deployment: bad-movies-theater (primary — runs migrations)
|
||||
|
||||
```env
|
||||
# Site Identity
|
||||
SITE_SLUG=bad-movies-theater
|
||||
PUBLIC_SITE_URL=https://badmovies.example.com
|
||||
|
||||
# Database (shared)
|
||||
DATABASE_URL=postgresql://hub_app:password@db.example.com:5432/collective_hub
|
||||
|
||||
# Auth (Better Auth)
|
||||
BETTER_AUTH_SECRET=generated-secret-value-here
|
||||
BETTER_AUTH_URL=https://badmovies.example.com
|
||||
|
||||
# Discord OAuth (shared)
|
||||
DISCORD_CLIENT_ID=123456789012345678
|
||||
DISCORD_CLIENT_SECRET=your-discord-client-secret
|
||||
|
||||
# Ownership Bootstrap
|
||||
OWNER_DISCORD_ID=123456789012345678
|
||||
|
||||
# Super Admin
|
||||
SUPER_ADMIN_DISCORD_IDS=111111111111111111
|
||||
|
||||
# Migration Control
|
||||
RUN_MIGRATIONS=true
|
||||
|
||||
# CDN (shared)
|
||||
CDN_BASE_URL=https://cdn.example.com
|
||||
CDN_STORAGE_ENDPOINT=https://ny.storage.bunnycdn.com
|
||||
CDN_ACCESS_KEY=your-bunny-access-key
|
||||
CDN_BUCKET=collective-hub
|
||||
|
||||
# Optional
|
||||
NODE_ENV=production
|
||||
LOG_LEVEL=info
|
||||
```
|
||||
|
||||
### Deployment: garbage-day (secondary — skips migrations)
|
||||
|
||||
```env
|
||||
# Site Identity
|
||||
SITE_SLUG=garbage-day
|
||||
PUBLIC_SITE_URL=https://garbageday.example.com
|
||||
|
||||
# Database (shared — same value)
|
||||
DATABASE_URL=postgresql://hub_app:password@db.example.com:5432/collective_hub
|
||||
|
||||
# Auth (Better Auth)
|
||||
BETTER_AUTH_SECRET=generated-secret-value-here
|
||||
BETTER_AUTH_URL=https://garbageday.example.com
|
||||
|
||||
# Discord OAuth (shared — same value)
|
||||
DISCORD_CLIENT_ID=123456789012345678
|
||||
DISCORD_CLIENT_SECRET=your-discord-client-secret
|
||||
|
||||
# Ownership Bootstrap
|
||||
OWNER_DISCORD_ID=987654321098765432
|
||||
|
||||
# Super Admin (same value)
|
||||
SUPER_ADMIN_DISCORD_IDS=111111111111111111
|
||||
|
||||
# Migration Control
|
||||
RUN_MIGRATIONS=false
|
||||
|
||||
# CDN (shared — same values)
|
||||
CDN_BASE_URL=https://cdn.example.com
|
||||
CDN_STORAGE_ENDPOINT=https://ny.storage.bunnycdn.com
|
||||
CDN_ACCESS_KEY=your-bunny-access-key
|
||||
CDN_BUCKET=collective-hub
|
||||
|
||||
# Optional
|
||||
NODE_ENV=production
|
||||
LOG_LEVEL=info
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## What's Shared vs Per-Site
|
||||
|
||||
```
|
||||
Shared across ALL deployments:
|
||||
├── DATABASE_URL (one database)
|
||||
├── BETTER_AUTH_SECRET (same session signing key)
|
||||
├── DISCORD_CLIENT_ID (one Discord OAuth app)
|
||||
├── DISCORD_CLIENT_SECRET
|
||||
├── SUPER_ADMIN_DISCORD_IDS (system maintainers)
|
||||
├── CDN_BASE_URL (one CDN bucket)
|
||||
├── CDN_STORAGE_ENDPOINT
|
||||
├── CDN_ACCESS_KEY
|
||||
├── CDN_SECRET_KEY (if S3)
|
||||
├── CDN_BUCKET
|
||||
└── CDN_REGION (if S3)
|
||||
|
||||
Unique per deployment:
|
||||
├── SITE_SLUG (which site this is)
|
||||
├── PUBLIC_SITE_URL (its domain)
|
||||
├── BETTER_AUTH_URL (must match PUBLIC_SITE_URL)
|
||||
├── OWNER_DISCORD_ID (who owns this site)
|
||||
└── RUN_MIGRATIONS (only one deployment = true)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Notes & Warnings
|
||||
|
||||
### Discord OAuth: Shared App
|
||||
|
||||
All sites share one Discord OAuth application. This means:
|
||||
- Users see the same app name when authorizing (e.g., "The Collective Hub")
|
||||
- The OAuth redirect URL list must include every site's callback URL
|
||||
- Discord has a limit on redirect URLs — manageable for a handful of sites
|
||||
- If the system grows to many sites, each site may need its own Discord OAuth app, or a central auth domain pattern should be introduced
|
||||
|
||||
### Better Auth Secret
|
||||
|
||||
`BETTER_AUTH_SECRET` must be the same across all deployments because sessions are signed with it. This enables super admins to potentially navigate between site admin panels with a single session (future enhancement).
|
||||
|
||||
### Database Migrations — Automated but Coordinated
|
||||
|
||||
Migrations run automatically on app startup, but only on the deployment with `RUN_MIGRATIONS=true`. All other deployments skip migrations (`RUN_MIGRATIONS=false`). This means:
|
||||
|
||||
- **Exactly one deployment** is designated as the migration runner
|
||||
- That deployment must be deployed first when schema changes are made
|
||||
- If the migration runner is down, other deployments still work (they just can't run new migrations)
|
||||
- Choose a stable, low-traffic deployment as the migration runner (or the super admin dashboard deployment)
|
||||
|
||||
### Super Admin Access
|
||||
|
||||
`SUPER_ADMIN_DISCORD_IDS` is a comma-separated list of Discord user IDs. Users matching these IDs bypass site-scoped membership checks and can access any site's admin panel. This is intended for David (system maintainer) and should be kept to a minimal, trusted set of IDs.
|
||||
|
||||
### Sensitive Values
|
||||
|
||||
All secrets (database URL, auth secrets, Discord secrets, CDN keys) must be stored securely in Coolify's environment variable management — never committed to the repository.
|
||||
@@ -0,0 +1,265 @@
|
||||
# Admin UX Planning Document — The Collective Hub
|
||||
|
||||
## Design Principles
|
||||
|
||||
- **Practical over fancy.** A basic form that works beats a beautiful UI that's confusing.
|
||||
- **One page per concern.** Settings, branding, homepage, links, events, assets — each gets its own admin page.
|
||||
- **Instant feedback.** Save button with clear success/error state. No ambiguous "it might have saved" experiences.
|
||||
- **Site-scoped by default, super admin cross-site.** Regular admins see only their site. Super admins (David) can access any site's admin panel.
|
||||
- **Mobile-accessible but desktop-first.** Admins will primarily manage their site from a desktop/laptop.
|
||||
|
||||
---
|
||||
|
||||
## Login Flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant A as Admin/Owner
|
||||
participant S as Site
|
||||
participant D as Discord
|
||||
|
||||
A->>S: Clicks "Admin" or visits /admin
|
||||
S->>S: Check session
|
||||
S-->>A: Redirect to /login (if not logged in)
|
||||
A->>S: Clicks "Login with Discord"
|
||||
S->>D: Redirect to Discord OAuth
|
||||
D->>A: Authorize screen
|
||||
A->>D: Approve
|
||||
D->>S: Callback with code
|
||||
S->>S: Exchange code, get user info
|
||||
S->>S: Check OWNER_DISCORD_ID match
|
||||
S->>S: Create/confirm membership (owner role)
|
||||
S->>S: Create session
|
||||
S-->>A: Redirect to /admin
|
||||
```
|
||||
|
||||
### Login Page
|
||||
|
||||
A simple, centered page with:
|
||||
- Site logo (or site name if no logo set)
|
||||
- "Login with Discord" button (prominent, branded)
|
||||
- Brief explanation: "Sign in to manage [Site Name]"
|
||||
- No other login methods in V1
|
||||
|
||||
### First Owner Login
|
||||
|
||||
1. Owner visits `/login`
|
||||
2. Logs in with Discord
|
||||
3. System detects their Discord ID matches `OWNER_DISCORD_ID`
|
||||
4. Owner membership created automatically
|
||||
5. Redirected to admin dashboard
|
||||
6. Dashboard shows: "Welcome, [username]. You are the owner of [Site Name]."
|
||||
|
||||
### Super Admin Login
|
||||
|
||||
Super admins (Discord IDs listed in `SUPER_ADMIN_DISCORD_IDS`):
|
||||
- Can log in to any site's admin panel regardless of `OWNER_DISCORD_ID`
|
||||
- See a "Super Admin" badge or indicator in the admin UI
|
||||
- Have a "View All Sites" link (placeholdered until Phase 4 super admin dashboard)
|
||||
- Can perform all actions a site owner can
|
||||
|
||||
### Non-Owner Login (Before Role Management)
|
||||
|
||||
If a non-owner, non-super-admin logs in (Phase 1-4 before role management exists):
|
||||
- They are authenticated but have no membership for the current site
|
||||
- Show: "You don't have access to manage this site. Contact the site owner."
|
||||
- No admin pages are accessible
|
||||
|
||||
### Session Behavior
|
||||
|
||||
- Session persists across browser restarts (HTTP-only cookie via Better Auth)
|
||||
- Logout button in admin nav clears session
|
||||
- Session expiry: Better Auth defaults (configurable)
|
||||
|
||||
---
|
||||
|
||||
## Admin Layout
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ [Logo/Name] Admin Panel [User] [Logout] │
|
||||
├──────────┬───────────────────────────────────┤
|
||||
│ │ │
|
||||
│ Dashboard│ │
|
||||
│ Settings │ Content Area │
|
||||
│ Branding │ │
|
||||
│ Homepage │ │
|
||||
│ Links │ │
|
||||
│ Events │ │
|
||||
│ Assets │ │
|
||||
│ Team │ │
|
||||
│ │ │
|
||||
└──────────┴───────────────────────────────────┘
|
||||
```
|
||||
|
||||
- **Left sidebar:** Navigation links. Highlight current page. Collapse on mobile (hamburger).
|
||||
- **Top bar:** Site name, current user avatar + name, logout button.
|
||||
- **Content area:** The active page content.
|
||||
|
||||
---
|
||||
|
||||
## Admin Pages
|
||||
|
||||
### Dashboard (`/admin`)
|
||||
|
||||
**Purpose:** Quick overview and navigation hub.
|
||||
|
||||
**Content:**
|
||||
- Welcome message with owner name
|
||||
- Site status: "Your site is live at [URL]"
|
||||
- Quick stats (future): event count, page views
|
||||
- Quick links to common actions: "Edit Homepage", "Manage Events"
|
||||
- In V1, this can be minimal—a simple landing page after login
|
||||
|
||||
### Settings (`/admin/settings`)
|
||||
|
||||
**Purpose:** Basic site identity.
|
||||
|
||||
**Fields:**
|
||||
- Site name (text input)
|
||||
- Tagline (text input)
|
||||
- Save button
|
||||
|
||||
**Behavior:**
|
||||
- Load current values from `siteSettings` JSON
|
||||
- Save updates the JSON blob
|
||||
- Success toast: "Settings saved"
|
||||
- Error toast with specific message on failure
|
||||
|
||||
### Branding (`/admin/branding`)
|
||||
|
||||
**Purpose:** Visual identity and theme.
|
||||
|
||||
**Fields:**
|
||||
- Logo: file upload button OR manual URL input (Phase 2: asset picker from library)
|
||||
- Background image: same pattern
|
||||
- Favicon: file upload
|
||||
- Theme preset: dropdown (Dark, Light, Custom)
|
||||
- Accent color: color picker + hex input
|
||||
- Background color: color picker + hex input
|
||||
- Text color: color picker + hex input
|
||||
|
||||
**Behavior:**
|
||||
- Color pickers show live preview of the color
|
||||
- "Preview" link opens public site in new tab
|
||||
- Save updates `siteSettings.branding` and `siteSettings.theme` in JSON
|
||||
|
||||
### Homepage (`/admin/homepage`)
|
||||
|
||||
**Purpose:** Edit public homepage content.
|
||||
|
||||
**Fields:**
|
||||
- Hero title (text input)
|
||||
- Hero subtitle (text input)
|
||||
- About/intro text (textarea, plain text or basic Markdown)
|
||||
- Primary button text (text input, e.g., "Join Discord")
|
||||
- Primary button link (URL input)
|
||||
- Toggle: Show next event section (checkbox)
|
||||
- Toggle: Show schedule section (checkbox)
|
||||
|
||||
**Behavior:**
|
||||
- All fields are optional — empty fields are hidden on the public site
|
||||
- Save updates `siteSettings.homepage` in JSON
|
||||
- "View site" link opens public homepage in new tab
|
||||
|
||||
### Links (`/admin/links`)
|
||||
|
||||
**Purpose:** Manage navigation and social links.
|
||||
|
||||
**Sub-pages or tabs:**
|
||||
- **Nav Links tab:** Table of links with label, URL, position (header/footer), sort order
|
||||
- **Social Links tab:** Table of links with platform (dropdown: Discord, Twitter/X, YouTube, Twitch, etc.), label, URL, sort order
|
||||
|
||||
**Behavior:**
|
||||
- Add new link: inline form at top or modal
|
||||
- Edit: inline or modal
|
||||
- Delete: with confirmation
|
||||
- Drag-to-reorder (nice-to-have, manual sort order numbers are fine for V1)
|
||||
- Save is per-link or "Save all changes" button
|
||||
|
||||
### Events (`/admin/events`)
|
||||
|
||||
**Purpose:** Manage event listings.
|
||||
|
||||
**List view:**
|
||||
- Table: title, date/time, status (published/draft), actions (edit/delete)
|
||||
- "Add Event" button
|
||||
- Filter: upcoming, past, drafts
|
||||
|
||||
**Event edit/create form:**
|
||||
- Title (text, required)
|
||||
- Description (textarea)
|
||||
- Event type (dropdown: Screening, Watch Party, Meetup, Other)
|
||||
- Start time (datetime picker)
|
||||
- End time (datetime picker, optional)
|
||||
- Timezone (dropdown, defaults to `America/New_York`)
|
||||
- Location (text, e.g., "Discord Stage")
|
||||
- External link (URL)
|
||||
- Event image (upload or URL)
|
||||
- Published toggle
|
||||
|
||||
**Behavior:**
|
||||
- Events sorted by start time
|
||||
- Draft events hidden from public site
|
||||
- Delete with confirmation
|
||||
- Basic validation: title required, start time required
|
||||
|
||||
### Assets (`/admin/assets`)
|
||||
|
||||
**Purpose:** Browse and manage uploaded files.
|
||||
|
||||
**List view:**
|
||||
- Grid or table of assets with thumbnail, filename, type, size, date
|
||||
- Search/filter by filename
|
||||
- Click to copy CDN URL
|
||||
- Delete with confirmation
|
||||
|
||||
**Upload:**
|
||||
- Drag-and-drop zone or file input
|
||||
- Accept: image/webp, image/png, image/jpeg
|
||||
- Max size: configurable (e.g., 5MB)
|
||||
- Progress indicator during upload
|
||||
- Success: asset appears in list
|
||||
|
||||
**Note:** Asset upload with webp conversion is available from Phase 1. The asset library is fully functional from day one — no manual URL phase.
|
||||
|
||||
### Team (`/admin/team`) — Phase 5+
|
||||
|
||||
**Purpose:** Manage admins and editors.
|
||||
|
||||
**List view:**
|
||||
- Table: user avatar, username, role, actions (change role, remove)
|
||||
|
||||
**Add member:**
|
||||
- Input for Discord username or ID
|
||||
- Role dropdown
|
||||
- "Add" button
|
||||
|
||||
**Behavior:**
|
||||
- Owner cannot be removed or demoted
|
||||
- Owner can promote/demote/remove admins and editors
|
||||
- Changes take effect immediately
|
||||
|
||||
---
|
||||
|
||||
## Error States & Edge Cases
|
||||
|
||||
| Situation | Behavior |
|
||||
|-----------|----------|
|
||||
| Save fails (network error) | Show error toast, keep form data intact |
|
||||
| Save fails (validation) | Highlight invalid fields, show specific messages |
|
||||
| Session expires while editing | Redirect to login on next action, preserve intended destination |
|
||||
| Concurrent editing (two admins) | Last write wins in V1 (no conflict detection) |
|
||||
| Loading state | Skeleton/spinner while data loads |
|
||||
| Empty state | "No events yet. Create your first event!" with action button |
|
||||
| CDN unreachable | Admin still works; image uploads fail with clear error |
|
||||
|
||||
---
|
||||
|
||||
## What to Avoid
|
||||
|
||||
- **Multi-step wizards.** Single-page forms are simpler.
|
||||
- **Inline editing everywhere.** Form pages with clear save buttons are more predictable.
|
||||
- **Real-time preview in V1.** A "preview" link to the public site is sufficient.
|
||||
- **Over-designed dashboards.** The admin panel is a tool, not a product demo.
|
||||
- **Custom permission UIs in V1.** Owner + super admin access is sufficient until Phase 5.
|
||||
@@ -0,0 +1,202 @@
|
||||
# Public Site UX Planning Document
|
||||
|
||||
## Design Principles
|
||||
|
||||
- **Simple landing page, not a full website.** The public site is a single page (with maybe an event detail modal or page). It tells visitors who the community is, what they do, when they meet, and how to join.
|
||||
- **Content-driven by settings.** Everything on the page comes from the database (site settings, events, links). No hardcoded content.
|
||||
- **Theme-aware.** The page renders with the site's branding settings: logo, colors, background image.
|
||||
- **Fast and accessible.** Server-side rendered, minimal client JS, semantic HTML.
|
||||
- **Mobile-responsive.** Many visitors will check on their phones.
|
||||
|
||||
---
|
||||
|
||||
## Homepage Layout
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ NAV BAR │
|
||||
│ [Logo] Site Name [Link1] [Link2] [Link3] │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ HERO SECTION │
|
||||
│ [Background Image Optional] │
|
||||
│ │
|
||||
│ Site Name (large heading) │
|
||||
│ Tagline / Subtitle │
|
||||
│ [Primary CTA Button] │
|
||||
│ │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ABOUT / INTRO SECTION │
|
||||
│ │
|
||||
│ About text (from settings) │
|
||||
│ │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ NEXT EVENT SECTION │
|
||||
│ (if showNextEvent is enabled) │
|
||||
│ │
|
||||
│ "Next Event" heading │
|
||||
│ Event card with: │
|
||||
│ - Title │
|
||||
│ - Date/Time │
|
||||
│ - Description snippet │
|
||||
│ - [Event Link / Details] │
|
||||
│ │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ UPCOMING SCHEDULE SECTION │
|
||||
│ (if showSchedule is enabled) │
|
||||
│ │
|
||||
│ "Upcoming Events" heading │
|
||||
│ List of upcoming events: │
|
||||
│ - Date | Title | Type │
|
||||
│ - Each links to event or external │
|
||||
│ │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ SOCIAL LINKS SECTION │
|
||||
│ │
|
||||
│ [Discord] [Twitter] [YouTube] ... │
|
||||
│ Icon + label, opens in new tab │
|
||||
│ │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ FOOTER │
|
||||
│ Footer nav links │
|
||||
│ "Powered by The Collective Hub" │
|
||||
│ Copyright / site name │
|
||||
└─────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Section Details
|
||||
|
||||
### Nav Bar
|
||||
|
||||
- **Position:** Sticky top (optional, can be static)
|
||||
- **Content:**
|
||||
- Site logo (or site name text if no logo)
|
||||
- Site name (hidden if logo is present and preferred)
|
||||
- Nav links from database (header position), ordered by `sortOrder`
|
||||
- **Mobile:** Hamburger menu collapses nav links into a drawer or dropdown
|
||||
- **Behavior:** External links open in new tab with `rel="noopener noreferrer"`
|
||||
|
||||
### Hero Section
|
||||
|
||||
- **Background:** Site background image (if set) with overlay/gradient for text readability. Falls back to background color.
|
||||
- **Content:**
|
||||
- Site name (large heading, `h1`)
|
||||
- Tagline/subtitle (`p` or `h2`)
|
||||
- Primary CTA button (if button text and link are set)
|
||||
- **Button styling:** Uses accent color. Links to Discord invite, event page, or any URL set by admin.
|
||||
- **If no hero content is set:** Section collapses or shows a minimal version with just the site name.
|
||||
|
||||
### About / Intro Section
|
||||
|
||||
- **Content:** About text from settings. Plain text or basic Markdown rendered to HTML.
|
||||
- **If empty:** Section is hidden entirely.
|
||||
- **Styling:** Clean typography, max-width for readability (~65ch).
|
||||
|
||||
### Next Event Section
|
||||
|
||||
- **Visibility:** Only shown if `showNextEvent` is enabled AND at least one published event exists with a future `startTime`.
|
||||
- **Content:** A single event card showing the soonest upcoming event.
|
||||
- Event title
|
||||
- Date and time (displayed in visitor's local timezone via client-side JS)
|
||||
- Short description (truncated)
|
||||
- Event type badge/chip
|
||||
- Location (if set)
|
||||
- Link: "Event Details" → external link or internal event page
|
||||
- **If no upcoming events:** Optionally show "No upcoming events — check back soon!" or hide section.
|
||||
|
||||
### Upcoming Schedule Section
|
||||
|
||||
- **Visibility:** Only shown if `showSchedule` is enabled.
|
||||
- **Content:** List of upcoming published events (excluding the one shown in "Next Event" if duplicate).
|
||||
- **List format:**
|
||||
- Date (short format)
|
||||
- Event title
|
||||
- Event type icon or badge
|
||||
- Optional: time, location
|
||||
- **Max items:** Configurable, e.g., show next 5 events with a "View all events" link if more exist.
|
||||
- **If no events:** Hide the section.
|
||||
|
||||
### Social Links Section
|
||||
|
||||
- **Visibility:** Shown if any social links exist in the database.
|
||||
- **Content:** Row of icon+label links for each social platform.
|
||||
- **Icons:** Simple SVG icons or icon font. Recognizable platform icons (Discord, Twitter/X, YouTube, Twitch, GitHub, etc.).
|
||||
- **Behavior:** All open in new tab.
|
||||
|
||||
### Footer
|
||||
|
||||
- **Content:**
|
||||
- Footer-position nav links (if any)
|
||||
- Optional: "Powered by The Collective Hub" credit
|
||||
- Site name / copyright
|
||||
- **Styling:** Subtle, smaller text, subdued colors.
|
||||
|
||||
---
|
||||
|
||||
## Theme Rendering
|
||||
|
||||
The site's theme settings are applied via CSS custom properties on the root element:
|
||||
|
||||
```css
|
||||
:root {
|
||||
--color-accent: #e63946;
|
||||
--color-background: #1a1a2e;
|
||||
--color-text: #eaeaea;
|
||||
--color-text-secondary: #b0b0b0;
|
||||
--font-family: 'Inter', sans-serif;
|
||||
/* derived values generated from presets if needed */
|
||||
}
|
||||
```
|
||||
|
||||
**Theme Presets:**
|
||||
- **Dark:** Dark background, light text, accent color applied
|
||||
- **Light:** Light background, dark text, accent color applied
|
||||
- **Custom:** Owner sets each color manually
|
||||
|
||||
Presets provide sensible defaults for derived colors (card backgrounds, borders, muted text) so the owner only needs to pick 3 colors.
|
||||
|
||||
---
|
||||
|
||||
## States
|
||||
|
||||
| State | Behavior |
|
||||
|-------|----------|
|
||||
| **Loading** | Server-rendered, so no loading spinner on initial load. Fast. |
|
||||
| **Empty site (no settings)** | Show site name from `sites` table. Everything else gracefully hidden. |
|
||||
| **No events** | Hide event sections. |
|
||||
| **No branding set** | Use neutral default colors (dark theme as fallback). |
|
||||
| **Error (data fetch fails)** | Show a minimal page with site name. Log error server-side. Don't crash. |
|
||||
| **Site deactivated** (`isActive=false`) | Show a simple "Site is currently unavailable" page with HTTP 503. |
|
||||
|
||||
---
|
||||
|
||||
## Responsive Behavior
|
||||
|
||||
- **Desktop (≥1024px):** Full layout as designed. Multi-column where appropriate.
|
||||
- **Tablet (768-1023px):** Same layout, adjusted spacing. Nav may collapse.
|
||||
- **Mobile (<768px):** Single column. Hamburger nav. Stacked sections. Larger touch targets for buttons.
|
||||
|
||||
---
|
||||
|
||||
## Performance Targets
|
||||
|
||||
- Server-rendered HTML (SSR) — no client-side rendering for public pages
|
||||
- Minimal JavaScript — only for timezone conversion, mobile nav toggle
|
||||
- Images from CDN with proper `width`/`height` and `loading="lazy"` on below-fold images
|
||||
- No client-side data fetching for public pages (all data loaded server-side)
|
||||
|
||||
---
|
||||
|
||||
## What to Avoid
|
||||
|
||||
- **Animation-heavy designs.** Subtle transitions are fine; scroll-triggered animations are not needed.
|
||||
- **Over-complicated layouts.** One column of stacked sections works well and is easy to maintain.
|
||||
- **Custom fonts that hurt performance.** System font stack or a single web font (Inter) is sufficient.
|
||||
- **Client-side routing for public pages.** The homepage is one page. If event detail pages are added later, they should be server-rendered too.
|
||||
- **Assuming content exists.** Every section must handle the "nothing configured yet" case gracefully.
|
||||
@@ -0,0 +1,260 @@
|
||||
# Development Implementation Plan
|
||||
|
||||
## How to Use This
|
||||
|
||||
This is a step-by-step build order for Phase 1 and beyond. Each step lists what to build, what it depends on, and how to verify it works.
|
||||
|
||||
**Follow the order.** Each step builds on the previous one. Don't skip ahead.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Foundation
|
||||
|
||||
### Step 1: Initialize SvelteKit Project
|
||||
|
||||
**What to do:**
|
||||
- Create a new SvelteKit project with `create-svelte` (choose TypeScript, ESLint, Prettier)
|
||||
- Set up the directory structure as outlined in the architecture plan
|
||||
- Configure `svelte.config.js` with the Node adapter for production builds
|
||||
- Create a basic `+page.svelte` that says "Hello World"
|
||||
- Create a `Dockerfile` for production builds
|
||||
|
||||
**Depends on:** Nothing.
|
||||
|
||||
**Verify:** `npm run dev` starts. Browser shows "Hello World". `docker build` succeeds.
|
||||
|
||||
---
|
||||
|
||||
### Step 2: Set Up Postgres and Drizzle
|
||||
|
||||
**What to do:**
|
||||
- Install `drizzle-orm`, `drizzle-kit`, and `postgres` (the npm package)
|
||||
- Create `drizzle.config.ts`
|
||||
- Create `src/lib/server/db/index.ts` with the Postgres connection
|
||||
- Create `src/lib/server/db/schema.ts` with the `sites` table only (start simple)
|
||||
- Run `drizzle-kit push` to create the table in Postgres
|
||||
- Insert a test site row manually: `INSERT INTO sites (slug, name) VALUES ('test-site', 'Test Site');`
|
||||
|
||||
**Depends on:** Step 1. A running Postgres instance (local Docker or remote).
|
||||
|
||||
**Verify:** `drizzle-kit studio` shows the table. A simple script can query the test site.
|
||||
|
||||
---
|
||||
|
||||
### Step 3: Create Core Tables
|
||||
|
||||
**What to do:**
|
||||
- Add all Phase 1 tables to `schema.ts`: `sites`, `users`, `memberships`, `siteSettings`
|
||||
- Define types, enums, indexes, and foreign keys
|
||||
- Run `drizzle-kit push` to create all tables
|
||||
- Optionally create a seed script for a test site
|
||||
|
||||
**Depends on:** Step 2.
|
||||
|
||||
**Verify:** All tables exist in the database. Relationships are correct in Drizzle Studio.
|
||||
|
||||
---
|
||||
|
||||
### Step 4: Implement Site Resolver
|
||||
|
||||
**What to do:**
|
||||
- Create `src/lib/server/site-resolver.ts`
|
||||
- Export a function `getSiteBySlug(slug: string)` that queries the `sites` table and includes `siteSettings`
|
||||
- In `src/hooks.server.ts`, read `SITE_SLUG` from env, call the resolver, attach site to `locals`
|
||||
- Augment `app.d.ts` so `locals.site` is typed
|
||||
- In `src/routes/+layout.server.ts`, load site data from locals and return it
|
||||
- The homepage `+page.svelte` should display the site name
|
||||
|
||||
**Depends on:** Step 3.
|
||||
|
||||
**Verify:** Set `SITE_SLUG=test-site` in `.env`. Homepage shows "Test Site". Change the slug, restart, page breaks cleanly.
|
||||
|
||||
---
|
||||
|
||||
### Step 5: Set Up Better Auth with Discord
|
||||
|
||||
**What to do:**
|
||||
- Install Better Auth and follow its SvelteKit setup guide
|
||||
- Configure Discord OAuth provider
|
||||
- Create `src/lib/server/auth.ts` with the Better Auth configuration
|
||||
- Add the Better Auth API route: `src/routes/api/auth/[...betterAuth]/+server.ts`
|
||||
- Create a `/login` page with a "Login with Discord" button
|
||||
- On successful login, upsert the user into the `users` table
|
||||
|
||||
**Depends on:** Step 3 (users table must exist).
|
||||
|
||||
**Verify:** Visit `/login`, click the Discord button, authorize, get redirected back. User record appears in the `users` table.
|
||||
|
||||
---
|
||||
|
||||
### Step 6: Implement Owner Bootstrap
|
||||
|
||||
**What to do:**
|
||||
- In the auth callback or post-login hook, check if `user.discordId === process.env.OWNER_DISCORD_ID`
|
||||
- If match: upsert a membership row with role `owner` for the current site
|
||||
- In `src/hooks.server.ts`, after auth, load the user's membership for the current site and attach to `locals`
|
||||
- Create an admin auth guard: a `+layout.server.ts` in `/admin` that checks `locals.membership`
|
||||
|
||||
**Depends on:** Step 4 (site resolver), Step 5 (auth).
|
||||
|
||||
**Verify:** Log in with the Discord account matching `OWNER_DISCORD_ID`. Membership row with `owner` role appears. Navigate to `/admin` — accessible. Log in with a different Discord account — `/admin` redirects to `/login` with an error message.
|
||||
|
||||
---
|
||||
|
||||
### Step 7: Build Admin Layout
|
||||
|
||||
**What to do:**
|
||||
- Create `src/routes/admin/+layout.svelte` with sidebar navigation and top bar
|
||||
- Create `src/routes/admin/+layout.server.ts` with the auth guard
|
||||
- Create a minimal `src/routes/admin/+page.svelte` dashboard
|
||||
|
||||
**Depends on:** Step 6.
|
||||
|
||||
**Verify:** Owner logs in, sees admin layout with nav sidebar. Non-owner cannot access.
|
||||
|
||||
---
|
||||
|
||||
### Step 8: Build Admin Settings Page
|
||||
|
||||
**What to do:**
|
||||
- Create `src/routes/admin/settings/+page.svelte`
|
||||
- Form fields: site name, tagline
|
||||
- Load current values from `siteSettings.settings` JSON
|
||||
- Create a form action (or API endpoint) that updates the JSON
|
||||
- Add basic form validation and success/error feedback
|
||||
|
||||
**Depends on:** Step 7.
|
||||
|
||||
**Verify:** Owner can edit site name and tagline. Changes persist after reload. Public homepage reflects changes.
|
||||
|
||||
---
|
||||
|
||||
### Step 9: Build Public Homepage
|
||||
|
||||
**What to do:**
|
||||
- Update `src/routes/+page.server.ts` to load site settings as a flat object
|
||||
- Build `src/routes/+page.svelte` with sections:
|
||||
- Hero (site name, tagline, CTA button if configured)
|
||||
- About (text from settings if configured)
|
||||
- Apply basic CSS styling with CSS custom properties (hardcode dark theme defaults for now)
|
||||
- Handle empty states: sections hide when no content configured
|
||||
|
||||
**Depends on:** Step 4 (site resolver), Step 8 (settings exist).
|
||||
|
||||
**Verify:** Public homepage shows site name and content. Changing settings in admin updates the public page.
|
||||
|
||||
---
|
||||
|
||||
### Step 10: Add Theme CSS Variables
|
||||
|
||||
**What to do:**
|
||||
- In the root layout, generate CSS custom properties from theme settings
|
||||
- Fall back to sensible defaults if no theme configured
|
||||
- Apply variables to the public homepage
|
||||
- Admin panel can use a fixed theme (don't need the public theme in admin)
|
||||
|
||||
**Depends on:** Step 9.
|
||||
|
||||
**Verify:** Changing accent color in admin (once branding page is built) changes the public page's button color. Default theme looks clean.
|
||||
|
||||
---
|
||||
|
||||
### Step 11: Set Up CDN and Asset Upload
|
||||
|
||||
**What to do:**
|
||||
- Configure CDN storage client (Bunny CDN or S3-compatible)
|
||||
- Create `src/lib/server/cdn.ts` with upload, delete, and URL construction helpers
|
||||
- Create an image upload API endpoint: `src/routes/api/assets/+server.ts`
|
||||
- Implement webp conversion and optimization (using `sharp` or similar)
|
||||
- File validation: accepted types (PNG, JPEG, WebP input), max size
|
||||
- Auto-generate CDN keys: `sites/{siteSlug}/{type}/{uuid}.webp`
|
||||
- Create asset records in the `assets` table on successful upload
|
||||
- Create asset library page: `src/routes/admin/assets/+page.svelte`
|
||||
|
||||
**Depends on:** Step 4 (site resolver for siteSlug), Step 7 (admin layout).
|
||||
|
||||
**Verify:** Upload an image via admin. Asset record appears in database. File exists in CDN bucket. Asset library page shows uploaded files. Image is served correctly via CDN URL.
|
||||
|
||||
---
|
||||
|
||||
### Step 12: Set Up Migration Automation
|
||||
|
||||
**What to do:**
|
||||
- Add `RUN_MIGRATIONS` env var check in app startup
|
||||
- If `RUN_MIGRATIONS=true`, run `drizzle-kit migrate` on startup
|
||||
- If `RUN_MIGRATIONS=false` (or unset), skip migrations entirely
|
||||
- Log migration status on startup for observability
|
||||
- Document which deployment is the migration runner
|
||||
|
||||
**Depends on:** Step 2 (Drizzle configured).
|
||||
|
||||
**Verify:** Start app with `RUN_MIGRATIONS=true` — migrations run. Start with `RUN_MIGRATIONS=false` — migrations are skipped. Both deployments work against the same database.
|
||||
|
||||
---
|
||||
|
||||
### Step 13: Implement Super Admin Access
|
||||
|
||||
**What to do:**
|
||||
- Parse `SUPER_ADMIN_DISCORD_IDS` env var (comma-separated) on startup
|
||||
- In the auth hook / membership check, compare user's Discord ID against the super admin list
|
||||
- If match: bypass site-scoped membership checks, grant full admin access
|
||||
- Attach super admin flag to `locals` for conditional UI (e.g., "View All Sites" link)
|
||||
|
||||
**Depends on:** Step 6 (owner bootstrap / membership logic).
|
||||
|
||||
**Verify:** Log in with a Discord ID in `SUPER_ADMIN_DISCORD_IDS`. Access any site's admin panel. Log in with a non-super-admin ID — restricted to their own site.
|
||||
|
||||
---
|
||||
|
||||
### Step 14: Admin Branding Page
|
||||
|
||||
**What to do:**
|
||||
- Create `src/routes/admin/branding/+page.svelte`
|
||||
- Color pickers for accent, background, text colors
|
||||
- Theme preset dropdown (Dark, Light, Custom)
|
||||
- Logo and background selectors: pick from asset library (uploaded in Step 11)
|
||||
|
||||
**Depends on:** Step 8 (settings save pattern), Step 11 (asset library).
|
||||
|
||||
**Verify:** Owner can change colors. Public page reflects changes immediately. Logo and background can be selected from uploaded assets.
|
||||
|
||||
---
|
||||
|
||||
### Step 15: Dockerize and Deploy
|
||||
|
||||
**What to do:**
|
||||
- Finalize Dockerfile (multi-stage build for Node)
|
||||
- Create `docker-compose.yml` for local testing with Postgres
|
||||
- Set up first Coolify deployment (designate as migration runner: `RUN_MIGRATIONS=true`)
|
||||
- Configure all environment variables in Coolify
|
||||
- Deploy and verify public site is accessible
|
||||
|
||||
**Depends on:** Step 9 (working public site), Step 12 (migration automation).
|
||||
|
||||
**Verify:** Site is live at the configured URL. Login works. Admin works. Migrations ran on startup.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 Onward
|
||||
|
||||
Once Phase 1 is fully working, continue with:
|
||||
|
||||
1. Nav links + social links admin pages and public rendering
|
||||
2. Homepage content editor (hero title, subtitle, about text, CTA)
|
||||
3. Events: schema, admin CRUD, public render
|
||||
4. Super admin dashboard (Phase 4)
|
||||
5. Role management (Phase 5)
|
||||
|
||||
Detailed steps for Phases 2-5 should be written once Phase 1 is complete and any lessons learned are incorporated.
|
||||
|
||||
---
|
||||
|
||||
## Development Rules
|
||||
|
||||
- **One migration per schema change.** Don't batch unrelated changes into one migration.
|
||||
- **Test with multiple SITE_SLUG values locally.** Use different `.env` files or a script that switches them.
|
||||
- **Commit often.** Small, focused commits are easier to reason about.
|
||||
- **Migrations run automatically on startup, gated by `RUN_MIGRATIONS`.** Only one deployment runs them. All others skip.
|
||||
- **Keep the public site SSR-only.** No client-side data fetching for public pages.
|
||||
- **Admin pages can use client-side interactivity.** Forms, file uploads, etc. can use Svelte's client-side features.
|
||||
- **All uploaded images are converted to webp.** No raw user files stored on CDN.
|
||||
@@ -0,0 +1,87 @@
|
||||
# Decision Register — The Collective Hub
|
||||
|
||||
All key architectural and product decisions made during planning. This document serves as the authoritative record of what was decided and why.
|
||||
|
||||
---
|
||||
|
||||
## Auth & Discord
|
||||
|
||||
| # | Decision | Rationale |
|
||||
|---|----------|-----------|
|
||||
| Q1 | **One shared Discord OAuth app** for all sites in V1 | Simpler to manage. Works for up to ~10 sites before Discord's redirect URL limit becomes an issue. Each site's callback URL is added to the Discord app's redirect list. Switch to per-site apps later if needed. |
|
||||
| Q2 | **Defer central auth domain** | Not needed for V1. Architecture supports adding it later. |
|
||||
|
||||
## Sites & Domains
|
||||
|
||||
| # | Decision | Rationale |
|
||||
|---|----------|-----------|
|
||||
| Q3 | **Support both custom domains and subdomains** | The app doesn't care what the URL is — it uses `PUBLIC_SITE_URL` from env vars. DNS and Coolify routing are manual per site. Each site runs in its own Coolify container. |
|
||||
| Q4 | **No Host header validation in V1** | Each site is in its own container. Trust the `SITE_SLUG` env var. Add Host validation if moving to single-deployment multi-domain later. |
|
||||
|
||||
## Roles & Permissions
|
||||
|
||||
| # | Decision | Rationale |
|
||||
|---|----------|-----------|
|
||||
| Q5 | **Defer admin invites to Phase 5** | V1 has one owner per site bootstrapped via `OWNER_DISCORD_ID`. David has cross-site access via `SUPER_ADMIN_DISCORD_IDS`. The `memberships` table supports multiple members from day one — just no UI for it yet. |
|
||||
| Q6 | **Multi-site membership with different roles** | Already baked into the schema. A user can be owner of one site and editor of another. No special handling needed. |
|
||||
|
||||
## Super Admin
|
||||
|
||||
| # | Decision | Rationale |
|
||||
|---|----------|-----------|
|
||||
| — | **`SUPER_ADMIN_DISCORD_IDS` env var** for cross-site super admin access | Comma-separated Discord user IDs. David (system maintainer) can access any site's admin panel. These IDs bypass site-scoped membership checks. A dedicated super admin dashboard is planned for Phase 4. |
|
||||
|
||||
## Content & Customization
|
||||
|
||||
| # | Decision | Rationale |
|
||||
|---|----------|-----------|
|
||||
| Q7 | **Single `jsonb` column for theme/site settings** | Simpler than separate columns or key-value tables. No migrations needed when adding new settings. Settings are always loaded as a batch — no need to query individual settings in SQL. |
|
||||
| Q8 | **Fixed homepage fields for V1** | Hero title, about text, CTA button, etc. stored in settings JSON. Covers 90% of what theater sites need. A flexible `homepageSections` table is defined in the database plan as a future upgrade. |
|
||||
| Q9 | **Basic Markdown for long text fields** | About text and event descriptions support headings, bold, italic, links, lists. No raw HTML passthrough for safety. Short fields (hero title, button text) are plain text only. |
|
||||
| Q10 | **Customization checklist confirmed as drafted** | Name/tagline/logo/colors/hero/CTA/nav/social/theme presets in V1. No custom CSS, no extra layout presets, no custom fonts, no custom pages beyond homepage. |
|
||||
|
||||
### V1 Customization Scope (Q10 Detail)
|
||||
|
||||
| Feature | V1? | Phase |
|
||||
|---------|-----|-------|
|
||||
| Site name, tagline | ✅ Yes | Phase 1 |
|
||||
| Logo, background image | ✅ Yes | Phase 2 (from asset library) |
|
||||
| Accent/background/text colors | ✅ Yes | Phase 2 |
|
||||
| Hero title, subtitle, about text | ✅ Yes | Phase 2 |
|
||||
| CTA button text and link | ✅ Yes | Phase 2 |
|
||||
| Nav links | ✅ Yes | Phase 2 |
|
||||
| Social links | ✅ Yes | Phase 2 |
|
||||
| Theme presets (dark/light) | ✅ Yes | Phase 2 |
|
||||
| Custom CSS | ❌ No | Future |
|
||||
| Multiple layout presets | ❌ No | One flexible layout |
|
||||
| Custom fonts | ❌ No | System stack + Inter |
|
||||
| Per-page customization | ❌ No | Homepage only |
|
||||
| Custom pages beyond homepage | ❌ No | Future |
|
||||
|
||||
## Events
|
||||
|
||||
| # | Decision | Rationale |
|
||||
|---|----------|-----------|
|
||||
| Q11 | **No recurring events in V1** | V1 events are one-off. The `isRecurring` boolean is a placeholder. Recurring events add significant complexity (RRULE parsing, occurrence generation, timezone math). |
|
||||
| Q12 | **UTC storage + client-side timezone conversion** | All event times stored as `timestamptz` in UTC. Event's intended timezone stored as a string. Public site converts to visitor's local time using `Intl.DateTimeFormat`. |
|
||||
|
||||
## Assets & CDN
|
||||
|
||||
| # | Decision | Rationale |
|
||||
|---|----------|-----------|
|
||||
| Q13 | **Full upload flow from day one** | No manual URL pasting. Admin uploads images through the app. Assets table tracks all files. CDN integration is Phase 1, not deferred. |
|
||||
| Q14 | **Auto webp conversion and optimization on upload** | All uploaded images are converted to webp and optimized before storage. Uses `sharp` or similar for processing. Consistent format, smaller files, better performance. |
|
||||
|
||||
## Database & Operations
|
||||
|
||||
| # | Decision | Rationale |
|
||||
|---|----------|-----------|
|
||||
| Q15 | **Automated migrations, gated by `RUN_MIGRATIONS` env var** | Exactly one deployment (the "primary") has `RUN_MIGRATIONS=true` and runs migrations on startup. All others skip. This avoids migration collisions while eliminating manual steps. |
|
||||
| Q16 | **Backups handled externally by David** | Not a code concern. David manages Postgres backups separately. |
|
||||
| — | **Shared database with `siteId` scoping** | Confirmed after discussion. One Postgres database, all tables scoped by `siteId`. Simpler than per-site tables/schemas/databases. Enables super admin cross-site queries. |
|
||||
|
||||
## Project Identity
|
||||
|
||||
| # | Decision | Rationale |
|
||||
|---|----------|-----------|
|
||||
| Q17 | **Project name: The Collective Hub** | Chosen by David. Used for repo name, package name, documentation references, CDN bucket name, database name, and public references (e.g., Discord OAuth app name). |
|
||||
@@ -0,0 +1,152 @@
|
||||
# Risks & Things to Avoid
|
||||
|
||||
## Critical Risks
|
||||
|
||||
### Risk 1: Multiple Deployments Running Migrations Simultaneously
|
||||
|
||||
**Severity:** High — could corrupt the database.
|
||||
|
||||
**Why it's a risk:** With multiple Coolify deployments all pointing to the same database, if every deployment runs migrations on startup, they could conflict — two containers trying to create the same table at the same time.
|
||||
|
||||
**Mitigation:**
|
||||
- Migrations run automatically on startup, but gated by `RUN_MIGRATIONS` env var
|
||||
- **Exactly one deployment** has `RUN_MIGRATIONS=true` (the "primary" deployment)
|
||||
- All other deployments set `RUN_MIGRATIONS=false` and skip migrations
|
||||
- Deploy the primary first when schema changes are included
|
||||
- This is enforced by convention, not by a lock — David must ensure only one deployment has the flag set to `true`
|
||||
|
||||
### Risk 2: Missing siteId Scope on Queries
|
||||
|
||||
**Severity:** High — data leaks between sites.
|
||||
|
||||
**Why it's a risk:** If a query forgets to filter by `siteId`, one site could display another site's events, settings, or assets. This is the most common multi-tenant bug.
|
||||
|
||||
**Mitigation:**
|
||||
- All data access functions accept `siteId` as a required parameter
|
||||
- Create a helper that wraps Drizzle queries and enforces `siteId`
|
||||
- Review every query in code review for `siteId` filtering
|
||||
- Consider a lint rule or type-level enforcement (e.g., branded types) later
|
||||
|
||||
### Risk 3: Hardcoding Full CDN URLs
|
||||
|
||||
**Severity:** Medium — painful CDN migration if the CDN provider changes.
|
||||
|
||||
**Why it's a risk:** If the database stores `https://cdn.example.com/sites/bad-movies/logo.webp` and you later switch CDN providers, every URL in the database needs updating.
|
||||
|
||||
**Mitigation:**
|
||||
- Database stores only the CDN key/path (`sites/bad-movies-theater/logo.webp`)
|
||||
- Application constructs full URLs using `CDN_BASE_URL` env var
|
||||
- A single env var change switches all CDN URLs
|
||||
|
||||
### Risk 4: Overbuilding Before the Core Works
|
||||
|
||||
**Severity:** Medium — wasted effort, complexity without value.
|
||||
|
||||
**Why it's a risk:** It's tempting to build the fancy admin dashboard, the AI features, the perfect theming system before the basic site actually works end-to-end. This leads to a complex codebase that doesn't ship.
|
||||
|
||||
**Mitigation:**
|
||||
- Follow the phases strictly
|
||||
- Phase 1 must be fully working (public site + admin login + settings save) before anything else
|
||||
- A working simple site is infinitely more valuable than a half-built complex one
|
||||
- Ask "Can this wait until Phase X?" before building anything
|
||||
|
||||
### Risk 5: Per-Site Custom Code
|
||||
|
||||
**Severity:** Medium — maintenance nightmare.
|
||||
|
||||
**Why it's a risk:** If "Bad Movies Theater" needs something special and you add an `if (site.slug === 'bad-movies-theater')` in the code, that's the start of a slippery slope. Soon every site has special cases, and the "shared codebase" advantage is lost.
|
||||
|
||||
**Mitigation:**
|
||||
- Never write site-specific conditional logic
|
||||
- All customization comes from the database (settings, theme, content)
|
||||
- If a site genuinely needs a unique feature, it should be built as a configurable feature for all sites, or that site should fork the codebase (last resort)
|
||||
|
||||
### Risk 6: Letting Site Owners Write Raw CSS/HTML
|
||||
|
||||
**Severity:** Medium — security and maintenance risk.
|
||||
|
||||
**Why it's a risk:** Allowing custom CSS or HTML opens XSS vectors (if not properly sanitized), breaks the design system, and makes future template updates unpredictable. A site owner could accidentally break their own site's layout.
|
||||
|
||||
**Mitigation:**
|
||||
- No custom CSS field in V1
|
||||
- No raw HTML in content fields — use Markdown with safe rendering
|
||||
- If custom CSS is ever added (future phase), sandbox it (e.g., a per-site stylesheet loaded separately, not injected into the main app)
|
||||
- Clearly document that custom CSS is an advanced/risky feature
|
||||
|
||||
---
|
||||
|
||||
## Design Pitfalls to Avoid
|
||||
|
||||
### Pitfall 1: Making the Admin Panel Too Complex Before the Public Page Works
|
||||
|
||||
The admin panel exists to serve the public page. If you spend weeks on a beautiful admin UI but the public site is a broken placeholder, priorities are wrong.
|
||||
|
||||
**Rule:** The public page must work before any admin page is considered complete.
|
||||
|
||||
### Pitfall 2: Building AI Features Before the Core Template Works
|
||||
|
||||
AI content suggestions, semantic search with pgvector, AI chatbots — these are exciting but useless if a site can't display a homepage with basic content.
|
||||
|
||||
**Rule:** No AI features until Phase 5+ and even then, only if the core template is stable and useful.
|
||||
|
||||
### Pitfall 3: Planning Too Many Layout Options
|
||||
|
||||
One clean, flexible layout that adapts to content is better than three half-baked layout options. Adding a second layout doubles the testing surface.
|
||||
|
||||
**Rule:** One layout in V1. Add a second only when there's a clear, proven need.
|
||||
|
||||
### Pitfall 4: Normalizing Settings Too Aggressively
|
||||
|
||||
A `siteSettings` table with 30 columns (one per setting) means a migration for every new setting. A `jsonb` column means adding a setting is zero-cost.
|
||||
|
||||
**Rule:** Settings go in JSON. Structured, relational data (events, links, assets) goes in normalized tables.
|
||||
|
||||
### Pitfall 5: Building for Scale That Doesn't Exist Yet
|
||||
|
||||
Planning for 1,000 sites when you have 3 leads to over-engineering. The architecture supports growth (multi-tenant from day one), but don't optimize for scale prematurely.
|
||||
|
||||
**Rule:** Architecture should not block growth; implementation should not optimize for it yet.
|
||||
|
||||
---
|
||||
|
||||
## Process Risks
|
||||
|
||||
### Risk 7: Skipping Documentation
|
||||
|
||||
**Severity:** Low now, high later.
|
||||
|
||||
**Why it's a risk:** When there's only one developer (David), documentation feels optional. But when you come back to the project after 6 months, or when a site owner asks how something works, missing documentation hurts.
|
||||
|
||||
**Mitigation:**
|
||||
- These planning docs live in the repo
|
||||
- Add a simple `README.md` with setup instructions
|
||||
- Add code comments for non-obvious logic
|
||||
- Keep a `CHANGELOG.md` once the project is live
|
||||
|
||||
### Risk 8: Super Admin Account Compromise
|
||||
|
||||
**Severity:** High — cross-site data breach.
|
||||
|
||||
**Why it's a risk:** A `SUPER_ADMIN_DISCORD_IDS` entry grants unrestricted access to all sites. If a super admin's Discord account is compromised, all sites are exposed.
|
||||
|
||||
**Mitigation:**
|
||||
- Keep the super admin list minimal (ideally just David)
|
||||
- Super admin Discord accounts should have 2FA enabled
|
||||
- Consider adding a secondary verification step for super admin actions in a future phase
|
||||
- Monitor for unusual super admin activity
|
||||
|
||||
---
|
||||
|
||||
## Summary Checklist
|
||||
|
||||
Before writing any code, confirm:
|
||||
|
||||
- [ ] Migrations are automated but gated by `RUN_MIGRATIONS` — only one deployment runs them
|
||||
- [ ] Every query includes `siteId` filtering
|
||||
- [ ] CDN keys stored in DB, not full URLs
|
||||
- [ ] Phase 1 scope is locked — no feature creep
|
||||
- [ ] No site-specific conditional code planned
|
||||
- [ ] No raw CSS/HTML inputs for site owners
|
||||
- [ ] Database backups are configured (handled externally by David)
|
||||
- [ ] Project name decided: **The Collective Hub**
|
||||
- [ ] Super admin Discord IDs are set and accounts have 2FA enabled
|
||||
Reference in New Issue
Block a user