Self-hosted playlist sync between Spotify and YouTube Music.
- Sync one playlist, a selection, or everything, in either direction.
- Only changes mode applies what was added/removed since the last sync; Full mirror makes the target an exact copy.
- Playlists are paired by id, so renames on either side are fine. Unpaired playlists fall back to a same-named playlist, otherwise a new one is created.
- Liked songs sync too (Spotify "Liked Songs" ↔ YouTube Music "Liked Music").
- Every removal is previewed and must be confirmed.
- Songs without a confident match are skipped and listed under Review, where you pick the right version or dismiss them.
Browser (Vue SPA) ──► Nuxt server API ──► services ──► providers ──► Spotify Web API
│ └──► YouTube Music (InnerTube, youtubei.js)
└──► repositories ──► SQLite
- The server is a thin JSON API. Artwork loads straight from the services' CDNs.
- Spotify uses OAuth. YouTube Music has no public playlist API, so it is accessed through its internal API with your browser cookie.
- Tokens and cookies are encrypted with AES-256-GCM before they hit the database and never reach the browser.
- The first Spotify and YouTube Music accounts to connect become the instance owner. Other accounts are rejected.
- Spotify development mode (Feb 2026 rules): the app owner needs Premium, at most 5 allow-listed users, and only playlists you own or collaborate on can be read.
- Covers can be copied YouTube Music → Spotify only. YouTube does not allow custom playlist covers through any API.
- YouTube Music cookie expires when you sign out of that browser session. Paste a new one under Settings.
- Song order is not synced.
Requires Node.js 24 (as in the Dockerfile; node:sqlite needs at least 22.13) or Docker.
-
Create an app at the Spotify developer dashboard (Web API). Add the redirect URI:
http://127.0.0.1:3000/api/auth/spotifyfornpm run devhttp://127.0.0.1:8080/api/auth/spotifyfor Dockerhttps://your.domain/api/auth/spotifyin production
Spotify rejects
localhost; use127.0.0.1and open the app on that same host. -
Copy
.env.exampleto.envand fill it in:cp .env.example .env openssl rand -hex 32 # NUXT_SESSION_PASSWORD openssl rand -hex 32 # NUXT_ENCRYPTION_KEY
Variable Required Description NUXT_OAUTH_SPOTIFY_CLIENT_IDYes Client ID of the Spotify app from step 1. NUXT_OAUTH_SPOTIFY_CLIENT_SECRETYes Client secret of the same app. NUXT_OAUTH_SPOTIFY_REDIRECT_URLYes Must match a redirect URI from step 1 exactly. Defaults to http://127.0.0.1:3000/api/auth/spotify.NUXT_SESSION_PASSWORDYes Signs the session cookie. At least 32 characters. NUXT_ENCRYPTION_KEYYes Encrypts stored tokens and cookies. 32 bytes, as 64 hex characters or base64. Changing it makes stored credentials unreadable; connect both services again. NUXT_OWNER_SPOTIFY_IDNo Locks the instance to this Spotify user id (the "Username" on your Spotify account page). Without it, the first account to connect becomes the owner. NUXT_DATABASE_PATHNo SQLite file location. Defaults to ./data/you2sync.db.NUXT_SESSION_COOKIE_SECURENo Set to falseonly when serving production builds over plain HTTP, like the local Docker setup.
npm install
npm run dev # http://127.0.0.1:3000| Command | What it does |
|---|---|
npm install |
Installs dependencies. Also runs postinstall (nuxt prepare), which generates the types in .nuxt/. |
npm run dev |
Starts the dev server with hot reload at http://127.0.0.1:3000. |
npm run build |
Builds the production server into .output/. |
npm run preview |
Runs the production build locally. Run npm run build first. |
npm test |
Runs the unit tests once with Vitest. |
npm run typecheck |
Type-checks the app and server with vue-tsc. Tests are not included. |
npm run generate |
Nuxt's static-site export. Not usable here: the app needs its server API. |
docker compose up -d --build # http://127.0.0.1:8080This runs the app and nginx on your machine only. To put it on a server, see Deployment.
The Docker setup is meant for a small VPS or home server with a domain pointing at it. The app needs HTTPS in production: Spotify requires it for non-loopback redirect URIs, and the session cookie is Secure.
First, on any server:
- Spotify. Add
https://your.domain/api/auth/spotifyas a redirect URI in the Spotify app. - Environment. On the server, clone the repository and create
.envas in Setup, withNUXT_OAUTH_SPOTIFY_REDIRECT_URL=https://your.domain/api/auth/spotify. On a public domain, also setNUXT_OWNER_SPOTIFY_ID; otherwise whoever connects first becomes the owner.
Then pick the option that fits your server.
For a server that already runs Nginx and Certbot for other sites. Docker runs only the app, on localhost; your Nginx proxies the domain to it.
-
Compose file. Create
compose.prod.ymlnext todocker-compose.yml. It is not part of the repository, sogit pullnever touches it.# Fixed project name: the container, network and volume are prefixed # "you2sync", so they never collide with other Compose projects. name: you2sync services: app: build: . restart: unless-stopped env_file: .env environment: NUXT_DATABASE_PATH: /data/you2sync.db # Localhost only, so the app is reachable just through Nginx. # Docker's published ports bypass ufw. Change 3000 on the left # if another service already uses it. ports: - "127.0.0.1:3000:3000" volumes: - data:/data # SQLite database; survives rebuilds mem_limit: 256m read_only: true tmpfs: - /tmp volumes: data:
Add this line to
.env, so plaindocker composecommands use that file:COMPOSE_FILE=compose.prod.yml -
Start.
docker compose up -d --build curl -I http://127.0.0.1:3000 # expect HTTP 200 -
Nginx site. Create
/etc/nginx/sites-available/your.domain:server { listen 80; listen [::]:80; server_name your.domain; # Small JSON API; the only large body is the YouTube Music cookie. client_max_body_size 64k; # Hashed build assets never change. location /_nuxt/ { proxy_pass http://127.0.0.1:3000; proxy_http_version 1.1; proxy_set_header Connection ""; add_header Cache-Control "public, max-age=31536000, immutable"; } # Host and X-Forwarded-* are required: the app compares them with the # request origin and rejects every write without them. location / { proxy_pass http://127.0.0.1:3000; proxy_http_version 1.1; proxy_set_header Connection ""; proxy_set_header Host $host; proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }
sudo ln -s /etc/nginx/sites-available/your.domain /etc/nginx/sites-enabled/ sudo nginx -t && sudo systemctl reload nginx -
TLS. Certbot adds the HTTPS server block and the HTTP redirect to that file:
sudo certbot --nginx -d your.domain
Day to day, from the project folder:
| Task | Command |
|---|---|
| Status | docker compose ps |
| Logs | docker compose logs -f |
| Stop / start / restart | docker compose stop / start / restart |
| Remove the container, keep data | docker compose down |
A stopped app stays stopped, even after a reboot, and nothing else on the server is affected.
For a server with nothing else on ports 80 and 443. docker-compose.yml runs the app and its own nginx.
-
Compose. In
docker-compose.yml, values underenvironmentoverride.env. For theappservice:- set
NUXT_OAUTH_SPOTIFY_REDIRECT_URLtohttps://your.domain/api/auth/spotify; - remove
NUXT_SESSION_COOKIE_SECURE: "false".
For the
nginxservice, publish the public ports and mount your certificates:ports: - "80:80" - "443:443" volumes: - ./deploy/nginx.conf:/etc/nginx/conf.d/default.conf:ro - ./certs:/etc/nginx/certs:ro # fullchain.pem and privkey.pem
- set
-
TLS. In
deploy/nginx.conf, uncomment the443server block and setserver_nameto your domain. Replace thelocationblocks of the port 80 server with a redirect, so the app is only served over HTTPS:location / { return 301 https://$host$request_uri; }
Certificates can come from any ACME client, e.g. certbot. Copy them into
./certsrather than mounting certbot'slivedirectory, which contains symlinks. -
Start.
docker compose up -d --build
Updating: git pull && docker compose up -d --build. Data survives in the data volume.
Backups: the SQLite database lives in the data volume at /data/you2sync.db. Keep NUXT_ENCRYPTION_KEY with it; the stored credentials cannot be decrypted without it.
Run npm ci && npm run build, then node .output/server/index.mjs with the variables from .env set. It listens on PORT (default 3000). Put any HTTPS reverse proxy in front that forwards Host and X-Forwarded-Proto.
- Sign in at music.youtube.com in a desktop browser.
- Open developer tools → Network, and click around until a
browserequest appears. - Copy the full
cookierequest header and paste it into You2Sync.
Nuxt 4 (SPA mode) · Vue 3 · shadcn-vue · Tailwind CSS 4 · nuxt-auth-utils · youtubei.js · node:sqlite · Vitest
- Built by Eduardo Gomez.
- youtubei.js by LuanRT makes the YouTube Music integration possible.
- UI components from shadcn-vue on Reka UI; icons from Lucide.
- Spotify and YouTube Music logos from Simple Icons (CC0).
You2Sync is not affiliated with, endorsed by, or sponsored by Spotify or YouTube. Spotify and YouTube Music are trademarks of their respective owners.
If You2Sync saves you time, you can buy me a coffee on Ko-fi. ☕
MIT © 2026 Eduardo Gomez

