Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
b594a34
Refactor Activity and Profile components: replace TopNav with BottomN…
GraysonCAdams Feb 21, 2026
7c3e302
Resolve all ESLint warnings: refactor for max-lines and cognitive com…
GraysonCAdams Feb 22, 2026
eab7877
Fix all security scan failures: CodeQL alerts, gitleaks, npm audit, c…
GraysonCAdams Feb 22, 2026
e48856e
Fix DAST: create /app/data directory for PGlite in Docker image
GraysonCAdams Feb 22, 2026
9b7db85
Suppress 5 informational ZAP warnings in DAST scan
GraysonCAdams Feb 22, 2026
ede605e
Fix CodeQL HIGH/CRITICAL alerts: ReDoS and SSRF
GraysonCAdams Feb 22, 2026
5f73428
Fix CodeQL log-injection alerts: use structured Pino logging
GraysonCAdams Feb 22, 2026
41139aa
Update spotify-budget tests for structured logging format
GraysonCAdams Feb 22, 2026
3464913
Add local security scanning: CodeQL + npm audit + Gitleaks
GraysonCAdams Feb 22, 2026
20fade4
Add DB schema: funnel sync mode + help banner dismissed
GraysonCAdams Feb 22, 2026
f613acf
Add email system with Resend integration and preview routes
GraysonCAdams Feb 22, 2026
163fc37
Extract spotify-errors module, consolidate activity utils, add auth h…
GraysonCAdams Feb 22, 2026
2c1e88e
Add shared UI primitives: skeleton-row, spinner, track-list-row, user…
GraysonCAdams Feb 22, 2026
a1a7be6
Refactor API routes: centralized error handling, funnel sync, per-cir…
GraysonCAdams Feb 22, 2026
c15ab9a
Refactor client components: extract hooks, sections, use shared UI pr…
GraysonCAdams Feb 22, 2026
2e923af
Update tests: fix mocks for refactored routes and modules
GraysonCAdams Feb 22, 2026
2e5eaca
Update README, global styles, and layout
GraysonCAdams Feb 22, 2026
387956f
Merge remote-tracking branch 'origin/main' into fix/lint-warnings-cle…
GraysonCAdams Feb 22, 2026
309c3dc
Fix prettier formatting in playlists route test
GraysonCAdams Feb 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ VAPID_SUBJECT=mailto:you@example.com
# App
NEXT_PUBLIC_APP_URL=http://127.0.0.1:3000

# Database (optional, defaults to ./data/swapify.db)
# DATABASE_PATH=./data/swapify.db
# Database (optional — omit DATABASE_URL for local PGlite, set for production Postgres)
# DATABASE_URL=postgresql://user:pass@host:5432/swapify
# DATABASE_PATH=./data/swapify-pg

# AI — vibe name generation (optional, get key at console.anthropic.com)
ANTHROPIC_API_KEY=
Expand Down
81 changes: 57 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,37 +1,52 @@
# Swapify
<p align="center">
<img src="public/icons/icon-192.png" alt="Swapify" width="128">
</p>

Collaborative Spotify playlists with reactions — share music with friends through **Swaplists**.
<h1 align="center">Swapify</h1>

Each member adds tracks, swipes to react (thumbs up / thumbs down), and the playlist syncs back to Spotify. Built as a mobile-first PWA.
<p align="center">
<a href="https://github.com/312-dev/swapify/actions/workflows/ci.yml"><img src="https://github.com/312-dev/swapify/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
<a href="https://github.com/312-dev/swapify/actions/workflows/security.yml"><img src="https://github.com/312-dev/swapify/actions/workflows/security.yml/badge.svg" alt="Security"></a>
<a href="https://scorecard.dev/viewer/?uri=github.com/312-dev/swapify"><img src="https://api.scorecard.dev/projects/github.com/312-dev/swapify/badge" alt="OpenSSF Scorecard"></a>
</p>

<p align="center">
Collaborative Spotify playlists with swipe reactions.<br>
Create shared playlists (<strong>Swaplists</strong>), add tracks, and react to each other's picks.
</p>

---

## Features

- **Swaplists** — create or join collaborative playlists linked to Spotify
- **Swipe reactions** — swipe right to vibe, swipe left to skip
- **Vibe sort** — reorder tracks by collective reaction score
- **Real-time sync** — tracks stay in sync with the Spotify playlist
- **Push notifications** — get notified when friends add tracks or react
- **Email invites** — invite members by email with verification
- **Installable PWA** — add to home screen, works offline-capable
- **Swaplists** — create or import collaborative playlists linked to your Spotify account
- **Swipe reactions** — swipe right to vibe, swipe left to skip; reactions drive playlist curation
- **Vibe sort** — auto-reorder tracks by collective reaction score and audio features
- **Real-time sync** — tracks stay in sync with the underlying Spotify playlist
- **Activity feed** — see what friends are adding, reacting to, and listening to
- **Push notifications** — get notified when friends add tracks or react to yours
- **Email invites** — invite members by email with secure verification links
- **AI vibe names** — auto-generated Daylist-style labels for playlists (Claude Haiku)
- **Installable PWA** — add to home screen with offline support and native feel

## Tech Stack

