Live wait-time and status dashboard for public HeyTea locations.
I wanted to go to HeyTea with my friends and did not want to be surprised by the wait time. Ellie said it would be nice to have an app that just checked the wait time, so I decompiled the Android app and found the public endpoints that power wait-time data.
The first version was a singleton app for the Downtown Metreon location in San Francisco. Later, a friend was leaving for the UK, so I extended it to work for every location I could find, including China. China required decompiling the China version of the app too, then stitching both endpoint families into one service.
The result is a small public service for nerds who like HeyTea.
The public API uses stable location slugs and never accepts or returns upstream shop IDs. Status, wait, notice, history, and stream endpoints require an explicit location slug.
Live values are considered fresh until the next expected poll: ttl_seconds = max(0, observed_at + poll_interval - now). Poller commits normalized data to Postgres, sends a Postgres notification, and the API broadcasts status.updated to connected SSE clients.
heytea-pollerdiscovers locations and polls public HeyTea app endpoints for wait times and notices.- Postgres + TimescaleDB store the normalized location catalog, current status, and wait-time history.
heytea-apiserves JSON, OpenAPI, health/readiness, metrics, and location-specific SSE streams.heytea-siteserves the finder, location pages, docs shell, status page, and discovery files.heytea-mcpexposes anonymous HTTP MCP tools for agents.heytea-ssh-tuilets anyone runssh heytea.devfor a readonly terminal view.- The Rust SDK crate and
heyteaCLI make the public API scriptable. - Postgres notifications fan out live updates without Redis.
- NixOS, Caddy, deploy-rs, agenix, and Tailscale run the minimal production host.
- Caddy serves HTTP/1.1, HTTP/2, HTTP/3/QUIC, TLS 1.2, and TLS 1.3.
Base URL: https://api.heytea.dev
GET /locationsGET /locations/{slug}GET /locations/{slug}/statusGET /locations/{slug}/wait-timeGET /locations/{slug}/noticeGET /locations/{slug}/closing-noticeGET /locations/{slug}/history?range=todayGET /locations/{slug}/streamGET /healthzGET /readyzGET /metricsGET /openapi.json
No /v1, no upstream shop IDs, no menu endpoints, and no default-location aliases.
nix develop
cargo check
pnpm build
nix build .#heytea-api .#heytea-poller .#heytea-mcp .#heytea-ssh-tui .#heytea-cli .#heytea-assets .#heytea-site-assets .#heytea-site .#heytea-migrations --no-linkGitHub Actions run CI on pull requests and pushes to main, configure Cloudflare, deploy production after successful CI on main, publish new Rust SDK versions, and create Linux CLI releases automatically. See docs/ci-cd.md for required GitHub secrets, environments, Tailscale, Cloudflare, and crates.io setup.
Production uses a normal DigitalOcean Ubuntu droplet, nixos-anywhere for the initial NixOS install, and deploy-rs for updates. See docs/deploy-nixos-digitalocean.md.