Repo for the website codebase https://s-nc.org
  • TypeScript 93.4%
  • CSS 6.5%
Find a file
Kevoun f9ff1b5efb memory: drop vestigial related_designs frontmatter (root designs/ tier retired)
Mechanical sweep — single-line removal across 104 items. The
related_designs: [] field is dropped from the items convention; root's
designs/ tier was retired in the same pass with the §releases/ schema
absorbed into item-convention.md.
2026-04-27 15:51:50 +00:00
.claude tech-reference: bump uppy-tus skill v4 → v5 2026-04-23 19:03:57 +00:00
.memory memory: drop vestigial related_designs frontmatter (root designs/ tier retired) 2026-04-27 15:51:50 +00:00
apps fix(api): streaming 0.3.1 triage — on_forward classifier + admin authz 2026-04-24 20:07:04 +00:00
deploy hardening for port forwarding 2026-04-01 06:28:56 +00:00
docs 0.3.0 release-deploy: gates cleared + fix-in-flight + archival 2026-04-24 07:32:02 +00:00
liquidsoap stop tracking playout.liq (now gitignored) 2026-04-16 16:28:16 +00:00
packages/shared fix(api): streaming 0.3.1 triage — on_forward classifier + admin authz 2026-04-24 20:07:04 +00:00
.env.example env: canonical .env.example + mailpit dev SMTP + streaming defaults 2026-04-18 19:59:08 +00:00
.gitignore Item 4: research → .memory/research/, AGENTS.md tech-references hint 2026-04-16 21:25:24 +00:00
AGENTS.md tech-reference: bump uppy-tus skill v4 → v5 2026-04-23 19:03:57 +00:00
bun.lock platform: resumable-uploads-tus implemented — 6 units, tusd + @uppy/tus dual-instance 2026-04-23 14:29:50 +00:00
Caddyfile.dev platform: resumable-uploads-tus implemented — 6 units, tusd + @uppy/tus dual-instance 2026-04-23 14:29:50 +00:00
CLAUDE.md platform: adopt AGENTS.md, slim CLAUDE.md to @AGENTS.md import 2026-04-14 13:21:22 +00:00
CONTRIBUTING.md platform: swap pnpm → bun as package manager 2026-04-14 13:21:22 +00:00
docker-compose.claude.yml platform: resumable-uploads-tus implemented — 6 units, tusd + @uppy/tus dual-instance 2026-04-23 14:29:50 +00:00
docker-compose.yml platform: resumable-uploads-tus implemented — 6 units, tusd + @uppy/tus dual-instance 2026-04-23 14:29:50 +00:00
ecosystem.config.cjs PNPM fix 2026-04-14 18:02:05 +00:00
garage.toml Snctv phase 5 (LiquidSoap playout), playout admin page, route split for content, global player, content detail page overhaul. 2026-03-28 18:30:43 +00:00
LICENSE Updated emissions links and liscensing docs 2026-03-04 20:18:30 +00:00
LICENSE-DOCS Updated emissions links and liscensing docs 2026-03-04 20:18:30 +00:00
package.json platform: swap pnpm → bun as package manager 2026-04-14 13:21:22 +00:00
README.md platform: swap pnpm → bun as package manager 2026-04-14 13:21:22 +00:00
srs.conf 0.2.1, 0.2.2 migration into new format. Reviewed 0.2.1 2026-04-19 11:04:40 +00:00
tsconfig.json Initial commit 2026-03-04 11:57:18 -07:00

S/NC

A worker-cooperative content publishing platform. Creators publish video, audio, and written content behind a subscription gate, sell merchandise via a headless Shopify storefront, embed Bandcamp players, and offer bookable creative services — all governed democratically by cooperative members.

Built as an alternative to extractive platforms.

Live: s-nc.org

Tech Stack

