Settle the vacation argument. Pit Airbnb listings against each other. Rate, argue in the comments, settle it on the map.
π Live demo Β· π¨ Brand snapshot Β· π Full brand book Β· π‘ Privacy Β· π Terms
StayBattle is a self-hosted, open-source web app where your group drops Airbnb URLs into one place and votes on the winner together. No accounts, no SaaS, no tracking. You run it on your own box for your own crew.
A public demo runs at https://app.staybattle.com with pre-seeded fake data. Anything you do there gets wiped at 04:00 UTC nightly.
Sign in with any of these demo accounts (PINs are scrypt-hashed server-side; the modal in the app shows the same list):
| Name | PIN | Role |
|---|---|---|
| Alex | 1111 | Organizer |
| Sam | 2222 | Voter |
| Jordan | 3333 | Voter |
| Riley | 4444 | Voter |
| Casey | 5555 | Voter |
| Morgan | 6666 | Voter |
| Drew | 7777 | Voter |
| Quinn | 8888 | Voter |
Invite code: DEMO99. Sign in as Alex / 1111 to try the
organizer powers (close the battle, kick voters, edit dates,
set must-haves). Everyone else is a regular voter.
curl -fsSL https://raw.githubusercontent.com/datboip/StayBattle/main/install.sh | shThat's it. Opens at http://localhost:3000 on the machine you ran the install on. Data lives in ~/staybattle/data.
β οΈ Pin a release tag, don't trackmain. The one-liner above grabs whatever's onmainright now. To pin a known-good version:STAYBATTLE_TAG=v0.4.4 sh <(curl -fsSL https://raw.githubusercontent.com/datboip/StayBattle/v0.4.4/install.sh)
π Before you curl | sh anything (this or any other project) β click to expand
This applies to any project you find online, not just this one. The command above pulls and runs other people's code on your machine.
-
Read
install.shline-by-line. It's 130 lines, no obfuscation. You should be able to tell exactly what it does in under two minutes. If you can't, don't run it. -
Read
docker-compose.ymlandDockerfilebefore docker-compose-up. Both are short. -
npm installis not safe-by-default. The npm ecosystem has had real supply-chain attacks for years and they keep happening, including in the last few weeks:- axios (April 2026) β a package with ~50M weekly downloads, compromised.
- TanStack packages β the React-ecosystem family (Query, Router, Table) used by huge swaths of frontends.
- Older but instructive:
event-stream(2018),ua-parser-js(2021),node-ipc(2022),lottie-player(2024), plus a steady drip of typosquats.
Some basics that protect you:
- Use
npm ci, notnpm installin production.cireadspackage-lock.jsonexactly and refuses to install anything not pinned.npm installwill happily update versions on you. npm auditafter install to flag known CVEs. Not exhaustive, but catches the obvious stuff.npm install --ignore-scriptsskipspostinstallscripts β that's the main vector for arbitrary code execution at install time. Some packages legitimately need them; if you're paranoid, install with the flag and selectively re-enable scripts for packages you trust.- Avoid
npm install -gunless you really need a global binary. Globals install with broader permissions and stick around forever. - Be skeptical of brand-new versions of dependencies you didn't update on purpose. Compromised maintainer accounts publish malicious versions of legit packages. If something updated 2 days ago and now wants to run a postinstall, look at it first.
-
Same goes for
.envfiles and any "paste this command" instruction anywhere in this repo. If something says "run X", check what X is.
StayBattle is open source under AGPL v3, so you can audit everything. That only helps if you actually look.
The installer does exactly four things: check Docker is installed, pull the image, mount a data folder, run the container. Nothing else. No telemetry, no analytics, no remote callbacks.
localhost:3000only works on the machine running the container β your crew can't click it unless you make it reachable. Pick one:
- Same WiFi (easiest) β find your machine's LAN IP (
ip addron Linux Β·ipconfigon Windows Β· System Settings β Network on Mac), then send your crewhttp://192.168.x.y:3000. Works for trips where everyone's at the same house.- From anywhere (recommended) β expose the port with a tunnel. No port-forwarding, no router config:
- Cloudflare Tunnel quick tunnel (free, no signup):
cloudflared tunnel --url http://localhost:3000β prints ahttps://random.trycloudflare.comURL you send to the crew.- ngrok (free tier):
ngrok http 3000β same idea.- Tailscale if your crew is already on your tailnet β share the machine's tailnet IP. This is how the live demo at https://app.staybattle.com works: Docker + a named Cloudflare Tunnel + a domain you own.
Real talk: my family cannot pick a vacation rental. Every single time. Six people, twelve Airbnb candidates, four group texts, zero structure. Someone screenshots one in the chat. Someone else replies "no, this one." Someone DMs you privately "tell them my one was better." After an hour, nobody knows what the actual options are anymore. Trip is soon. Cool.
So I turned the family-vacation-argument into a dope little app.
Drop every candidate Airbnb URL into one place. Rate together. Comment together. Argue in the open. Settle it on the map. Real availability check against Airbnb's own booking widget so nobody picks a place that's actually booked. Trophy case at the end so we remember which house won. Done.
You still book on Airbnb. StayBattle is the meeting table, not the storefront.
- Organizer sets up a battle: trip name, dates, submission deadline, optional must-have amenities (wifi, pool, parking, etc.).
- Crew joins with an invite code, each with their own name + PIN.
- Submission phase β everyone pastes Airbnb URLs in. The server grabs photos, location, beds/baths, rating, and amenity tags from Airbnb's own GraphQL endpoint. Others see only anonymized photos until the deadline β no name-dropping, no bias.
- Battle phase β at the deadline (or when the organizer hits "start now"), everyone can see all submissions. Rate each one 1β5 on a slider (Nope Β· Meh Β· OK Β· Like Β· Love). Leave trash talk. Each submitter's pre-submission "case" sits pinned at the top of the comments. You can't rate your own submission β no ballot stuffing.
- Swipe-through review β a one-card-at-a-time mode for going through the pile; drag the slider to rate, swipe direction follows the score.
- Map β every candidate as a status-colored teardrop pin. Anyone can drop a categorized reference pin (theme parks, restaurants, airports, etc.) β no geocoding needed, click the map. Filter chips toggle categories on/off. Each listing card shows real OSRM-routed drive times to the 3 closest pinned places.
- Close + archive β organizer closes the battle. Top 3 (with ties grouped as co-medalists) get archived to the trophy case with month-rounded dates and scrubbed URLs. Past battles persist across sessions so trip #2 remembers what won trip #1.
You click through to Airbnb to actually book. StayBattle never replaces that step.
First time you use a name, you claim it with any 4β6 digit PIN. After that, the same name+PIN on any device signs you into the same identity β your ratings follow you. No email signup, no OAuth, no Google login, no nothing. PINs are scrypt-hashed (N=16384) with a per-voter random salt; rate-limited to 5 attempts/min/name.
| Live demo at <app.staybattle.com> | Self-hosted | |
|---|---|---|
| Data persistence | Wiped nightly at 04:00 UTC | Forever (it's your SQLite file) |
| Airbnb scraping | None β availability badges are simulated seed data; add-listing and re-check are disabled | Real listing scrape + GraphQL availability check |
| Sign-in | Use the 8 demo accounts above | Anyone with the URL claims a name |
| Cost | Free, no signup | Free, no signup, your hardware |
| Best for | Kicking the tires | Actual trip planning |
git clone https://github.com/datboip/StayBattle.git
cd staybattle
docker compose up -dgit clone https://github.com/datboip/StayBattle.git
cd staybattle
npm install
npm run devStayBattle works with zero config. If you want to tweak:
| Env var | Default | Effect |
|---|---|---|
STAYBATTLE_PORT |
3000 |
Port for the install script. |
STAYBATTLE_DIR |
~/staybattle |
Where SQLite + data live. |
STAYBATTLE_DB_DIR |
./data |
Where the SQLite DB lives (production override). |
STAYBATTLE_HTTPS |
(unset) | Set to true at build time (STAYBATTLE_HTTPS=true npm run build, or docker compose build --build-arg STAYBATTLE_HTTPS=true) when the app sits behind a TLS terminator (nginx/Caddy/Cloudflare). Enables upgrade-insecure-requests + HSTS and marks the sign-in cookie Secure (this last part is read at run time, so it also works as a container env var). Leave unset for plain-http LAN use: otherwise the page sticks on "Loadingβ¦" and, even if it loads, every action answers "Sign in first" because browsers drop Secure cookies over http. |
STAYBATTLE_DEMO_MODE |
(unset) | When true: skip Airbnb GraphQL availability calls, use pre-baked statuses (avoids rate-limiting). Used by the public demo. |
STAYBATTLE_OSRM_URL |
https://router.project-osrm.org |
OSRM server for drive-time routing on the "Nearby" pills. Point at a self-hosted OSRM when traffic outgrows the public demo. |
NEXT_ALLOWED_DEV_ORIGINS |
(unset) | Extra hostnames allowed to hit dev-mode HMR. Private RFC1918 IP ranges already match. |
There's no separate admin page because the person who installs it owns the box, and the organizer of each battle owns the battle. The organizer has everything they need inside the UI:
- Edit battle name, dates, deadline, must-have amenities
- Start the battle early (skip waiting for the deadline)
- Generate / regenerate the invite code
- Force-recheck availability for all listings
- Kick participants (with or without removing their votes)
- Override a stale availability status with a note
- Close + archive the battle to the trophy case
- Reset the whole battle and start fresh
Instance operators (whoever runs the box) also have a one-line CLI for DMCA takedowns: node scripts/admin/remove-url.mjs <url> "<reason>" β see SECURITY.md.
If you ever need server-wide config, edit env vars and re-run the install script. No separate dashboard, no separate login, no separate threat surface.
The dev server binds to all network interfaces. People on the same Wi-Fi hit http://<your-lan-ip>:3000.
For friends off your network:
- tailscale β install on each device, share the magic DNS name. Free for personal use, fully private.
- ngrok β
npx ngrok http 3000gives you a public URL. Anyone with it can sign up β protect with the invite code anyway. - Small VPS β see
Dockerfile. Anything that runs a container works.
Invite links use a URL fragment (#invite=ABCDEF) so the code never reaches server logs β your nginx access log can't accidentally retain the invite when someone clicks the link.
- Next.js 16 App Router (Turbopack)
- React 19
- SQLite via
better-sqlite3 - Tailwind 4
- Leaflet + OpenStreetMap (no API key)
- OSRM (
router.project-osrm.orgby default, configurable) for drive-time routing - cheerio for HTML parsing
- scrypt (Node built-in) for PIN hashing
- Vitest for the test suite
CI runs typecheck, tests, and build on every push and PR. A separate workflow publishes the multi-arch Docker image to GHCR on every push to main and on every tag. Dev workflows and the screenshot-regen recipe live in CONTRIBUTING.md.
StayBattle is built for small private groups. See PRIVACY.md for the full threat model + what's still soft.
- No analytics, no tracking pixels, no third-party JavaScript loaded into the page.
- One cookie: a same-site
staybattle_votercookie set on sign-in so the server-side gate can tell whether to render battle data (added 2026-05-27 to close a leak where anonymous visitors couldcurlthe page and get the invite code + every comment). Mirrored tolocalStoragefor the client UI. Cleared on sign-out. Not third-party. Not analytics. - Invite codes ride in the URL fragment (
#invite=β¦), not the query string β fragments never reach the server, so nginx access logs / Cloudflare logs / browser referer headers never capture them. - nginx logs use a scrubbed format that drops query strings entirely (belt-and-suspenders against the above).
- Past-battle archives are date-scrubbed to month/year and have the clickable Airbnb URL stripped β the trophy case can't pinpoint "the crew was at this exact rental from to " after the trip.
- No telemetry. Next.js telemetry is disabled in the Dockerfile.
- All data is in a single SQLite file. Delete it to nuke everything.
Outbound traffic, in full:
- When a user adds a URL: one HTTP request to
airbnb.comto fetch the listing page (skipped inSTAYBATTLE_DEMO_MODE=true). - When a user pins a place via address (drop-pin works without this): one HTTP request to
nominatim.openstreetmap.orgfor geocoding. - When a battle has reference places + listings with coordinates: one HTTP request to OSRM (
router.project-osrm.orgby default) per SSR to compute drive-time pills. - When the map is open: tile images from
*.tile.openstreetmap.org.
That's the entire list.
StayBattle is unaffiliated with Airbnb, Inc. "Airbnb" is a trademark of Airbnb, Inc.
This project is a decision-support tool that helps small groups organize their own discussions about Airbnb listings they're considering. The tool never replaces Airbnb's booking flow β every action links you back to Airbnb to actually book. There is no bulk scraping, no discovery / search functionality, no listing aggregation. The server fetches a single page only when a user pastes a URL they already have.
Each user is responsible for their own use of this software with respect to Airbnb's Terms of Service. The software is provided AS IS, without warranty of any kind, per AGPL v3.
Terms of service for users of the live demo at app.staybattle.com are in TOS.md. Privacy practices are in PRIVACY.md. Security policy + DMCA takedown procedure are in SECURITY.md.
If you're with Airbnb and want to talk β partnership, licensing, or anything in between β open a GitHub Private Security Advisory (use "Contact" as the title; the channel is private to the maintainer). Formal takedown requests use the same channel β full procedure in SECURITY.md.
StayBattle is AGPL v3 β see LICENSE. The spirit:
Use it freely. Fork it, run it for your crew, modify it. But if you improve it β share your improvements back. The whole point is that we all benefit from each other's work.
If you fork it, polish it, deploy it for your group: please open a PR with your improvements, or at least share them publicly under AGPL so others can pick them up. If you want to build something proprietary on top of it that you don't want to AGPL-license, open a GitHub Private Security Advisory (use "Licensing inquiry" as the title) instead of forking quietly.
See CONTRIBUTING.md for the dev workflow and PR checklist.
GNU Affero General Public License v3.0 Β© StayBattle contributors.
You can use, modify, and share this freely. If you distribute it or run a modified version as a network service, your modifications must be released under AGPL too. That's the whole rulebook.
Built one annoyed family vacation at a time.
Hope your crew picks the right house. ποΈ See you at the beach.





