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_SLUGenvironment 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
# 1. Clone and install
git clone <repo-url>
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:
DATABASE_URL=postgresql://hub_dev:hub_dev_password@localhost:5432/collective_hub
SITE_SLUG=local-dev
# 4. Create database tables
npm run db:push
Alternative: Generate and run formal migrations instead of
db:push:npm run db:generate npm run db:migrate
# 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.
Note: Discord login will not work locally without a configured Discord OAuth application with
http://localhost:5173in 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 setRUN_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.
License
TBD