Zoom Keyhole
A small FastAPI service that shows who is in a Zoom room on one or more Miro boards. Room configuration (users, rooms, boards, role symbols) is read from a Google Sheet; Miro text widgets are updated to display live participants. The service listens for Zoom webhooks, periodically scans Miro boards, and updates text widgets to reflect live meeting state.
Requirements
Python 3.10–3.12 with Poetry
Make (to use the provided targets)
Optional: Docker (for local container runs) and Helm (for Kubernetes)
Local development
Clone and install dependencies using Poetry:
poetry install
Configure environment (recommended via
.envin the project root). See Environment variables below for details. The Makefile auto-loads.env.Activate the virtualenv when running commands manually:
poetry shell
Quick start
Prepare your Miro boards:
Add a text widget per Zoom room, with the room name and the literal text “Meeting ID:”. The service will insert the meeting ID as a clickable link and keep the participant list up to date.
Prepare your Google Sheet:
Create a sheet with tabs named: Users, Rooms, Boards, Role symbols. Populate them according to your conventions. The service periodically reloads these tabs.
Run locally:
Create a .env file (or export the variables in your shell) with the required environment variables listed below. The Makefile will auto-load .env.
Start the service:
make run
# or:
uvicorn src.zoom_keyhole.main:APP --host 0.0.0.0 --port 8000 --loop asyncio --reload
The service exposes its routes under the optional ROOT_PATH (default is set by the Helm chart).
Run locally
# Using the Makefile (auto-reload)
poetry run make run
# Or directly with uvicorn
poetry run uvicorn src.zoom_keyhole.main:APP \
--host 0.0.0.0 --port 8000 --loop asyncio --reload
Once running, the service listens on http://0.0.0.0:8000 (or the port you
choose). If deploying behind a reverse proxy, set ROOT_PATH accordingly.
Run with Docker
# Build a local image with an empty ROOT_PATH (good for localhost)
make docker-build-local
# Run the container, passing variables from .env if present
make run-docker PORT=8000
Linting
poetry run make python-lint
This runs isort, black (check), flake8, and pylint. Reports are written under
build/reports/ where applicable.
Testing
poetry run make python-test
This runs the pytest suite with coverage. Outputs include JUnit XML and
coverage HTML in build/reports/code-coverage/index.html.
Environment variables
Required:
ZOOM_KEYHOLE_MIRO_API_TOKEN: Miro API token. For multiple tokens, use a semicolon-separated list locally; when set via Helm/values, use colon-separated.
ZOOM_KEYHOLE_ZOOM_CLIENT_ID: Zoom OAuth client ID.
ZOOM_KEYHOLE_ZOOM_CLIENT_SECRET: Zoom OAuth client secret.
ZOOM_KEYHOLE_ZOOM_ACCOUNT_ID: Zoom account ID for server-to-server OAuth.
ZOOM_KEYHOLE_WEBHOOK_SECRET_TOKEN: Secret used to validate Zoom webhook URL challenges and payloads.
ZOOM_KEYHOLE_GSHEET_ID: Google Sheet ID containing configuration.
One of:
GOOGLE_SERVICE_ACCOUNT_KEY_B64: Base64-encoded Google service account JSON (recommended), or
ZOOM_KEYHOLE_GSHEET_KEY: Google Sheets API key (fallback).
Optional (sensible defaults exist):
ROOT_PATH: URL prefix when running behind a reverse proxy (Helm defaults to /ska/zoom-keyhole).
ENABLE_DEBUG_METHODS: Enable debug/admin routes (default: true). Disable in production.
ZOOM_KEYHOLE_DEBUG_TOKEN: Optional token required to access debug endpoints when set. If provided, clients must present either
Authorization: Bearer <token>orX-Debug-Token: <token>.ZOOM_KEYHOLE_FILE_LOGGER: Enable file logging when set to a non-zero value.
Scan/refresh periods (seconds): ZOOM_ROOM_SCAN_PERIOD, MIRO_BOARD_SCAN_PERIOD, MIRO_BOARD_UPDATE_PERIOD, GSHEET_USERS_SCAN_PERIOD, GSHEET_BOARDS_SCAN_PERIOD, GSHEET_ROOMS_SCAN_PERIOD, GSHEET_ROLES_SCAN_PERIOD.
Project layout
src/zoom_keyhole/ FastAPI app and domain logic
main.py Uvicorn/ASGI entrypoint
lifespan.py App startup/shutdown lifecycle
routes/ API + debug endpoints (webhook, metrics, etc.)
gsheet/ Google Sheets load/write helpers
miro_* Miro discovery/update utilities
models/ Pydantic models and data shapes
tests/ Pytest suite and examples
charts/ Helm chart for Kubernetes deployment
docs/ Sphinx documentation sources
Makefile Dev shortcuts (run, lint, test, docker helpers)
Kubernetes (Helm)
The included chart deploys the service and sets the default ingress path to /ska/zoom-keyhole. Install by providing sensitive values at install time:
helm install zoom ./charts/ska-zoom-keyhole --values values.yaml
Minimal values.yaml example (redacted):
---
env:
ZOOM_KEYHOLE_MIRO_API_TOKEN: "<token or token:token2>"
ZOOM_KEYHOLE_ZOOM_CLIENT_ID: "<client id>"
ZOOM_KEYHOLE_ZOOM_CLIENT_SECRET: "<client secret>"
ZOOM_KEYHOLE_ZOOM_ACCOUNT_ID: "<account id>"
ZOOM_KEYHOLE_WEBHOOK_SECRET_TOKEN: "<webhook secret>"
ZOOM_KEYHOLE_GSHEET_ID: "<sheet id>"
# Choose one auth method for Google Sheets:
GOOGLE_SERVICE_ACCOUNT_KEY_B64: "<base64 service account json>"
# or, as a fallback:
ZOOM_KEYHOLE_GSHEET_KEY: "<api key>"
# Optional
ROOT_PATH: "/ska/zoom-keyhole"
In environments using an ingress controller (e.g. NGINX), the default path is /ska/zoom-keyhole. Adjust ROOT_PATH and ingress settings as needed in the chart values.
Debug endpoints
Debug/admin endpoints are mounted under the /debug prefix when ENABLE_DEBUG_METHODS=true (default for local dev). In production, set ENABLE_DEBUG_METHODS=false to completely disable these endpoints (404). Optionally, set ZOOM_KEYHOLE_DEBUG_TOKEN to require a token for access even in non-prod.
Enable and protect debug endpoints:
export ENABLE_DEBUG_METHODS=true
export ZOOM_KEYHOLE_DEBUG_TOKEN=supersecret
Example calls:
# List config keys
curl -H "Authorization: Bearer supersecret" \
http://localhost:8000/debug/config/keys
# View users config
curl -H "X-Debug-Token: supersecret" \
http://localhost:8000/debug/users/config
# Reset Zoom rooms config
curl -X PATCH -H "Authorization: Bearer supersecret" \
http://localhost:8000/debug/zoom_rooms/config/reset
Note: Some debug actions can be disruptive (e.g., forcing scans/updates). Use them with care and avoid exposing them publicly.
Security tips
Do not commit secrets.
.envis ignored and intended for local use.Prefer
GOOGLE_SERVICE_ACCOUNT_KEY_B64for Sheets authentication (base64-encoded service account JSON). Locally, multiple Miro tokens can be;-separated; in Kubernetes/Helm values use:-separated tokens.
Documentation
Full documentation (architecture, configuration, operations) is available on Read the Docs:
https://zoom-keyhole.readthedocs.io/en/latest/
For chart defaults, see charts/ska-zoom-keyhole/values.yaml in this repository.
CI Version Check
The pipeline includes a pre-lint “verify” stage that checks version consistency across code, docs, Helm chart, and Dockerfile.
It runs
scripts/check_versions.pyand fails the pipeline if mismatches are found.Run locally with:
make check-version.
Disabled Miro boards
Boards are declared in the Google Sheet (Boards tab) with
name,id, andenabled.When a board’s
enabledis set tofalsein configuration:All discovered widgets on that board are marked in-place with “(Board updates disabled)”.
Widgets are locked (
editable: false) so their content is clearly frozen.The service does not update those widgets further while the board remains disabled.
Re-enable the board by setting
enabledtotrue; normal updates resume automatically on the next cycle.