Monthly YouTube playlist sharing app for the Favorite Medium team. Next.js 15 + Clerk + Postgres, deployable with Docker Compose or a managed Postgres host.
- Google sign-in via Clerk (restricted to
@favoritemedium.com) - Monthly playlists — browse by year and month, then sort by newest or most liked
- Add tracks — paste a YouTube URL with an optional description
- Likes and comments — like songs, add comments, and reply one level deep
- Realtime engagement — live like/comment count updates and in-app submitter notifications
- Google Chat reminders — scheduled prompts and weekly submitter thanks
- Search — filter by submitter, title, artist, or description
- Airtable -> Postgres sync — optional legacy import, runs on page load
- Responsive — works on mobile, tablet, and desktop
- Quality gates — ESLint, TypeScript, Vitest, and GitHub Actions CI
Requires Docker and Docker Compose.
cp .env.example .env
# Edit .env - set Clerk keys and POSTGRES_PASSWORD at minimum.
docker compose up -d --buildOpen http://localhost:3000.
The schema is auto-provisioned on first startup (see docs/DATABASE_SCHEMA.md) — no manual DB setup.
Requires Node 20+ and a running Postgres. Set DATABASE_URL in .env.local.
npm install
cp .env.example .env.local
# Edit .env.local (including DATABASE_URL)
npm run devSee .env.example for the full list.
| Variable | Required | Description |
|---|---|---|
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY |
✔ | Clerk publishable key |
CLERK_SECRET_KEY |
✔ | Clerk secret key |
POSTGRES_DB / POSTGRES_USER / POSTGRES_PASSWORD |
✔ (compose) | Postgres credentials. DATABASE_URL is derived in docker-compose.yml |
DATABASE_URL |
✔ (non-compose) | Full Postgres connection string |
AIRTABLE_API_TOKEN / AIRTABLE_BASE_ID |
— | Enable Airtable sync |
ALLOWED_EMAIL_DOMAIN |
— | Server-side fallback allowlist. Defaults to favoritemedium.com |
SERVICE_URL_APP |
reminders | Public app URL included in Google Chat messages |
GOOGLE_CHAT_WEBHOOK_URL |
reminders | Google Chat Space webhook URL |
REMINDER_CRON_SECRET |
reminders | Bearer token required by scheduled reminder endpoints |
REMINDER_TIME_ZONE |
— | Reminder business timezone. Defaults to Asia/Singapore |
Never commit real environment files. .env, .env.local, and .env.*.local
are ignored, and Docker builds also exclude env files from the build context.
npm run dev # Start dev server (port 3000)
npm run build # Production build
npm run start # Start production server
npm run lint # Run ESLint
npm run typecheck # Run TypeScript without emitting files
npm run test # Run unit testsGET /api/healthis public and returns{"ok": true}for orchestration.GET /api/songsandPOST /api/songsrequire an authenticated Clerk user from the allowed email domain.GET,POST, andDELETE /api/songs/[songId]/likesrequire the same allowed-domain Clerk auth and return the current song engagement summary.GETandPOST /api/songs/[songId]/commentsplusPATCHandDELETE /api/comments/[commentId]require the same auth, support one level of replies, limit comment bodies to 500 characters, and enforce a 5-comments-per-minute rate limit.GET /api/engagement/eventsis an authenticated Server-Sent Events stream used for live engagement counts and submitter comment notifications.GETorPOST /api/reminders/mondayand/api/reminders/fridayrequireAuthorization: Bearer <REMINDER_CRON_SECRET>and send Google Chat messages when reminder env vars are configured.- Postgres is required and is the source of truth. If Postgres is unavailable, the app surfaces an error instead of pretending the playlist is empty.
- Airtable is optional. If Airtable credentials are absent or Airtable fails, the app serves Postgres data only and logs the sync issue.
- New submissions accept YouTube
watch,youtu.be,embed, andshortsURLs only. Descriptions are limited to 500 characters.
See docs/DEPLOYMENT.md.
src/
app/
page.tsx # Home (server component)
api/songs/route.ts # Songs API (GET all, POST new)
api/songs/[songId]/ # Likes and comments APIs per song
api/comments/[commentId]/route.ts # Comment edit/delete API
api/engagement/events/route.ts # Authenticated SSE engagement stream
api/health/route.ts # Unauthenticated health check
api/reminders/ # Secret-protected Google Chat reminder endpoints
components/ # UI, playlist, layout, auth
playlist/EngagementDialog.tsx # Likes/comments UI
playlist/useEngagementEvents.ts # Client SSE subscription hook
lib/
auth.ts # Clerk user mapping and domain checks
airtable.ts # Airtable API (server-only)
db.ts # pg.Pool + ensureSchema()
engagement-db.ts # Likes/comments queries and rules
engagement-events.ts # Postgres LISTEN/NOTIFY fan-out for SSE
songs-db.ts # Postgres queries for songs
reminders*.ts # Reminder scheduling, messages, Google Chat, DB helpers
songs.ts # Unified data layer (Airtable + DB)
youtube.ts # YouTube URL utilities
constants.ts # Shared constants
types/song.ts # TypeScript interfaces
middleware.ts # Clerk middleware
db/
init/001_schema.sql # Auto-provisioned on first DB startup