All apps · 0 apps
Swiparr
Docker app from grtgbln's Repository
Overview
Readme
View on GitHub
Swiparr 🍿
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 Problem
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
✨ Features
🎬 Content Discovery
- 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
👥 Built for Groups
- 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
🔧 Universal Compatibility
- Jellyfin - Full native integration
- Emby - Experimental support (improving)
- Plex - Experimental support (improving)
- TMDB - No media server required, works standalone
🚀 Quick Start
Fastest: Swiparr Global
No setup, no server, no problem.
🌐 swiparr.com - Free to use, community-supported (hopefully)
Easiest: Deploy to Vercel
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.
Full Control: Self-Host with Docker
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 -d
Using 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:latest
Docker security note: AUTH_SECRET is auto-generated on first boot (via scripts/ensure-auth-secret.cjs) and stored in the database when not provided.
⚙️ Configuration Reference
Provider-Specific Settings
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)
Security & Advanced Options
# 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)
Environment Variable Matrix
| 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. |
[^1]: 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).
✳️ = Required conditionally
Features (Deep dive)
Session Settings
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 Flexibility: Two Modes
Server Lock Mode
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
BYOP Mode
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
🔒 Security & Privacy
- Generating
AUTH_SECRET(optional):
# macOS and Linux
openssl rand -base64 32
Windows 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
🤝 Contributing
Swiparr is now open for contributions! 🎉
How to Contribute
- Start with Discussion - Propose changes before coding
- Fork & Develop - After discussion approval
- Pull Request - With clear description and tests
Development Setup
git clone https://github.com/m3sserstudi0s/swiparr.git
cd swiparr
npm install
npm run dev # Start dev server
npm run lint # Check code style
Contribution Areas
- 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.
💚 Support the Project
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.
📞 Community & Support
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 |
🐳 Docker Advanced Topics
Custom Base Path
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.
If you are not using compose and want to build the 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.
If you are using compose, compose can build and manage the image for you at runtime
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.
Step 3 — Configure your reverse proxy
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
}
Reverse Proxy Configuration (root path)
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
Volume Management
volumes:
- ./data:/app/data # Database & cache
- ./logs:/app/logs # Optional: Persist logs
Environment File
Use --env-file .env with Docker for cleaner configuration management.
📚 Additional Resources
- AGENTS.md - Developer guide and code standards (for contributors)
- GitHub Releases - Detailed changelog for each version
📄 License
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
Install Swiparr on Unraid in a few clicks.
Find Swiparr in Community Apps on your Unraid server, review the template, and click Install. Unraid handles the Docker app or plugin setup from the published template.
Requirements
Requires a separate Jellyfin instance.
Related apps
Explore more like this
Explore allDetails
ghcr.io/m3sserstudi0s/swiparr:latestRuntime arguments
- Web UI
http://[IP]:[PORT:4321]/- Network
bridge- Privileged
- false
Template configuration
Container Port: 4321
- Target
- 4321
- Default
- 4321
- Value
- 4321
Internal URL to your Jellyfin server.
- Target
- JELLYFIN_URL
- Default
- http://IP_ADDRESS:8096
- Value
- http://IP_ADDRESS:8096
(Optional) Public URL to your Jellyfin server. Defaults to the internal URL if not set.
- Target
- JELLYFIN_PUBLIC_URL
Set to true to use Watchlist instead of Favorites
- Target
- JELLYFIN_USE_WATCHLIST
- Default
- false|true
Set to true if accessing Swiparr over HTTPS
- Target
- USE_SECURE_COOKIES
- Default
- false|true
(Optional) A Jellyfin username that will always have admin privileges.
- Target
- ADMIN_USERNAME
Random string (min 32 chars). Generated automatically if not set.
- Target
- AUTH_SECRET
Internal path to database. Not recommended to change.
- Target
- DATABASE_URL
- Default
- file:/app/data/swiparr.db
- Value
- file:/app/data/swiparr.db
Internal port Swiparr listens on. Not recommended to change.
- Target
- PORT
- Default
- 4321
- Value
- 4321
Internal host Swiparr binds to. Not recommended to change.
- Target
- HOSTNAME
- Default
- 0.0.0.0
- Value
- 0.0.0.0
X-Frame-Options header. Set to DISABLED to remove.
- Target
- X_FRAME_OPTIONS
- Default
- DENY
- Value
- DENY
Content-Security-Policy frame-ancestors directive.
- Target
- CSP_FRAME_ANCESTORS
- Default
- none
- Value
- none
Path to the application data folder
- Target
- /app/data
- Default
- /mnt/user/appdata/swiparr/data
- Value
- /mnt/user/appdata/swiparr/data
- Default
- 099
- Value
- 099
- Default
- 100
- Value
- 100