Layer Technology
API Hono on Node.js 24+ with OpenAPI 3.1 docs
Frontend TanStack Start (React 19, SSR, file-based routing)
Database PostgreSQL 16 via Drizzle ORM
Auth Better Auth (email/password, multi-role)
Payments Stripe (subscriptions, checkout, webhooks)
Merch Shopify Storefront API (headless)
Validation Zod 4 (API + shared), zod/mini (frontend)
Testing Vitest, Testing Library, Playwright (e2e)
Object Storage Garage (S3-compatible, production)
Logging pino (structured JSON, request-scoped via hono-pino)
Styling CSS Modules + CSS custom properties (design tokens)
Package Manager Bun 1.3+ with workspaces
Reverse Proxy Caddy (dev + production)

Repository Structure

snc/
├── apps/
│   ├── api/                  # Hono API server
│   │   ├── src/
│   │   │   ├── app.ts        # Hono instance, middleware, route registration
│   │   │   ├── index.ts      # Server entry point, graceful shutdown
│   │   │   ├── config.ts     # Zod-validated env config (60+ fields)
│   │   │   ├── auth/         # Better Auth instance, role service, OIDC seeding
│   │   │   ├── db/           # Drizzle connection + schema definitions
│   │   │   ├── email/        # SMTP sending + inquiry templates
│   │   │   ├── lib/          # Shared helpers (cursor, file utils, OpenAPI errors)
│   │   │   ├── logging/      # Structured pino logger
│   │   │   ├── middleware/    # Auth, CORS, error handling, rate limiting, request logging
│   │   │   ├── routes/       # Domain-grouped route handlers
│   │   │   ├── scripts/      # Seed scripts (admin, demo data)
│   │   │   ├── services/     # Business logic (Stripe, Shopify, SRS, emissions, creators)
│   │   │   └── storage/      # Pluggable StorageProvider (local or S3)
│   │   ├── tests/            # API tests
│   │   └── drizzle/          # SQL migrations
│   ├── e2e/                  # Playwright end-to-end tests
│   └── web/                  # TanStack Start frontend
│       ├── src/
│       │   ├── routes/       # File-based routes
│       │   ├── components/   # React components by domain
│       │   ├── config/       # Navigation links + route config
│       │   ├── contexts/     # Audio player + upload contexts
│       │   ├── hooks/        # Shared hooks (pagination, auth, media, forms)
│       │   ├── lib/          # API clients, auth, formatting utilities
│       │   └── styles/       # Global CSS + shared CSS modules
│       └── tests/            # Web tests
├── packages/
│   └── shared/               # Zod schemas, types, error classes, Result<T,E>
│       ├── src/
│       └── tests/
├── deploy/                   # Example production configs (Caddy, systemd)
├── docs/                     # Platform documentation
├── docker-compose.yml        # PostgreSQL 16
├── ecosystem.config.cjs      # PM2 process config
├── tsconfig.json             # Root TypeScript config
├── Caddyfile.dev             # Dev reverse proxy (localhost ports)
├── .env.example              # Environment template
└── CLAUDE.md                 # Coding conventions

CI workflows live in .forgejo/workflows/. test-and-build.yml is project-generic; deploy workflows (deploy-prod.yml, deploy-demo.yml) are environment-specific and require Forgejo secrets to be configured.

Prerequisites

  • Node.js >= 24.0.0
  • Bun >= 1.3.0
  • Docker (for PostgreSQL)
  • Caddy (optional — reverse proxy for unified :3080 entry point; without it, access API on :3000 and web on :3001 directly)

Getting Started

1. Install dependencies

bun install

2. Start PostgreSQL

docker compose up -d

This starts a PostgreSQL 16 container (snc-postgres) on port 5432 with user snc, password snc, database snc.

3. Configure environment

cp .env.example .env

Edit .env and add the required secrets:

# Required — copy these and fill in real values
DATABASE_URL=postgres://snc:snc@localhost:5432/snc
PORT=3000
CORS_ORIGIN=http://localhost:3080

