Skip to content

Latest commit

Β 

History

96 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

StayBattle

StayBattle

Settle the vacation argument. Pit Airbnb listings against each other. Rate, argue in the comments, settle it on the map.

Latest release CI License: AGPL v3 Made with Next.js Self-hosted No tracking

🌐 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.


🌐 Try the live demo

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.


60-second install

curl -fsSL https://raw.githubusercontent.com/datboip/StayBattle/main/install.sh | sh

That'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 track main. The one-liner above grabs whatever's on main right 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.sh line-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.yml and Dockerfile before docker-compose-up. Both are short.

  • npm install is 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, not npm install in production. ci reads package-lock.json exactly and refuses to install anything not pinned. npm install will happily update versions on you.
    • npm audit after install to flag known CVEs. Not exhaustive, but catches the obvious stuff.
    • npm install --ignore-scripts skips postinstall scripts β€” 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 -g unless 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 .env files 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.

Letting your crew actually click the link

localhost:3000 only 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 addr on Linux Β· ipconfig on Windows Β· System Settings β†’ Network on Mac), then send your crew http://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 a https://random.trycloudflare.com URL 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.

Why this exists

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.

What it looks like

Voting grid with ranked listings, 1–5 rating sliders, must-haves checklist, and BOOKED stamp on a booked listing Battle header with trip dates, invite code panel, and crew list
The roster. Every submission ranked by mean rating, with status-colored availability badges, prices, must-haves checklist, "Nearby" drive-time pills to your pinned places, and a one-click path to verify on Airbnb. Booked listings get a full-card BOOKED rubber stamp. The battle. Trip dates + invite code + crew list. Organizer can re-check all dates, set must-have amenities, close the battle, or start fresh.
Map of Orlando with status-colored listing pins and category-colored reference places, plus filter chips at the bottom Swipe-through review mode with the large 1–5 rating slider (Nope Β· Meh Β· OK Β· Like Β· Love)
The map. Every candidate as a status-colored teardrop (teal=available, rose-with-βœ•=booked, amber=unknown). Drop reference pins for theme parks, restaurants, airports, the wedding venue β€” categorized so the map color-codes them, with filter chips to hide categories. OpenStreetMap tiles, no API keys. Swipe-through review. One-card-at-a-time mode. Big slider with the Nope Β· Meh Β· OK Β· Like Β· Love labels. Keyboard shortcuts (1–5 set the rating, ← / β†’ navigate, Esc bails) on a laptop, swipe on a phone.
StayBattle review mode on an iPhone with the rating slider visible Trophy case showing the gold/silver/bronze podium for past battles
Mobile-first. Sign-in, voting, comments, drop-pin, swipe-review β€” all responsive. Your crew uses their phones, this works on their phones. Trophy case. Past battles get archived as gold/silver/bronze podiums. Dates round to month-only and the clickable Airbnb URL is stripped on archive β€” the case is a memento, not a permanent record of where and when the crew was.

What it does

  1. Organizer sets up a battle: trip name, dates, submission deadline, optional must-have amenities (wifi, pool, parking, etc.).
  2. Crew joins with an invite code, each with their own name + PIN.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. 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.

Identity = name + PIN, no accounts

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.

Self-host vs. live demo

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

Other ways to install

Docker Compose (recommended for tinkering)

git clone https://github.com/datboip/StayBattle.git
cd staybattle
docker compose up -d

From source (recommended for hacking)

git clone https://github.com/datboip/StayBattle.git
cd staybattle
npm install
npm run dev

Configuration

StayBattle 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.

Admin? You.

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.

Sharing with your crew

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 3000 gives 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.

Stack

  • Next.js 16 App Router (Turbopack)
  • React 19
  • SQLite via better-sqlite3
  • Tailwind 4
  • Leaflet + OpenStreetMap (no API key)
  • OSRM (router.project-osrm.org by 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.

Privacy

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_voter cookie 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 could curl the page and get the invite code + every comment). Mirrored to localStorage for 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.com to fetch the listing page (skipped in STAYBATTLE_DEMO_MODE=true).
  • When a user pins a place via address (drop-pin works without this): one HTTP request to nominatim.openstreetmap.org for geocoding.
  • When a battle has reference places + listings with coordinates: one HTTP request to OSRM (router.project-osrm.org by default) per SSR to compute drive-time pills.
  • When the map is open: tile images from *.tile.openstreetmap.org.

That's the entire list.

Legal

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.

Contributing

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.

License

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.

About

Self-hosted vacation Airbnb voting app. Drop URLs, vote together, decide together. No accounts, no tracking.

Topics

Resources

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages