Running on Vercel
The entrypoint, caches in /tmp, Redis for shared state, and Tour of the Day computed once per date.
The same code runs in the workspace and on Vercel. Three things make that work: an entrypoint Vercel recognizes, a writable place for caches, and a shared store for everything players share.
The entrypoint
Vercel looks for a Flask instance named app in app.py. The app's folder has one, which puts src/ on the import path and imports the real app. Dependencies come from pyproject.toml; vercel.json gives the function up to 60 seconds (enough to fetch a big band's show list from the archive) and leaves the tests out of the bundle.
Caches in /tmp, shared state in Redis
A Vercel function's file system is read-only except /tmp, so on Vercel the archive caches move there:
61_DEFAULT_DATA_DIR = "/tmp/terrapin-trail" if os.environ.get("VERCEL") else "data/.apps/terrapin-trail" 62DATA_DIR = Path(os.environ.get("TERRAPIN_TRAIL_DATA_DIR", _DEFAULT_DATA_DIR))
/tmp is per instance and disappears, which is fine for caches (the archive is the source) and wrong for anything shared. That's why boards, tags, tickets, share counts, the rule gallery and Tour of the Day all go through the store from chapter 11: with a Redis store attached to the Vercel project, every instance sees the same data.
Tour of the Day is computed once
The first request each day picks the tour, and the store remembers it for everyone:
497 def pick_daily(day: str) -> dict: 498 """Deterministic for a given date: a top band, then its show closest to 499 today's month and day in any year, then the tour around that show.""" 500 seed = int(hashlib.sha256(day.encode()).hexdigest(), 16) 501 bands = [b for b in archive.list_bands(data_dir)[:DAILY_BAND_POOL] if b["items"] >= 50] 502 mmdd = day[5:] 503 for k in range(len(bands)): 504 band = bands[(seed + k) % len(bands)] 505 shows = archive.load_shows(band["id"], data_dir) 506 nights = archive.concert_nights(shows) 507 if not nights: 508 continue 509 510 def distance(n: dict) -> int: 511 d = abs(int(n["ymd"][5:7]) * 31 + int(n["ymd"][8:10]) - (int(mmdd[:2]) * 31 + int(mmdd[3:]))) 512 return min(d, 372 - d) 513 514 best = min(distance(n) for n in nights) 515 candidates = [n for n in nights if distance(n) == best] 516 night = candidates[seed % len(candidates)] 517 year = int(night["ymd"][:4]) 518 name = band_name_for(band["id"]) 519 for t in register_tours(band["id"], name, year, archive.tours_for_year(shows, year)): 520 final = t["tag"] 521 if t["start"] <= night["ymd"] <= t["end"]: 522 code = DAILY_PRESETS[seed % len(DAILY_PRESETS)] 523 return { 524 "date": day, "tag": final, "dna": code, "rules": dna.label(code), "fullTag": tags.full_tag(final, code), 525 "band": name, "collection": band["id"], "year": year, "night": night["ymd"], 526 "venue": night["venue"], "city": night["city"], "onThisDay": night["ymd"][5:] == mmdd, 527 } 528 raise archive.ArchiveError("No tour of the day yet.") 529 530 @app.get("/api/daily") 531 def api_daily() -> Response: 532 """Today's shared tour: one tag, one rule set, one leaderboard for everyone."""
What happens without Redis
The app still runs and every page works. Scores, tags and share counts then live in each instance's /tmp, so two players can land on different instances and see different boards. Attach the store before sharing links publicly.
- app.py is Vercel's entrypoint; pyproject.toml lists the dependencies; vercel.json sets limits and exclusions.
- Caches go to /tmp on Vercel; shared state needs a Redis store attached to the project.
- Tour of the Day is decided once per date and cached in the store.