# Auth — generate a secret with: openssl rand -base64 32
BETTER_AUTH_SECRET=<your-32+-char-secret>
BETTER_AUTH_URL=http://localhost:3080

# Storage (local for dev, s3 for production)
STORAGE_TYPE=local
STORAGE_LOCAL_DIR=./uploads

# S3-compatible storage (required when STORAGE_TYPE=s3)
# S3_ENDPOINT=https://your-s3-endpoint
# S3_REGION=garage
# S3_BUCKET=your-bucket
# S3_ACCESS_KEY_ID=your-access-key
# S3_SECRET_ACCESS_KEY=your-secret-key

# Stripe (optional) — get from https://dashboard.stripe.com/apikeys
# Subscription/billing features return 503 when not set. The rest of the app works.
# STRIPE_SECRET_KEY=sk_test_...
# STRIPE_WEBHOOK_SECRET=whsec_...

# Shopify (optional) — get from Shopify Admin → Apps → Headless
# Merch features return 503 when not set. The rest of the app works.
# SHOPIFY_STORE_DOMAIN=your-store.myshopify.com
# SHOPIFY_STOREFRONT_TOKEN=your_storefront_access_token

The frontend reads VITE_API_URL from apps/web/.env.local (defaults to http://localhost:3000 if not set):

# apps/web/.env.local (optional)
VITE_API_URL=http://localhost:3000

4. Run database migrations

bun run --filter @snc/api db:migrate

4b. Seed demo data (optional)

To populate the database with demo creators, content, users, and subscription plans:

bun run --filter @snc/api seed:demo

This creates three demo users (admin, stakeholder, subscriber) with sample content — useful for seeing the full app working. Skip this if you prefer to start with an empty database and seed manually (see Seeding Data below).

5. Start development servers

Start all three processes — API, web, and Caddy reverse proxy:

# Terminal 1: API server (port 3000)
bun run --filter @snc/api dev

# Terminal 2: Web server (port 3001)
bun run --filter @snc/web dev

# Terminal 3: Caddy reverse proxy (port 3080)
caddy run --config Caddyfile.dev

Or start API + web together with bun run dev, plus Caddy in a separate terminal. There's also bun run start which runs Docker, installs dependencies, migrates, and starts dev servers in one command (still needs .env configured first, and doesn't start Caddy).

The API runs with --watch for automatic restarts. The web server uses Vite HMR. Caddy routes /api, /health, and /uploads to the API and everything else to the web server.

6. Verify it's working

Running Tests

# All unit tests
bun run test:unit

# By workspace
bun run --filter @snc/api test
bun run --filter @snc/web test
bun run --filter @snc/shared test

# Watch mode
bun run --filter @snc/api test -- --watch

Unit tests mock all external services (Stripe, Shopify, database). No running services are needed.

E2E tests use Playwright against a running dev environment. Install browsers first (one-time):

bunx playwright install
bun run --filter @snc/e2e test

Available Scripts

Command Description
bun run dev Start API + web dev servers
bun run test:unit Run all unit tests
bun run build Build all workspaces
bun run typecheck TypeScript type checking
bun run lint Lint all workspaces (via tsc --noEmit)
bun run --filter @snc/api db:generate Generate new Drizzle migration
bun run --filter @snc/api db:migrate Apply pending migrations

Environment Variables Reference

Required

Variable Description
DATABASE_URL PostgreSQL connection string
BETTER_AUTH_SECRET Auth secret, min 32 characters (openssl rand -base64 32)

Optional (with defaults)

