This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Mobility Feed API — a FastAPI service that serves open mobility data (GTFS, GTFS-RT, GBFS feeds) from across the world, backing mobilitydatabase.org.
Three main runtime components:
api/— the FastAPI service (Feeds API + User Service), spec-first via OpenAPI Generator.functions-python/— Google Cloud Functions (Gen2) for background processing: batch dataset downloads, GBFS/GTFS validation, BigQuery ingestion, notifications, reverse geolocation, etc.- PostgreSQL + PostGIS, schema-migrated with Liquibase (
liquibase/), geospatial support for location-based feed queries.
infra/ holds Terraform for GCP infra (dev/qa/prod). integration-tests/ is a separate Python suite that hits a live deployed API.
- Never commit, push, or comment on PRs unless explicitly asked. Prepare changes and leave them for review.
- Preserve backward compatibility on the public surface.
docs/DatabaseCatalogAPI.yamlanddocs/UserServiceAPI.yamlare consumed by real external clients (see README's access-token flow), and the Liquibase-managed DB schema is relied on by both the API andfunctions-python/. If a change would break an existing endpoint, response shape, or column, flag it explicitly instead of making it silently — don't assume a breaking change is fine just because tests still pass.
This repo is spec-first. Never hand-edit generated code:
docs/DatabaseCatalogAPI.yaml→ generatesapi/src/feeds_gen/viascripts/api-gen.sh(configscripts/gen-config.yaml).docs/UserServiceAPI.yaml→ generatesapi/src/user_service_gen/viascripts/api-user-service-gen.sh(configscripts/gen-user-service-config.yaml).liquibase/changelog.xml(+changes/) → generatesapi/src/shared/database_gen/sqlacodegen_models.py(SQLAlchemy ORM) viascripts/db-gen.sh.liquibase/changelog_user.xml(+changes_user/) → generatesapi/src/shared/users_database_gen/viascripts/db-gen-user.sh.
Workflow when changing the API schema or DB schema:
- Edit the OpenAPI yaml (
docs/DatabaseCatalogAPI.yamlordocs/UserServiceAPI.yaml) and/or add a Liquibase changeset. - Re-run the matching
scripts/*-gen.shscript. - Implement/adjust business logic in
feeds/impl/oruser_service/impl/— never in the_genfolders.
Generated code (feeds_gen/, user_service_gen/, shared/database_gen/, shared/users_database_gen/) is excluded from black/flake8 (see api/pyproject.toml, api/.flake8) and should be treated as read-only.
All commands assume repo root unless noted.
# One-time setup
scripts/setup-openapi-generator.sh
cd api && pip3 install -r requirements.txt -r requirements_dev.txt
# Local Postgres + Liquibase migrations
docker-compose --env-file ./config/.env.local up -d --force-recreate
# Re-init local env after checking out a branch: rebuilds main+test DBs, regenerates
# SQLAlchemy models, FastAPI stubs (Feeds + User Service + Operations API), and
# re-symlinks all functions' shared code. Prefer this over the individual steps below
# whenever the branch you just checked out changed the schema/spec files.
scripts/init-local-folder.sh # migrations only, no data
scripts/init-local-folder.sh --populate-db # also pulls latest CSV from GCS into the main DB
# Regenerate stubs individually (after schema changes)
scripts/api-gen.sh # Feeds API -> feeds_gen
scripts/api-user-service-gen.sh # User Service API -> user_service_gen
scripts/db-gen.sh # DB schema -> database_gen
scripts/db-gen-user.sh # User DB schema -> users_database_gen
# Reset local DB and (re)populate from a catalog sources.csv (destructive — wipes local DB)
./scripts/populate-db.sh <path to sources.csv>
./scripts/docker-localdb-rebuild-data.sh --populate-db
./scripts/docker-localdb-rebuild-data.sh --populate-db --populate-test-data # also loads dataset entities
./scripts/docker-localdb-rebuild-data.sh --use-test-db # reset the empty test DB before running tests
# Run the API locally (Swagger UI at http://localhost:8080/docs/)
scripts/api-start.sh
# Lint (flake8 + black) across api/, functions-python/, integration-tests/
scripts/lint-tests.sh
pre-commit install && pre-commit run --all-files # same check, runs lint-tests.sh as a hook
# Tests
scripts/api-tests.sh # all `api/` tests + coverage (branch coverage must be >= 80%)
scripts/api-tests.sh --test_file <path/to/test_file.py>
scripts/api-tests.sh --folder functions-python # every function under functions-python/ with a tests/ dir
scripts/api-tests.sh --folder functions-python/<function_name> # a single Cloud Function
scripts/api-tests.sh --html_report # also emit HTML coverage under scripts/coverage_reports
# Run/test a single Cloud Function locally
scripts/function-python-setup.sh --function_name <name> # symlinks shared code (see below) into src/shared and tests/test_shared
scripts/function-python-run.sh --function_name <name> # installs deps into a venv, runs via functions-framework --debug
scripts/function-python-build.sh --function_name <name> # zips into .dist/ for deployapi-tests.sh copies config/.env.local to .env before running, so local env vars match the docker-compose Postgres instance.
feeds/impl/*_api_impl.py— one impl class per resource (FeedsApiImpl,DatasetsApiImpl,SearchApiImpl,MetadataApiImpl,LicensesApiImpl), each subclassing a generatedBase*Apifromfeeds_gen.user_service/impl/*_api_impl.pymirrors this forUsersApiImpl,NotificationsApiImpl,SubscriptionsApiImpl.main.py— a single FastAPIappmounts routers from bothfeeds_genanduser_service_gen; user-service routes are wrapped withoffload_blocking_routes()(utils/route_offload.py) since those impls make blocking DB/Brevo HTTP calls. CORS is configured via raw Starlette middleware, not FastAPI's, due to a known FastAPI CORS bug — don't "simplify" this.shared/— code shared between the API and the Cloud Functions:database_gen/sqlacodegen_models.py— generated ORM models;Feedis a polymorphic base withGtfsFeed,GbfsFeed,GtfsRTFeedsubclasses.db_models/— hand-written business-logic wrappers per entity (feed_impl.py,gtfs_feed_impl.py, etc.) layered over the generated models.database/—Database,with_db_sessiondecorator (use this for any DB-touching function),users_database.py.feed_filters/— query-parameter filter builders per feed type.common/—error_handling.py,logging_utils.py,config_reader.py,gcp_utils.py,rate_limiter.py,brevo.py(email),entity_type_enum.py.notifications/— notification event service + Brevo sender.
middleware/request_context.py— the de facto auth layer (no file is literally namedauth.py): decodes the GCP IAP JWT (x-goog-iap-jwt-assertionheader) viadecode_jwt/resolve_google_public_keys, exposesextract_user_id,is_user_email_restricted(), and builds aRequestContext.request_context_middleware.pywires this in as ASGI middleware and logs API access.scripts/(underapi/src/, distinct from repo-rootscripts/) — DB population scripts (populate_db*.py),gbfs_utils/(also defines the supported GBFS versions map referenced from the README).
- Raise
shared.common.error_handling.InternalHTTPExceptioninternally; convert toHTTPExceptionviafeeds.impl.error_handling.convert_exception(). - Keep error message strings as
Finals inapi/src/feeds/impl/error_handling.pyso they're reusable and easy to locate. - Responses follow
{"details": "the error message"}.
Each function directory follows the same shape:
functions-python/<name>/
src/main.py # @functions_framework.http (or event-triggered) entry point
src/shared/ # gitignored symlinks into api/src/shared/*, created by function-python-setup.sh
tests/ # pytest; tests/test_shared/ is also symlinked (test_utils, etc.)
function_config.json # entry_point, runtime, memory, timeout, trigger, include_folders/include_api_folders, secrets, scaling
requirements.txt / requirements_dev.txt
function_config.json's include_folders (from functions-python/) and include_api_folders (from api/src/shared/) tell function-python-setup.sh what to symlink in — that's how functions share DB models and helpers with the API without a package dependency. Non-deployed shared support code (e.g. helpers/, test_utils/) uses test_config.json instead.
Functions have no direct interdependencies on each other; shared logic lives in functions-python/helpers, functions-python/dataset_service, functions-python/test_utils, or api/src/shared/*.
Gotcha: functions-python/database_gen at the top level is a stray, gitignored, stale copy — it is not the symlink target and can be out of sync with the real source of truth, api/src/shared/database_gen/sqlacodegen_models.py. Don't treat it as canonical.
Liquibase changelogs live in liquibase/ (changelog.xml + changes/ for the main schema, changelog_user.xml + changes_user/ for the user-service schema). docker-compose.yaml runs liquibase/liquibase-test/liquibase-user/liquibase-user-test services to apply them to local Postgres containers before db-gen.sh/db-gen-user.sh regenerate the SQLAlchemy models.
integration-tests/ is a separate suite that runs against a live API URL (not the local unit-test DB). New tests: add a class under endpoints/ inheriting from the base IntegrationTests class, prefix methods with test_. Run via ./scripts/integration-tests.sh -u <API URL> -f <data file> [-c <ClassName1,ClassName2>].