subcult.tv
SUBCULT — Subculture Collective We build tools, media, and infrastructure for the counterculture.
Stack
| Layer | Choice | Why |
|---|---|---|
| Frontend | Vite + React 19 | Fast builds, HMR, modern ESM-first bundling |
| Language | TypeScript + Go | Type safety on both sides |
| Styling | Tailwind CSS v4 | Utility-first with custom design tokens in CSS |
| Routing | React Router v7 | Client-side SPA routing with nested layouts |
| Content | MDX (via @mdx-js/rollup) | Markdown + JSX for zine posts with rich components |
| API | Go + Chi v5 | Lightweight, idiomatic HTTP router with middleware |
| Database | PostgreSQL 16 | Relational storage for projects, posts, contacts, subscribers |
| Auth | JWT (HMAC SHA256) | Stateless auth with bcrypt password hashing |
| Analytics | Umami | Self-hosted, privacy-first analytics |
| Data | GitHub API + local JSON + DB | Auto-populate projects from GitHub, override locally, manage via admin |
| Fonts | Oswald / Libre Baskerville / JetBrains Mono | Display / body / mono — punk + editorial + terminal |
| Infra | Docker Compose | PostgreSQL, Go API, and Umami in containers |
Quick Start
# Clone
git clone https://github.com/subculture-collective/subcult-tv.git
cd subcult-tv
# Copy environment config
cp .env.example .env
# Edit .env with your values (DB password, JWT secret, admin password)
# Install frontend dependencies
npm install
# Start everything (Docker services + dev server)
make dev
# Or start services individually:
make docker-up # Start PostgreSQL, API, Umami
make dev-frontend # Start Vite dev server at http://localhost:5175
make dev-api # Start Go API locally (outside Docker)
Without Docker (API only)
# Install Go dependencies
cd api && go mod download && cd ..
# Start PostgreSQL separately, then:
make dev-api # Runs Go server on :8080
All Make targets
make help # Show all available commands
make setup # Install all dependencies (npm + Go)
make dev # Docker services + Vite dev server
make build # Build frontend + API for production
make lint # ESLint
make typecheck # TypeScript check
make check # lint + typecheck
make format # Prettier
make docker-up # Start Docker services
make docker-down # Stop Docker services
make docker-logs # Tail Docker logs
make docker-rebuild # Rebuild and restart containers
make db-shell # Open psql shell
make db-reset # Drop and recreate database (data loss!)
make clean # Remove build artifacts
Environment Variables
# Database
POSTGRES_USER=subcult
POSTGRES_PASSWORD=your_secure_password
POSTGRES_DB=subcult_db
DATABASE_URL=postgres://subcult:your_secure_password@localhost:5432/subcult_db?sslmode=disable
# API
JWT_SECRET=your_jwt_secret_here_min_32_chars
ADMIN_USER=admin
ADMIN_PASSWORD=your_admin_password
CORS_ORIGINS=http://localhost:5173,https://subcult.tv
PORT=8080
# Umami Analytics
UMAMI_DATABASE_URL=postgresql://subcult:your_secure_password@postgres:5432/umami
# Frontend (optional)
VITE_GITHUB_TOKEN=ghp_your_token_here # GitHub API (higher rate limits)
VITE_API_URL= # Override API base URL (defaults to /api via proxy)
Routes
Public Pages
| Route | Page | Description |
|---|---|---|
/ |
Home | Hero, featured projects, manifesto, latest posts, newsletter signup, Patreon CTA |
/projects |
Projects | Filterable grid of all projects (type + status filters) |
/projects/:slug |
Project Detail | Full project page with cover art, description, stack, links, CTA |
/support |
Patreon | Tier blocks, what support funds, shipping cadence, CTA |
/about |
About / Studio | Mission, values, what we build/won't build, toolchain |
/zine |
Zine | Index of all MDX posts |
/zine/:slug |
Post | Individual MDX post with full content |
/contact |
Contact | API-backed contact form with mailto fallback, social links |
/press |
Press Kit | Brand identity, color palette, typography, usage rules |
Admin Pages
| Route | Page | Description |
|---|---|---|
/admin/login |
Login | Admin authentication |
/admin |
Dashboard | Stats overview (projects, posts, contacts, subscribers) |
/admin/projects |
Projects CRUD | Create, edit, delete projects |
/admin/posts |
Posts CRUD | Create, edit, delete posts (with publish toggle) |
/admin/contacts |
Contacts | View contact submissions, mark read, delete |
/admin/subscribers |
Subscribers | View newsletter subscribers |
API Endpoints
All API routes are prefixed with /api/v1.
Public
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/health |
Health check |
GET |
/api/v1/projects |
List projects (?status=, ?type=) |
GET |
/api/v1/projects/:slug |
Get project by slug |
GET |
/api/v1/posts |
List published posts (paginated) |
GET |
/api/v1/posts/:slug |
Get post by slug |
POST |
/api/v1/contacts |
Submit contact form |
POST |
/api/v1/newsletter/subscribe |
Subscribe to newsletter |
GET |
/api/v1/newsletter/confirm/:token |
Confirm subscription |
DELETE |
/api/v1/newsletter/unsubscribe |
Unsubscribe |
Protected (requires Authorization: Bearer <token>)
| Method | Endpoint | Description |
|---|---|---|
POST |
/api/v1/auth/login |
Login → returns JWT |
GET |
/api/v1/auth/me |
Current user info |
POST |
/api/v1/projects |
Create project |
PUT |
/api/v1/projects/:id |
Update project |
DELETE |
/api/v1/projects/:id |
Delete project |
POST |
/api/v1/posts |
Create post |
PUT |
/api/v1/posts/:id |
Update post |
DELETE |
/api/v1/posts/:id |
Delete post |
GET |
/api/v1/contacts |
List contacts (paginated) |
PATCH |
/api/v1/contacts/:id/read |
Toggle read status |
DELETE |
/api/v1/contacts/:id |
Delete contact |
GET |
/api/v1/newsletter/subscribers |
List subscribers (paginated) |
GET |
/api/v1/admin/stats |
Dashboard statistics |
Project Structure
subcult_tv/
├── api/ # Go backend
│ ├── cmd/server/main.go # Entry point, graceful shutdown, admin seeding
│ ├── internal/
│ │ ├── config/ # Environment-based config
│ │ ├── database/ # PostgreSQL connection + migrations
│ │ │ └── migrations/ # Embedded SQL migrations
│ │ ├── handlers/ # HTTP handlers (auth, projects, posts, contacts, newsletter, admin)
│ │ ├── middleware/ # JWT auth + request logger
│ │ ├── models/ # Domain types
│ │ └── router/ # Chi router setup with CORS
│ ├── Dockerfile # Multi-stage Alpine build
│ ├── go.mod
│ └── go.sum
├── content/
│ ├── posts/ # MDX blog posts
│ │ ├── subcult-manifesto.mdx
│ │ ├── release-log-field-notes.mdx
│ │ └── how-we-build.mdx
│ └── projects.json # Local project overrides
├── docker/
│ └── postgres/init.sql # Creates umami database
├── public/
│ ├── favicon.svg
│ ├── og-image.svg
│ ├── robots.txt
│ ├── sitemap.xml
│ └── press-kit/
│ └── README.md
├── src/
│ ├── components/
│ │ ├── admin/ # AdminLayout
│ │ ├── effects/ # GlitchFrame, TerminalPanel, CoverArt
│ │ ├── ui/ # Button, Card, Tag
│ │ ├── NewsletterSignup.tsx
│ │ └── ...
│ ├── context/ # EffectsContext, AuthContext
│ ├── lib/
│ │ ├── api.ts # Typed API client for Go backend
│ │ ├── github.ts # GitHub API fetch layer
│ │ └── posts.ts # Post registry
│ ├── pages/
│ │ ├── admin/ # Admin pages (Login, Dashboard, Projects, Posts, Contacts, Subscribers)
│ │ └── ... # Public pages
│ ├── App.tsx # Route definitions (public + admin)
│ ├── main.tsx # Entry point (AuthProvider + EffectsProvider)
│ ├── types.ts # Shared types
│ └── index.css # Full design system + effects
├── docker-compose.yml # PostgreSQL, API, Umami services
├── Makefile # Development commands
├── DESIGN_NOTES.md
├── CONTENT_CHECKLIST.md
├── .env.example
├── index.html
├── package.json
└── vite.config.ts
Design System
Colors
| Token | Hex | Usage |
|---|---|---|
void |
#0a0a0a |
Primary background |
signal |
#ff3333 |
Primary accent (CTAs, links) |
static |
#00ff88 |
Terminal green, success |
flicker |
#ffcc00 |
Warning, highlights |
scan |
#6633ff |
Secondary accent |
chalk |
#e8e0d0 |
Primary text |
bone |
#c4b9a7 |
Body text |
Effects
Three configurable levels via the FX toggle in the nav:
- Clean — No visual effects. Maximum readability.
- Mild Glitch — Subtle scanlines, light noise. Default.
- Full VHS — Scanlines, noise, VHS tracking lines, chromatic aberration.
All effects respect prefers-reduced-motion and are CSS-first (no GPU shaders).
Deployment
Full Stack (recommended)
Deploy the Go API + PostgreSQL + Umami on a VPS, and the frontend on Vercel/Cloudflare Pages with VITE_API_URL pointing to your API domain.
# On your VPS:
cp .env.example .env
# Edit .env with production values
docker compose up -d
# Umami will be available at http://your-server:3001
# API at http://your-server:8080
Frontend Only (Vercel)
npm i -g vercel && vercel --prod
vercel.json is included for SPA fallback rewrites.
Set environment variables in Vercel dashboard:
VITE_API_URL— Your API server URL (e.g.,https://api.subcult.tv)VITE_GITHUB_TOKEN— Optional, for higher GitHub API rate limits
Frontend Only (Cloudflare Pages)
- Connect GitHub repo → Build command:
npm run build→ Output:dist - Add environment variables in the dashboard
Umami Analytics Setup
- After
docker compose up, visithttp://localhost:3001 - Default login:
admin/umami - Add your website, copy the Website ID
- Update
index.htmlwith the Website ID indata-website-id - For production, update the Umami script
srcto your Umami domain
DNS
Point subcult.tv to your deployment provider. Point api.subcult.tv to your API server.
Admin Access
After starting the services, the admin user is automatically seeded from .env:
Username: $ADMIN_USER (default: admin)
Password: $ADMIN_PASSWORD
Navigate to /admin/login to access the admin dashboard.
License
MIT — do what you want, credit appreciated.