Automated golf tee-time booking. A Python FastAPI service stores encrypted credentials and booking sessions in Redis; the wanted API registers persisted booking requests (one-shot by date, or recurring by day-of-week); a daily worker processes due requests, books a matching slot, records the outcome, and optionally sends a Twilio SMS. A React web UI drives the wanted workflow in the browser, and an optional local MCP server exposes the booking operations to Claude Desktop / MetaMCP.
| Component | Path | Purpose |
|---|---|---|
| API service | api/ |
FastAPI: login, find/book tee times, manage partners, wanted requests |
| Worker | api/app/cli/worker.py |
Processes due wanted requests (run as a CronJob) |
| Web UI | web/ |
Vite + React SPA for the wanted tee-times workflow (nginx-served) |
| MCP server | mcp/ |
Local stdio MCP client of the REST API |
| API Helm chart | charts/tee-sniper-api/ |
Deploys API + Redis + opt-in worker CronJob |
| Web Helm chart | charts/tee-sniper-web/ |
Deploys the web UI with path-based ingress (/api/* → API, /* → web) |
- A client logs in via
POST /api/login; credentials are encrypted (AES-256-GCM) and a session is stored in Redis with a sliding TTL. - Tee times are searched and booked through the booking endpoints.
- For unattended booking, register a wanted request:
POST /api/wanted?kind=one_shot|recurring— create a requestGET /api/wanted[?status=pending|booked|expired|disabled]— listGET /api/wanted/{slot_id}— fetch one (incl. attempt history)PATCH /api/wanted/{slot_id}— update window/partners/notify or disableDELETE /api/wanted/{slot_id}— remove
- The worker (
python -m app.cli.worker, deployed as the opt-inworkerHelm CronJob) runs daily, books due requests, records the outcome, and optionally sends a Twilio SMS. This worker + thewantedAPI replace the retired Go CLI and its external cron.
All endpoints except /health and /api/login require an
Authorization: Bearer <token> header obtained from the login endpoint.
The API is configured via environment variables prefixed TSA_
(see api/app/config.py and .env.example):
| Variable | Description | Required |
|---|---|---|
TSA_SHARED_SECRET |
Credential encryption secret | Yes |
TSA_BASE_URL |
Booking site base URL | Yes |
TSA_REDIS_URL |
Redis connection URL | Yes (prod) |
TSA_TWILIO_ACCOUNT_SID |
Twilio SID (SMS notifications) | No |
TSA_TWILIO_AUTH_TOKEN |
Twilio auth token | No |
TSA_TWILIO_FROM_NUMBER |
Twilio sender number | No |
TSA_LOG_LEVEL |
DEBUG/INFO/WARNING/ERROR |
No (INFO) |
TSA_LOG_FORMAT |
json or text |
No (json) |
TSA_REQUIRE_REDIS |
Test-only; 1 makes session integration tests fail loudly instead of skipping when Redis is down (set in CI) |
No |
# API + web + Redis via Docker Compose (dev override auto-loaded).
# API on :8000, web UI on :8080, Redis on :6379.
docker compose up
# Or run the API directly
cd api && .venv/bin/python -m uvicorn app.main:app --reload
# Run the web UI dev server (proxies /api → http://localhost:8000)
cd web && npm install && npm run dev # serves on :5173
# Run the wanted-slot worker once
cd api && .venv/bin/python -m app.cli.workerThe web UI reads its API base URL from window.__TSA_CONFIG__.apiBaseUrl,
rewritten from the API_BASE_URL env var at container start (empty → relative
/api/*). See web/README.md for details.
Two Helm charts deploy the stack. Both pin images by tag (the API chart's
schema rejects tag=latest); set image.tag / api.image.tag to a released
version.
Deploys the API, a bundled Redis (Bitnami subchart), and an opt-in worker
CronJob for wanted processing. Requires helm dep update first to pull the
Redis dependency.
helm dep update charts/tee-sniper-api
helm template testrel charts/tee-sniper-api -f charts/tee-sniper-api/values-dev.yaml # render
helm upgrade --install tee-sniper-api charts/tee-sniper-api \
-f charts/tee-sniper-api/values-prod.yamlKey values (see charts/tee-sniper-api/values.yaml, values-dev.yaml,
values-prod.yaml):
| Value | Default | Purpose |
|---|---|---|
api.image.tag |
.Chart.AppVersion |
API image tag (required, not latest) |
api.existingSecret |
tee-sniper-api |
Secret with TSA_* env (shared secret, base URL, etc.) |
worker.enabled |
false |
Enable the daily wanted worker CronJob |
worker.schedule |
30 6 * * * |
Worker cron schedule |
worker.twilio.enabled |
false |
Mount Twilio creds for SMS notifications |
redis.enabled |
true |
Deploy the bundled Redis subchart |
networkPolicy.enabled |
false |
Restrict API ingress to labelled clients |
When worker.enabled=true you must also have redis.enabled=true or supply
TSA_REDIS_URL via the chart secret — otherwise the worker connects to
localhost and fails.
Deploys the nginx-served React UI with a path-based ingress that routes
/api/* to the API service and /* to the web service on the same host.
helm template testrel charts/tee-sniper-web # render
helm upgrade --install tee-sniper-web charts/tee-sniper-web \
--set ingress.host=tee-sniper.example.comKey values (see charts/tee-sniper-web/values.yaml):
| Value | Default | Purpose |
|---|---|---|
image.tag |
.Chart.AppVersion |
Web image tag |
apiBaseUrl |
"" |
Browser API base; empty → relative /api/* (same-host ingress) |
ingress.enabled |
true |
Create the path-split ingress |
ingress.host |
tee-sniper.example.com |
Ingress hostname |
ingress.apiServiceName |
tee-sniper-api |
Service to route /api/* to |
ingress.tls.enabled |
false |
Enable TLS (set ingress.tls.secretName) |
A local stdio MCP server proxies the booking API to Claude Desktop /
MetaMCP. See mcp/README.md for install, configuration, and the tool
reference.
# Python API tests
cd api && .venv/bin/python -m pytest tests/ -v
# Web tests (Vitest + RTL + MSW)
cd web && npm install && npm test && npm run lint && npm run build
# MCP tests
cd mcp && uv sync --all-extras --dev && uv run pytest
# Session integration tests need a real Redis (skip silently without one):
docker run -d -p 6379:6379 redis
cd api && .venv/bin/python -m pytest tests/test_session_integration.py.github/workflows/api-build.yml— API build/test (provisions Redis, setsTSA_REQUIRE_REDIS=1)..github/workflows/web-build.yml— Web lint/test/build, then a Docker image build on PRs/pushes (built only, not pushed)..github/workflows/mcp-build.yml— MCP build/test..github/workflows/helm-chart.yml— Helm chart lint +kubeconformvalidation of the API chart's dev/prod values (and asserts the schema rejectstag=latest)..github/workflows/release.yml— onv*.*.*tags: builds and pushes the API Docker image (ghcr.io/<repo>-api), the web Docker image (ghcr.io/<repo>-web), the MCP Docker image (ghcr.io/<repo>-mcp), and the MCP wheel + sdist as release assets.
See LICENSE.