Initial commit: The Collective Hub planning documentation
This commit is contained in:
@@ -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 |
|
||||
Reference in New Issue
Block a user