# The Collective Hub > A reusable SvelteKit website template for launching branded landing pages for online theater hosts, watch-party communities, and similar groups. One codebase, multiple deployed websites, one shared database, one CDN. ## Features - Public homepage for a theater/community host with event and schedule display - Discord OAuth login for site owners and admins - Admin panel for customizing branding (name, logo, colors, tagline) - Homepage content editor (intro text, hero, buttons, links) - Multi-site support from a single codebase using `SITE_SLUG` environment variable - Shared PostgreSQL database with data scoped by `siteId` — logically multi-tenant - Shared CDN/object storage bucket with per-site path namespacing - Full image upload flow with automatic WebP conversion and optimization - Asset library for browsing and managing uploaded files - Super admin cross-site access via `SUPER_ADMIN_DISCORD_IDS` ## Tech Stack | Layer | Choice | Notes | |-------|--------|-------| | Framework | SvelteKit | File-based routing, SSR, API routes | | Language | TypeScript | Strict mode | | Database | PostgreSQL 16 | Single shared instance for all sites | | ORM | Drizzle ORM | Type-safe, SQL-first | | Auth | Better Auth | Discord OAuth provider | | CDN / Storage | Bunny CDN | Single bucket, site-scoped paths, auto WebP conversion | | Image Processing | Sharp | Server-side resize and format conversion | | Containerization | Docker | Multi-stage build, Node 22 Alpine | | Deployment | Coolify | Multiple deployments from one Git repo | ## Getting Started ### Prerequisites - Node.js 22+ - Docker (for local PostgreSQL) ### Setup ```bash # 1. Clone and install git clone cd collective-terminal npm install # 2. Start PostgreSQL docker compose up -d # 3. Create your .env file ``` Create a `.env` file at the project root with these values for local development: ```env DATABASE_URL=postgresql://hub_dev:hub_dev_password@localhost:5432/collective_hub SITE_SLUG=local-dev ``` ```bash # 4. Create database tables npm run db:push ``` > **Alternative:** Generate and run formal migrations instead of `db:push`: > ```bash > npm run db:generate > npm run db:migrate > ``` ```bash # 5. Seed the default "local-dev" site npm run db:seed # 6. Start the dev server npm run dev ``` The app opens at [http://localhost:5173](http://localhost:5173). > **Note:** Discord login will not work locally without a configured Discord OAuth application with `http://localhost:5173` in its redirect URL list. The site resolver and public homepage function without Discord auth. ## Environment Variables ### Required | Variable | Example | Description | |----------|---------|-------------| | `SITE_SLUG` | `bad-movies-theater` | Identifies which site this deployment serves. Must match a `slug` in the `sites` table. | | `PUBLIC_SITE_URL` | `https://badmovies.example.com` | Public URL of this deployment. Used for auth callbacks and canonical URLs. | | `DATABASE_URL` | `postgresql://user:pass@host:5432/collective_hub` | PostgreSQL connection string. Same value across all deployments. | | `BETTER_AUTH_SECRET` | *(generated)* | Secret key for session signing. Must be identical across all deployments. | | `BETTER_AUTH_URL` | `https://badmovies.example.com` | Base URL for auth callbacks. Must match `PUBLIC_SITE_URL`. | | `DISCORD_CLIENT_ID` | `123456789012345678` | Discord OAuth application client ID. Shared across all deployments. | | `DISCORD_CLIENT_SECRET` | `abc123...` | Discord OAuth application client secret. Shared across all deployments. | | `OWNER_DISCORD_ID` | `123456789012345678` | Discord user ID of the site owner. Bootstraps ownership on first login. Per-site. | ### CDN (Required for asset uploads) | Variable | Example | Description | |----------|---------|-------------| | `CDN_BASE_URL` | `https://cdn.example.com` | Base URL for constructing public CDN URLs. | | `CDN_STORAGE_ENDPOINT` | `https://ny.storage.bunnycdn.com` | Storage API endpoint. | | `CDN_ACCESS_KEY` | `abc123...` | Storage access key (BunnyCDN password or S3 access key). | | `CDN_BUCKET` | `collective-hub` | Bucket or storage zone name. | ### Optional | Variable | Example | Description | |----------|---------|-------------| | `SUPER_ADMIN_DISCORD_IDS` | `123456789,987654321` | Comma-separated Discord user IDs with cross-site super admin access. | | `RUN_MIGRATIONS` | `true` | Whether this deployment runs database migrations on startup. Only **one** deployment should have `true`. | | `NODE_ENV` | `production` | Node environment (`development` or `production`). | | `LOG_LEVEL` | `info` | Logging verbosity (`debug`, `info`, `warn`, `error`). | ### Shared vs Per-Site **Shared** (same across all deployments): `DATABASE_URL`, `BETTER_AUTH_SECRET`, `DISCORD_CLIENT_ID`, `DISCORD_CLIENT_SECRET`, `SUPER_ADMIN_DISCORD_IDS`, `CDN_*` **Per-site** (unique per deployment): `SITE_SLUG`, `PUBLIC_SITE_URL`, `BETTER_AUTH_URL`, `OWNER_DISCORD_ID`, `RUN_MIGRATIONS` ## npm Scripts | Script | Command | Description | |--------|---------|-------------| | `dev` | `vite dev` | Start the Vite dev server with HMR. | | `build` | `vite build` | Build for production (output to `build/`). | | `preview` | `vite preview` | Preview the production build locally. | | `db:generate` | `drizzle-kit generate` | Generate SQL migration files from Drizzle schema changes. | | `db:migrate` | `drizzle-kit migrate` | Apply pending SQL migrations to the database. | | `db:push` | `drizzle-kit push` | Push schema changes directly to the database (no migration files). Convenient for early development. | | `db:studio` | `drizzle-kit studio` | Open Drizzle Studio, a GUI for browsing and editing data. | | `db:seed` | `tsx src/lib/server/db/seed.ts` | Insert the default `local-dev` site and its settings row. Idempotent — safe to run multiple times. | | `check` | `svelte-kit sync && svelte-check` | Type-check the project with `svelte-check`. | | `lint` | `eslint .` | Run ESLint across the project. | | `format` | `prettier --write .` | Format all source files with Prettier. | ## Project Structure ``` . ├── src/ │ ├── app.d.ts # App types, locals augmentation │ ├── app.html # HTML shell │ ├── hooks.server.ts # Site resolver, auth handling │ ├── lib/ │ │ ├── server/ │ │ │ ├── auth.ts # Better Auth configuration │ │ │ ├── cdn.ts # CDN URL helpers │ │ │ ├── site-resolver.ts # Site loading by SITE_SLUG │ │ │ └── db/ │ │ │ ├── index.ts # Drizzle + Postgres connection │ │ │ ├── migrate.ts # Automated migration runner │ │ │ ├── schema.ts # All table definitions │ │ │ └── seed.ts # Local dev seed data │ │ └── shared/ │ │ └── types.ts # Shared TypeScript types │ └── routes/ │ ├── +layout.server.ts # Root layout, loads site context │ ├── +layout.svelte │ ├── +page.server.ts # Public homepage data │ ├── +page.svelte # Public homepage │ ├── login/ │ │ └── +page.svelte # Discord OAuth login page │ ├── admin/ │ │ ├── +layout.server.ts # Admin auth guard │ │ ├── +layout.svelte # Admin shell / navigation │ │ ├── +page.svelte # Admin dashboard │ │ ├── branding/ # Logo, colors, theme editor │ │ ├── settings/ # Site settings editor │ │ └── assets/ # Asset library (upload, browse) │ └── api/ │ └── assets/ # Asset upload API endpoint ├── drizzle/ # Drizzle migration files ├── scripts/ # Utility scripts ├── docs/ # Project documentation ├── docker-compose.yml # Local PostgreSQL ├── Dockerfile # Multi-stage production build ├── drizzle.config.ts # Drizzle Kit configuration ├── svelte.config.js # SvelteKit configuration ├── tsconfig.json ├── vite.config.ts └── package.json ``` ## Deployment The project is deployed via **Coolify** with Docker. Each community site is a separate Coolify deployment pointing to the same Git repository and branch. Deployments are differentiated solely by environment variables — specifically `SITE_SLUG`. ``` 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 ``` Key deployment rules: - All deployments share one PostgreSQL database and one CDN bucket. - Exactly **one** deployment must have `RUN_MIGRATIONS=true`. All others set `RUN_MIGRATIONS=false`. - The migration-runner deployment should be deployed first when schema changes are included in a release. - Sensitive values (secrets, keys, connection strings) are stored in Coolify's environment variable management — never committed to the repository. - The Dockerfile uses a multi-stage build (Node 22 Alpine) producing a small production image that runs [`build/index.js`](build/index.js). ## License TBD