docs(copilot): add Copilot instructions for The Collective Hub

- Add comprehensive project overview and core philosophy
- Document file structure reference for the codebase
- Create key files reference table for task-specific guidance
- Include multi-tenant guidelines and site resolution flow
This commit is contained in:
KungRaseri
2026-06-05 23:46:15 -07:00
parent f4245a996a
commit b192cd53ba
33 changed files with 5710 additions and 120 deletions
@@ -0,0 +1,162 @@
---
description: 'Use when understanding or modifying the multi-tenant architecture of The Collective Hub. Covers site resolution flow, data scoping rules, deployment model, migration safety, and environment variable conventions.'
applyTo: 'src/**/*.ts', 'src/**/*.svelte', 'docs/*.md'
---
# Multi-Tenant Architecture
## Overview
The Collective Hub is a **multi-tenant SvelteKit application**. One codebase powers multiple branded community/theater landing pages. Each deployment is differentiated only by its `SITE_SLUG` environment variable.
```
One Git Repo
├── Coolify Deployment "bad-movies-theater"
│ └── SITE_SLUG=bad-movies-theater
├── Coolify Deployment "garbage-day"
│ └── SITE_SLUG=garbage-day
└── Shared Infrastructure
├── PostgreSQL Database (all sites, scoped by siteId)
└── CDN / Object Storage (all sites, scoped by path)
```
## Site Resolution Flow
Every request follows this path:
```
HTTP Request
→ hooks.server.ts (reads SITE_SLUG from env)
→ site-resolver.ts (queries DB for site + settings by slug)
→ event.locals.site / event.locals.siteSettings (attached for request lifetime)
→ App renders with site context
```
### Implementation
In [`src/hooks.server.ts`](src/hooks.server.ts):
```ts
const slug = env.SITE_SLUG;
const siteContext = await getSiteBySlug(slug);
event.locals.site = siteContext.site;
event.locals.siteSlug = slug;
event.locals.siteSettings = siteContext.settings;
```
In [`src/lib/server/site-resolver.ts`](src/lib/server/site-resolver.ts):
```ts
export async function getSiteBySlug(slug: string): Promise<SiteContext> {
const [site] = await db.select().from(sites).where(eq(sites.slug, slug)).limit(1);
// ...throws if not found
const [settingsRow] = await db.select().from(siteSettings).where(eq(siteSettings.siteId, site.id)).limit(1);
return { site, settings: (settingsRow?.settings ?? {}) };
}
```
### `locals` Typing
See [`src/app.d.ts`](src/app.d.ts) for the full `locals` type definition. Key properties:
| Property | Type | Description |
|----------|------|-------------|
| `locals.site` | `Site` | The current site record from the DB |
| `locals.siteSlug` | `string` | The `SITE_SLUG` env var value |
| `locals.siteSettings` | `SiteSettingsData` | Parsed settings JSON for the site |
| `locals.user` | `User \| null` | Authenticated user (or null for visitors) |
| `locals.membership` | `Membership \| null` | User's membership/role for this site |
## Data Scoping Rule
**Every site-owned record MUST include a `siteId` column.**
This applies to: `siteSettings`, `events`, `assets`, `navLinks`, `socialLinks`, `memberships`, and any future content types.
```sql
-- Always filter by siteId
SELECT * FROM events WHERE site_id = $currentSiteId ORDER BY start_time ASC;
```
In Drizzle:
```ts
const events = await db
.select()
.from(eventsTable)
.where(
and(
eq(eventsTable.siteId, locals.site.id),
eq(eventsTable.isPublished, true)
)
)
.orderBy(asc(eventsTable.startTime));
```
**Consequences of this rule:**
- No cross-site data leaks
- Database is logically multi-tenant
- Future single-deployment/multi-domain model requires no schema changes
## Deployment Model
### Current: Multiple Coolify Deployments
Each Coolify 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 needed later, the system could switch to a single deployment that resolves the site by domain name instead of `SITE_SLUG`. The architecture supports this because all data is already scoped by `siteId`.
## 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
- The migration runner must be deployed first when schema changes are included in a release
- This is enforced by convention (the `RUN_MIGRATIONS` flag), not by a distributed lock
```ts
// In hooks.server.ts — runs at module level, before any request
if (!building && env.RUN_MIGRATIONS === 'true') {
await runMigrations();
}
```
## Environment Variables
Variables are classified as **shared** (same value for all deployments) or **per-site** (unique per deployment):
| Variable | Scope | Purpose |
|----------|-------|---------|
| `SITE_SLUG` | Per-site | Identifies which site this deployment serves |
| `PUBLIC_SITE_URL` | Per-site | Public URL, used for auth callbacks |
| `DATABASE_URL` | Shared | Postgres connection string |
| `BETTER_AUTH_SECRET` | Shared | Session signing key |
| `DISCORD_CLIENT_ID` | Shared | Discord OAuth app ID |
| `DISCORD_CLIENT_SECRET` | Shared | Discord OAuth app secret |
| `OWNER_DISCORD_ID` | Per-site | Discord user ID of the site owner |
| `SUPER_ADMIN_DISCORD_IDS` | Shared | Comma-separated super admin Discord IDs |
| `CDN_BASE_URL` | Shared | Base URL for CDN URLs |
| `RUN_MIGRATIONS` | Per-site (one `true`) | Whether this deployment runs migrations |
Full reference: [`docs/04-environment-variables.md`](docs/04-environment-variables.md)
## Key Risks
1. **Missing `siteId` on queries** — Use a query helper wrapper in Phase 2+ to enforce this at the type level
2. **Hardcoding CDN URLs** — Store only `cdnKey` paths in DB, construct full URLs with `CDN_BASE_URL`
3. **Site-specific conditionals** — Never write `if (site.slug === 'bad-movies-theater')`. All customization comes from DB settings
4. **Multiple migrations running** — Only one deployment should have `RUN_MIGRATIONS=true`
See [`docs/09-risks-and-rules.md`](docs/09-risks-and-rules.md) for the full risk assessment.