Variable Default Description
PORT 3000 API server port
LOG_LEVEL info pino log level (debug, info, warn, error)
CORS_ORIGIN http://localhost:3080 Allowed CORS origin(s), comma-separated
BETTER_AUTH_URL http://localhost:3080 Auth service base URL
STORAGE_TYPE local Storage backend (local or s3)
STORAGE_LOCAL_DIR ./uploads Upload directory for local storage
S3_ENDPOINT S3-compatible endpoint URL (required when STORAGE_TYPE=s3)
S3_REGION garage S3 region
S3_BUCKET S3 bucket name
S3_ACCESS_KEY_ID S3 access key
S3_SECRET_ACCESS_KEY S3 secret key
STRIPE_SECRET_KEY Stripe API key (billing returns 503 without it)
STRIPE_WEBHOOK_SECRET Stripe webhook secret (billing returns 503 without it)
SHOPIFY_STORE_DOMAIN Shopify .myshopify.com domain (merch returns 503 without it)
SHOPIFY_STOREFRONT_TOKEN Shopify Storefront API token (merch returns 503 without it)
SRS_API_URL SRS streaming server API URL (e.g., http://srs-ip:1985)
SRS_HLS_URL SRS HLS base URL (e.g., https://stream.s-nc.tv/live)
PLAYOUT_STREAM_KEY Stream key for Liquidsoap playout (validated by on_publish callback)
LIQUIDSOAP_API_URL Liquidsoap control API URL
LIQUIDSOAP_RTMP_URL rtmp://snc-liquidsoap:1936/live/stream Liquidsoap RTMP ingest URL
SRS_CALLBACK_SECRET Shared secret for SRS HTTP callback authentication
MEDIA_TEMP_DIR /tmp/snc-media Temp directory for media processing jobs
MEDIA_FFMPEG_CONCURRENCY 2 Max parallel FFmpeg processes for job queue
FEDERATION_DOMAIN s-nc.org ActivityPub federation domain
SEAFILE_OIDC_CLIENT_ID Seafile OIDC client ID (OIDC provider inactive without it)
SEAFILE_OIDC_CLIENT_SECRET Seafile OIDC client secret
SMTP_HOST SMTP server (email features degrade gracefully without it)
SMTP_PORT 587 SMTP port
SMTP_USER SMTP username
SMTP_PASS SMTP password
EMAIL_FROM S/NC <noreply@s-nc.org> Sender address for outbound email
STUDIO_INQUIRY_EMAIL Email address for studio booking inquiries
FEATURE_* true Feature flags — see Feature Flags
VITE_API_URL http://localhost:3000 API URL for the frontend (in apps/web/.env.local)

API Endpoints

The API serves OpenAPI 3.1 documentation at /api/openapi. Key endpoint groups:

Group Base Path Auth Description
Health GET /api/health Public Health check
Auth /api/auth/* Public Sign up, sign in, sign out (Better Auth)
Me GET /api/me Authenticated Current user + session + roles
Admin /api/admin/* Admin User management, role assignment
Content /api/content/* Mixed CRUD, upload, feed, media streaming
Creators /api/creators/* Mixed Profiles, avatar/banner, team members, events
Projects /api/projects/* Mixed Creator project/series management
Calendar /api/calendar/* Stakeholder Cooperative calendar, event types, iCal feed
Subscriptions /api/subscriptions/* Mixed Plans, checkout, cancel
Webhooks POST /api/webhooks/stripe Stripe signature Payment event processing
Merch /api/merch/* Public Product listing, detail, checkout
Bookings /api/bookings/* Mixed Services, booking requests, review
Dashboard /api/dashboard/* Stakeholder Revenue, subscribers, bookings KPIs
Studio /api/studio/* Stakeholder Studio dashboard
Emissions /api/emissions/* Stakeholder Carbon tracking and reporting
Streaming /api/streaming/* Mixed SRS streaming, channels, playout, chat
Chat /api/chat/* Mixed WebSocket live chat for streams
Federation /api/federation/* Public ActivityPub federation endpoints
Upload /api/upload/* Authenticated Multipart/presigned upload handling

User Roles

Platform roles are assigned via the user_roles join table. Users without a role are registered users; paying users with an active subscription are patrons. "Creator" is an entity type, not a role — users participate in creator teams (see Creators).

Role Capabilities
stakeholder Cooperative member — access dashboard, calendar, revenue, booking management
admin User management, role assignment, platform administration

Seeding Data

After migrations, the database is empty. To test the full flow:

Subscription plans

Create Products and Prices in the Stripe Dashboard, then insert them:

INSERT INTO subscription_plans (id, name, type, interval, price_cents, currency, stripe_price_id, active)
VALUES
  (gen_random_uuid(), 'All Access Monthly', 'platform', 'month', 999, 'usd', 'price_xxx', true),
  (gen_random_uuid(), 'All Access Yearly', 'platform', 'year', 9999, 'usd', 'price_yyy', true);

Services

INSERT INTO services (id, name, description, pricing_info, active, sort_order)
VALUES
  (gen_random_uuid(), 'Recording Session', 'Professional studio recording.', '$50/hour', true, 1),
  (gen_random_uuid(), 'Mixing & Mastering', 'Full mix and master.', '$200/track', true, 2),
  (gen_random_uuid(), 'Label Services', 'Distribution and licensing.', 'Contact for pricing', true, 3);

Stripe Webhook Testing

To test webhooks locally, install the Stripe CLI and forward events:

stripe listen --forward-to localhost:3000/api/webhooks/stripe

The webhook handler processes these events:

  • checkout.session.completed
  • customer.subscription.updated
  • customer.subscription.deleted
  • invoice.paid
  • invoice.payment_failed

Shopify Merch Setup

Merch is optional. To enable it:

  1. Set SHOPIFY_STORE_DOMAIN and SHOPIFY_STOREFRONT_TOKEN in .env
  2. In Shopify Admin, tag products with snc-creator:<userId> to associate them with creators
  3. Set the vendor field to the creator's display name

Without Shopify credentials, the merch endpoints return 503 "MERCH_NOT_CONFIGURED" — the rest of the app works normally.

Project Conventions

  • Named exports only — no default exports
  • Strict TypeScriptstrict: true, noUncheckedIndexedAccess, exactOptionalPropertyTypes
  • File namingkebab-case.ts (e.g., booking-form.tsx, content.routes.ts)
  • Error handling — Typed AppError subclasses, Result<T, E> for service functions
  • CSS — CSS Modules with design tokens from :root custom properties in global.css
  • Validation — Zod schemas on every route handler via zValidator; zod/mini on the frontend
  • Testing — Vitest with fixture factories (makeMock* pattern), mocked external services

See CLAUDE.md for the full coding conventions reference.

Architecture Notes

  • Monorepo — Bun workspaces, no Turborepo/Nx. Root scripts run across all packages.
  • Shared package@snc/shared contains all Zod schemas, TypeScript types, error classes, and the Result<T, E> type. Both API and web import from it for end-to-end type safety.
  • Storage abstraction — The StorageProvider interface supports local filesystem (dev) and S3-compatible storage (production) without changing route handlers.
  • Content gatingcheckContentAccess() middleware enforces 5 priority rules (public, unauth, owner bypass, active subscription, reject) for subscription-based access control.
  • Cursor pagination — Keyset pagination via base64-encoded cursors, with a reusable useCursorPagination<T> hook on the frontend.
  • No external charting library — The dashboard revenue chart is pure CSS.

Platform Documentation

Domain-specific documentation lives in docs/:

Document Description
Admin User management and role assignment
Auth Authentication, sessions, OIDC provider, roles
Calendar Cooperative calendar, iCal feed
Content Publishing lifecycle, storage backends, access gating
Creators Creator profiles, team membership
Feature Flags Domain-level feature flag system

License

Code and scripts are licensed under the GNU Affero General Public License v3.0 (AGPL-3.0-or-later).

Documentation (README, CLAUDE.md, and other written materials) is licensed under Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0).