- [Next.js](https://nextjs.org) 16 (App Router, Turbopack)
- TypeScript, Tailwind CSS v4, [shadcn/ui](https://ui.shadcn.com)
- [Next.js](https://nextjs.org) 16 (App Router, Turbopack) + React 19
- TypeScript, [Tailwind CSS](https://tailwindcss.com) v4, [shadcn/ui](https://ui.shadcn.com)
- [Motion](https://motion.dev) (Framer Motion v11+) for gestures and animations
- [Drizzle ORM](https://orm.drizzle.team) + SQLite (better-sqlite3)
- Spotify OAuth PKCE (no NextAuth)
- [iron-session](https://github.com/vvo/iron-session) for cookie sessions
- [Drizzle ORM](https://orm.drizzle.team) + PostgreSQL ([PGlite](https://pglite.dev) local / [node-postgres](https://node-postgres.com) production)
- Spotify OAuth PKCE (no NextAuth) + [iron-session](https://github.com/vvo/iron-session)
- [Resend](https://resend.com) for transactional email
- Web Push (VAPID) for notifications
- [Web Push](https://developer.mozilla.org/en-US/docs/Web/API/Push_API) (VAPID) for notifications
- [Pino](https://getpino.io) structured logging
- Deployed on [Fly.io](https://fly.io) with Docker

## Getting Started

### Prerequisites

- Node.js 20+
- A [Spotify Developer](https://developer.spotify.com/dashboard) app with a redirect URI set to `http://127.0.0.1:3000/api/auth/callback`
- A [Spotify Developer](https://developer.spotify.com/dashboard) app with redirect URI set to `http://127.0.0.1:3000/api/auth/callback`

### Setup

Expand All @@ -42,8 +57,8 @@ npm install
# Copy environment template and fill in your values
cp .env.example .env.local

# Initialize the database
npm run db:seed
# Run database migrations (creates local PGlite DB automatically)
npm run db:migrate

# Start the dev server
npm run dev
Expand All @@ -53,16 +68,28 @@ Open [http://127.0.0.1:3000](http://127.0.0.1:3000) to log in with Spotify.

### Environment Variables

See [`.env.example`](.env.example) for all required variables. At minimum you need:
See [`.env.example`](.env.example) for all variables. At minimum you need:

| Variable | Description |
|---|---|
| `SPOTIFY_CLIENT_ID` | From your Spotify Developer app |
| `SPOTIFY_REDIRECT_URI` | OAuth callback URL (`http://127.0.0.1:3000/api/auth/callback` for local) |
| `IRON_SESSION_PASSWORD` | Random string, min 32 characters |
| `POLL_SECRET` | Random string, min 16 characters (polling auth) |
| `NEXT_PUBLIC_APP_URL` | Full app URL (e.g., `http://127.0.0.1:3000`) |

Optional for full functionality:

Optional for full functionality: `RESEND_API_KEY` (email invites), `NEXT_PUBLIC_VAPID_PUBLIC_KEY` / `VAPID_PRIVATE_KEY` (push notifications).
| Variable | Description |
|---|---|
| `DATABASE_URL` | Production Postgres connection string (omit for local PGlite) |
| `RESEND_API_KEY` | Email invites via Resend |
| `NEXT_PUBLIC_VAPID_PUBLIC_KEY` / `VAPID_PRIVATE_KEY` | Web push notifications |
| `TOKEN_ENCRYPTION_KEY` | AES-256-GCM encryption for Spotify tokens at rest |
| `ANTHROPIC_API_KEY` | AI vibe name generation (Claude Haiku) |
| `SPOTIFY_DEV_MODE` | Set `true` for Spotify apps in development mode (5-user cap, conservative limits) |

Generate VAPID keys with:
Generate VAPID keys:

```bash
npx web-push generate-vapid-keys
Expand All @@ -75,9 +102,15 @@ npx web-push generate-vapid-keys
| `npm run dev` | Start dev server |
| `npm run build` | Production build |
| `npm run lint` | ESLint |
| `npm run format` | Prettier |
| `npm run lint:strict` | ESLint with zero warnings |
| `npm run format` | Prettier format |
| `npm run type-check` | TypeScript check |
| `npm run db:seed` | Seed database with sample data |
| `npm test` | Run unit tests (Vitest) |
| `npm run test:watch` | Tests in watch mode |
| `npm run db:migrate` | Run database migrations |
| `npm run db:generate` | Generate migrations from schema changes |
| `npm run security` | Local security scans (CodeQL + npm audit + Gitleaks) |
| `npm run security:codeql` | CodeQL SAST only |

## License

Expand Down
4 changes: 4 additions & 0 deletions drizzle/0026_funnel_sync_mode.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
ALTER TABLE "playlist_members" ADD COLUMN "liked_sync_mode" text;--> statement-breakpoint
ALTER TABLE "playlist_members" ADD COLUMN "liked_playlist_name" text;--> statement-breakpoint
-- Backfill: existing liked playlists were all created via "Save to Spotify"
UPDATE "playlist_members" SET "liked_sync_mode" = 'created' WHERE "liked_playlist_id" IS NOT NULL;
1 change: 1 addition & 0 deletions drizzle/0027_add_help_banner_dismissed.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE "users" ADD COLUMN "help_banner_dismissed" boolean DEFAULT false NOT NULL;
14 changes: 14 additions & 0 deletions drizzle/meta/_journal.json
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,20 @@
"when": 1773100000000,
"tag": "0025_worker_state",
"breakpoints": true
},
{
"idx": 20,
"version": "7",
"when": 1773200000000,
"tag": "0026_funnel_sync_mode",
"breakpoints": true
},
{
"idx": 21,
"version": "7",
"when": 1773300000000,
"tag": "0027_add_help_banner_dismissed",
"breakpoints": true
}
]
}
Loading
Loading