Discover what to watch next, by yourself or together.
Swiparr turns the dreaded "what should we watch?" question into a fun, collaborative experience.
Like Tinder for movies, but smarter and works for groups.
The struggle is real: 30 minutes of "what should we watch?" that ends with watching the same show again. Swiparr fixes this by:
โจ Turning discovery into a fun, game-like experience
๐ค Finding content everyone actually wants to watch
โก Making group decisions in minutes, not hours
๐ Working with your existing media libraries OR standalone
- Intuitive Swipe Interface - Browse movies with a familiar card-based design
- Multi-Provider Support - Works with Jellyfin, Emby, Plex, or TMDB directly
- Smart Matching - Automatically finds content everyone in your group will enjoy
- Mobile-First - Optimized for phones, with desktop keyboard shortcuts
- PWA Ready - Install as a web app for the best experience
- Instant Sessions - Create or join in seconds, no complex setup
- Flexible Match Rules - Choose "any two people" or "everyone must agree"
- Session Controls - Limit likes, dislikes, or total matches
- Watchlist Sync - Seamlessly save favorites back to your media server
- Jellyfin - Full native integration
- Emby - Experimental support (improving)
- Plex - Experimental support (improving)
- TMDB - No media server required, works standalone
No setup, no server, no problem.
๐ swiparr.com - Free to use, community-supported (hopefully)
One-click deployment, perfect for personal or small group use:
Note: The automatic deployment workflow in Vercel uses the Turso integration by default as a database service provider. Free to set up, possible to swap out 1.
Vercel security note: AUTH_SECRET is auto-generated during the build (via scripts/ensure-auth-secret.cjs) and persisted in the database when not provided.
Using Docker Compose (Recommended):
- Create
docker-compose.yml:
services:
swiparr:
image: ghcr.io/m3sserstudi0s/swiparr:latest
container_name: swiparr
restart: unless-stopped
environment:
- PROVIDER=jellyfin # or plex, emby, tmdb (or set PROVIDER_LOCK to "false")
- JELLYFIN_URL=http://your-jellyfin:8096 # adjust to provider, none without server lock
volumes:
- ./swiparr-data:/app/data
ports:
- 4321:4321- Run it:
docker compose up -dUsing Docker CLI:
docker run -d \
--name swiparr \
--restart unless-stopped \
-p 4321:4321 \
-v $(pwd)/swiparr-data:/app/data \
-e PROVIDER=jellyfin \
-e JELLYFIN_URL=http://your-jellyfin:8096 \
ghcr.io/m3sserstudi0s/swiparr:latestDocker security note: AUTH_SECRET is auto-generated on first boot (via scripts/ensure-auth-secret.cjs) and stored in the database when not provided.
Choose one provider setup based on your needs:
Jellyfin Setup
PROVIDER=jellyfin
JELLYFIN_URL=http://your-jellyfin:8096 # Internal URL (required)
JELLYFIN_PUBLIC_URL=https://jellyfin.example.com # Public URL (optional)
JELLYFIN_USE_WATCHLIST=false # Use Watchlist (plugin needed) vs Favorites (optional)Emby Setup (Experimental)
PROVIDER=emby
EMBY_URL=http://your-emby:8096 # Internal URL (required)
EMBY_PUBLIC_URL=https://emby.example.com # Public URL (optional)Plex Setup (Experimental)
PROVIDER=plex
PLEX_URL=http://your-plex:32400 # Internal URL (required)
PLEX_PUBLIC_URL=https://plex.example.com # Public URL (optional)TMDB Setup (No Server Required)
PROVIDER=tmdb
TMDB_ACCESS_TOKEN=your-tmdb-token # API Read-Only Token (required)
TMDB_DEFAULT_REGION=SE # Default region for availability/certifications (optional)# Authentication
AUTH_SECRET=random-string-32-chars-min # Auto-generated on boot/build when not provided. See Security & Privacy.
USE_SECURE_COOKIES=true # Required for HTTPS
# Application
PORT=4321 # Default port
HOSTNAME=0.0.0.0 # Bind address
DATABASE_URL=file:/app/data/swiparr.db # SQLite path or Turso URL
DATABASE_AUTH_TOKEN=your-token # Required for Turso/Remote DB
# Base path (build-time only โ see Custom Base Path section)
# URL_BASE_PATH=/swipe
# Admin
ADMIN_USERNAME=your-username # Global auto-grant admin privileges
JELLYFIN_ADMIN_USERNAME=jelly-admin # Provider-specific admin (overrides global)
PLEX_ADMIN_USERNAME=plex-admin # Provider-specific admin (overrides global)
EMBY_ADMIN_USERNAME=emby-admin # Provider-specific admin (overrides global)
# Security Headers
X_FRAME_OPTIONS=DENY # Frame control
CSP_FRAME_ANCESTORS=none # Embedding policy
# Network Safety
ALLOW_PRIVATE_PROVIDER_URLS=false # Block private/LAN URLs for user-supplied providers (BYOP)
PLEX_IMAGE_ALLOWED_HOSTS=plex.example.com,*.plex.direct # Optional extra image hosts
# BYOP Mode - Bring Your Own Provider
PROVIDER_LOCK=false # Let users choose and configure their own provider
# Misc
USE_ANALYTICS=false # Enable anonymous usage analytics (Vercel deployments)
ENABLE_DEBUG=false # Enable verbose debug logging and client-server error mapping
USE_STATIC_FILTERS=false # Skip dynamic filter fetching; use built-in genre/year/rating lists instead (useful for very large libraries where filter API calls time out)| Variable | Required? | Default | Description |
|---|---|---|---|
PROVIDER |
โณ๏ธ | jellyfin |
Primary media provider (jellyfin, tmdb, plex, emby) |
PROVIDER_LOCK |
โ | true |
If true, users cannot change the provider at runtime |
JELLYFIN_URL |
โณ๏ธ | - | Internal URL of your Jellyfin server |
JELLYFIN_PUBLIC_URL |
โ | - | Public URL of your Jellyfin server (for client-side access) |
JELLYFIN_USE_WATCHLIST |
โ | false |
Use Jellyfin Watchlist instead of Favorites |
EMBY_URL |
โณ๏ธ | - | Internal URL of your Emby server |
EMBY_PUBLIC_URL |
โ | - | Public URL of your Emby server (for client-side access) |
PLEX_URL |
โณ๏ธ | - | Internal URL of your Plex server |
PLEX_PUBLIC_URL |
โ | - | Public URL of your Plex server (for client-side access) |
PLEX_TOKEN |
โ | - | Plex Admin/Access Token |
TMDB_ACCESS_TOKEN |
โณ๏ธ | - | TMDB API Read-Only Access Token |
TMDB_DEFAULT_REGION |
โ | SE |
Default TMDB region (ISO 3166-1) for streaming availability/certifications |
AUTH_SECRET |
โ | Auto-generated on boot/build | Secret used for session encryption and guest lending token encryption (min 32 chars). See Security & Privacy. |
USE_SECURE_COOKIES |
โ | false |
Set to true for HTTPS deployments |
DATABASE_URL |
โ | file:/app/data/swiparr.db |
SQLite path or Turso URL 1 |
DATABASE_AUTH_TOKEN |
โ | - | Auth token for remote databases (e.g. Turso) |
APP_PUBLIC_URL |
โ | swiparr.com |
The public domain where the app is hosted |
URL_BASE_PATH |
โ | - | Base path for subpath deployments (e.g. /swipe). Must be set at image build time โ see Custom Base Path. |
ADMIN_USERNAME |
โ | - | Global admin username (overrides provider-specific) 2 |
JELLYFIN_ADMIN_USERNAME |
โ | - | Jellyfin-specific admin username 2 |
EMBY_ADMIN_USERNAME |
โ | - | Emby-specific admin username 2 |
PLEX_ADMIN_USERNAME |
โ | - | Plex-specific admin username 2 |
X_FRAME_OPTIONS |
โ | DENY |
Security header: X-Frame-Options |
CSP_FRAME_ANCESTORS |
โ | none |
Security header: Content-Security-Policy frame-ancestors |
ALLOW_PRIVATE_PROVIDER_URLS |
โ | false |
Allow private/LAN provider URLs for BYOP user inputs |
PLEX_IMAGE_ALLOWED_HOSTS |
โ | - | Extra allowlist for Plex image hosts (comma-separated). PLEX_URL/PLEX_PUBLIC_URL are allowed by default. |
USE_ANALYTICS |
โ | false |
Enable anonymous usage analytics (Vercel deployments) |
ENABLE_DEBUG |
โ | false |
Enable verbose debug logging and client-server error mapping |
USE_STATIC_FILTERS |
โ | false |
Skip dynamic filter fetching and use built-in genre/year/rating lists. Useful for very large libraries where filter API calls time out. |
โณ๏ธ = Required conditionally
When you create a session, customize it for your group:
Match Strategies
-
Two or More: Any two people liking the same content creates a match
- Best for: Larger groups where majority rules
- Finding: Quick results, more options
-
Unanimous: Everyone must like it for a match
- Best for: Smaller groups wanting guaranteed crowd-pleasers
- Finding: Fewer but higher-quality matches
Session Restrictions
-
Max Likes: Limit right swipes per person
- Forces thoughtful, selective choices
- Prevents mindless approval
-
Max Nopes: Limit left swipes per person
- Stops serial negativity
- Encourages open-mindedness
-
Max Matches: Auto-stop when you have enough options
- Perfect for when you just need 3-4 solid picks
Guest Lending (Account Sharing)
How it works:
- Host enables "Guest Lending" in settings
- Guest joins session with just a name - no account needed
- Swiparr uses the host's credentials to fetch content
- Guest gets a unique ID, their swipes are tracked separately
- Guests cannot access host account or modify settings
Security note: Host credentials are stored server-side and encrypted at rest using AUTH_SECRET while Guest Lending is enabled. See "Generating AUTH_SECRET" in Security & Privacy.
Perfect for: Movie nights with friends who don't have media servers
Admin Role
Automatically Assigned: First user to log in for each provider becomes that provider's admin.
Manual Assignment: Set ADMIN_USERNAME (global) or [PROVIDER]_ADMIN_USERNAME (e.g., JELLYFIN_ADMIN_USERNAME) environment variables.
Admin Privileges:
- Configure included media libraries for the provider
- Manage global provider settings
- Override session restrictions
- Access admin dashboard (only for providers with authentication)
PROVIDER_LOCK=true
One provider, admin-controlled
- Admin configures ONE provider in environment variables
- All users automatically use this provider
- Best for: Families, roommates, shared media servers
PROVIDER_LOCK=false
Bring Your Own Provider
- Each user connects their own provider during onboarding
- Users can switch providers anytime
- Best for: Users with different media servers, and/or you have none
- Generating
AUTH_SECRET(optional):
# macOS and Linux
openssl rand -base64 32Windows users can use https://generate-secret.vercel.app/32.
- Encrypted Sessions: iron-session with secure, encrypted cookies
- Encrypted Guest Lending Tokens: host access tokens are encrypted at rest when Guest Lending is enabled
- Scoped Access: Guests can only swipe, no account access
- Data Ownership: Self-hosted = your data stays on your server
- Provider Isolation: No credential sharing in BYOP mode
- CORS Protection: Configured for safe media server integration
- Security Headers: X-Content-Type-Options, X-XSS-Protection, CSP, Referrer-Policy
- Network Safety: Private/LAN provider URLs are blocked by default; enable via
ALLOW_PRIVATE_PROVIDER_URLS - Mode Awareness: Env-configured providers are trusted when
PROVIDER_LOCK=true; user-supplied URLs are checked
Swiparr is now open for contributions! ๐
- Start with Discussion - Propose changes before coding
- Fork & Develop - After discussion approval
- Pull Request - With clear description and tests
git clone https://github.com/m3sserstudi0s/swiparr.git
cd swiparr
npm install
npm run dev # Start dev server
npm run lint # Check code style- Provider Integrations: Improve Emby/Plex support
- UI/UX: Mobile responsiveness, accessibility
- Performance: Optimize queries, bundle size
- Documentation: Examples, guides, tutorials
- Testing: Add test coverage (currently minimal)
First-time contributors welcome! Start with "good first issue" discussions.
Swiparr is free, open source, and community-supported. Your contributions help:
- โ Buy Me a Coffee - Quick one-time support
- ๐ Star on GitHub - Show your support (it's free!)
- ๐ข Use swiparr.com - The hosted version includes infrastructure funding
All support directly funds development and infrastructure costs.
All support, questions, and discussions happen in GitHub Discussions:
| Topic | Link |
|---|---|
| โ Questions & Help | Ask a Question |
| ๐ก Feature Ideas | Propose a Feature |
| ๐ Bug Reports | Report a Bug |
| ๐ General Chat | Start a Discussion |
If you want to serve Swiparr under a subpath โ e.g. https://jellyfin.example.com/swipe/ โ you need to set URL_BASE_PATH at image build time, not as a runtime environment variable.
Why? Next.js bakes asset URLs (/_next/static/...) into the compiled output at build time. Setting a base path only at runtime can fix page routing but leaves all JS/CSS/image references pointing at the wrong path, breaking the app. The prefix must be known before the build.
The prebuilt image from
ghcr.iodoes not supportURL_BASE_PATHโ it is built without a base path. You must build your own image.
git clone https://github.com/m3sserstudi0s/swiparr.git
cd swiparr
docker build --build-arg URL_BASE_PATH=/swipe -t swiparr-custom .Then use swiparr-custom as your image name in your docker run command.
services:
swiparr:
pull_policy: build
build:
context: https://github.com/m3sserstudi0s/swiparr.git
args:
URL_BASE_PATH: /swipe
container_name: swiparr
restart: unless-stopped
environment:
- JELLYFIN_URL=http://jellyfin:8096
- URL_BASE_PATH=/swipe # must match the --build-arg value exactly
volumes:
- ./swiparr-data:/app/data
ports:
- 4321:4321
URL_BASE_PATHmust also be passed as a runtime environment variable so the app generates correct internal links and auth redirects. It must match the--build-argvalue exactly.
Forward requests for the subpath to the container. The app handles stripping the prefix internally โ do not strip it in the proxy.
Nginx:
location /swipe {
proxy_pass http://swiparr:4321;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}Traefik (Docker labels):
labels:
- "traefik.enable=true"
- "traefik.http.routers.swiparr.rule=Host(`jellyfin.example.com`) && PathPrefix(`/swipe`)"
- "traefik.http.routers.swiparr.tls=true"Caddy:
handle /swipe* {
reverse_proxy swiparr:4321
}Nginx Example:
location / {
proxy_pass http://swiparr:4321;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}Required Headers:
Host- Required for authenticationX-Forwarded-For- Client IP for loggingX-Forwarded-Proto- Protocol detection
volumes:
- ./data:/app/data # Database & cache
- ./logs:/app/logs # Optional: Persist logsUse --env-file .env with Docker for cleaner configuration management.
- AGENTS.md - Developer guide and code standards (for contributors)
- GitHub Releases - Detailed changelog for each version
MIT License - See LICENSE file for details
You're free to use, modify, and distribute Swiparr. Commercial use is permitted.
Made with โค๏ธ and late nights
Footnotes
-
Can be set to a local file (internal to container) OR external URL. Mostly relevant for Vercel deployments, which uses the Turso integration in the set-up workflow by default where these values are auto-generated and -injected. Can of course be swapped out with a database service provider of choice. โฉ โฉ2
-
Only applicable for providers with authentication (Jellyfin, Plex, Emby). Admin role ownership is tracked per-provider. Defaults to the first user of that provider that logs in (if supported), or matching env vars. Admin capabilities are disabled for providers without built-in authentication (like TMDB). โฉ โฉ2 โฉ3 โฉ4
