A FastAPI application demonstrating Firebase Authentication, Firestore CRUD operations, and modern Python development workflow using uv (dependency & virtualenv manager) and just (task runner).
Python logo from python.org. Trademark of the Python Software Foundation.
- Layered middleware architecture with security headers, CORS, request IDs, and structured access logs via
fastapi-request-observability - Request-scoped logging with incoming W3C Trace Context correlation metadata
- RFC 9457 Problem Details for FastAPI error responses, including field-level validation errors
- JSON and CBOR request/response content negotiation using
Content-TypeandAccept - Cursor-based pagination with RFC 8288 Link headers
- OpenAPI 3.1 documentation with Swagger UI and ReDoc
- Firebase Authentication with ID token verification and revocation checks
- Firestore persistence with async transactional operations
- Health check endpoint (
/health) for liveness probes
- Use plural nouns for collections (
/items, not/item) - Avoid verbs in URIs; let HTTP methods convey the action
- Return resources directly without wrapper envelopes
- Use
snake_caseconsistently for public JSON and CBOR properties, request parameters, and persisted Firestore fields
| Method | Purpose | Success Status |
|---|---|---|
| GET | Retrieve resource(s) | 200 OK |
| POST | Process input and return a computed representation | 200 OK |
| POST | Create a resource | 201 Created; persistent resources include a Location header |
| PATCH | Partial update | 200 OK |
| DELETE | Remove a resource | 204 No Content |
Errors follow RFC 9457 Problem Details and honor content negotiation:
{
"title": "Not Found",
"status": 404,
"detail": "Profile not found"
}Validation errors (422) include detailed field locations:
{
"title": "Unprocessable Entity",
"status": 422,
"detail": "validation failed",
"errors": [
{"location": "body.email", "message": "value is not a valid email address", "value": "invalid"}
]
}Modeled responses advertise their standalone JSON Schema through an RFC 8288
Link: </schemas/Model.json>; rel="describedBy" header. $schema belongs to the schema document itself, not to each
API response instance.
- API responses with a body default to
application/json. CBOR is optional and selected only by an explicitAccept: application/cbor; wildcards and equal quality values keep JSON. - An explicit
Acceptvalue that excludes every supported success representation returns 406 before a representation-bearing endpoint executes. A 204 response has no representation, soAcceptdoes not gate it. - Schema discovery returns only
application/schema+json. Strict clients must accept that media type or a matching wildcard;application/jsondoes not match a distinct+jsonsubtype. - Errors keep their original status. They use RFC 9457
application/problem+jsonby default, or registeredapplication/cborwhen explicitly preferred. Unsupported error preferences fall back to JSON; the unregisteredapplication/problem+cbormedia type is not implemented. - JSON and CBOR request bodies are selected independently with
Content-Type. /openapi.json,/api-docs, and/api-redocare fixed FastAPI documentation assets outside optional CBOR negotiation.
- Cursor-based tokens for stability
- Links provided via HTTP
Linkheader per RFC 8288
- Python 3.14+
- uv package manager (the minimum supported version is enforced in both project manifests)
- just command runner
- Firebase project with Authentication and Firestore enabled
- Firebase CLI for emulators and Functions deployment
git clone <repository-url>
cd fastapi-playground
cp .env.example .env
# Set FIREBASE_PROJECT_ID in .env
just install # Install dependencies via uv
just serve # Start dev server at http://127.0.0.1:8080Then visit:
http://localhost:8080/health- service health probehttp://localhost:8080/api-docs- interactive API explorer (Swagger UI)http://localhost:8080/api-redoc- API documentation (ReDoc)
Sample request:
curl -s localhost:8080/health | jqCopy .env.example to .env and customize:
cp .env.example .env| Variable | Description | Default |
|---|---|---|
PORT |
Server listen port | 8080 |
ENVIRONMENT |
Runtime mode; production enables HSTS and strips 5xx response extensions |
production |
LOG_LEVEL |
Application log verbosity (DEBUG, INFO, WARNING, ERROR, or CRITICAL) |
INFO |
FIREBASE_PROJECT_ID |
Firebase/GCP project ID | - |
FIRESTORE_DATABASE |
Firestore database ID | (default) |
GOOGLE_APPLICATION_CREDENTIALS |
Service account JSON path (local dev) | - |
CORS_ORIGINS |
JSON array or comma-separated allowed origins | - |
MAX_REQUEST_SIZE_BYTES |
Request body size limit | 1000000 |
The workspace MCP configurations in .vscode/mcp.json and .codex/config.toml use the Firebase CLI server and the
managed Cloud Logging server. Firebase uses the credentials available to the Firebase CLI. Cloud Logging reads the
quota project and a short-lived Application Default Credentials access token from the shell environment.
First align the gcloud default project and the Application Default Credentials quota project:
gcloud config set project PROJECT_ID
gcloud auth application-default login
gcloud auth application-default set-quota-project PROJECT_IDFor zsh, add these public, secret-free definitions to ~/.zshrc:
export GOOGLE_CLOUD_PROJECT="PROJECT_ID"
export GOOGLE_CLOUD_ACCESS_TOKEN="$(gcloud auth application-default print-access-token)"Run source ~/.zshrc, then restart VS Code or Codex so the client inherits the variables. The access token is
short-lived; opening a new shell regenerates it, while a long-running client must be restarted after a refresh. Never
commit the expanded token. Keep this convenience setup to a trusted workstation because child processes inherit the
token. See GCP.md for IAM and troubleshooting details.
.agents/skills/ Six portable project workflows with Codex UI metadata
.github/agents/ Evidence-based security review profile for GitHub Copilot
app/
main.py # FastAPI composition, lifespan, and outer ASGI middleware
dependencies.py # Dependency injection (CurrentUser, ProfileServiceDependency)
api/ # API route handlers
health.py # Health check endpoint
hello.py # Hello greeting endpoints
items.py # Items with pagination
profile.py # Profile CRUD (Firebase Auth protected)
auth/ # Firebase authentication
firebase.py # Token verification, FirebaseUser
core/ # Configuration and infrastructure
config.py # Settings class (pydantic-settings)
logging.py # Structured JSON logging configuration
firebase.py # Firebase Admin SDK and async Firestore client
exception_handler.py # RFC 9457 Problem Details
content_negotiation.py # RFC 9110 media-type selection
cbor.py # CBOR request and response adaptation
openapi.py # Reusable response contract metadata
schema_links.py # RFC 8288 schema links
validation.py # Validation error formatting and redaction
exceptions/ # Domain exceptions using fastapi-problem
profile.py # ProfileNotFoundError, ProfileAlreadyExistsError
middleware/ # ASGI middleware stack
body_limit.py # Request size guard (413 on oversized)
security.py # Security headers (HSTS, X-Frame-Options)
models/ # Pydantic schemas
error.py # ProblemResponse schema
types.py # Shared types (NormalizedEmail, Phone, UTCDateTime)
health/ # Health response models
hello/ # Hello response models
items/ # Items response models
profile/ # Profile domain models
pagination/ # Cursor-based pagination
cursor.py # Cursor encoding/decoding
link.py # RFC 8288 Link header builder
paginator.py # Pagination helper
services/ # Business logic layer
profile/ # ProfileService with Firestore operations
tests/
unit/ # Unit tests (mocked dependencies)
integration/ # API route tests (TestClient)
e2e/ # Firebase emulator tests (local only)
helpers/ # Shared test utilities
functions/ # Firebase Cloud Functions (Python 3.14)
main.py # HTTP dad-joke function
pyproject.toml # Functions-specific dependencies
tests/ # Isolated function contract tests
Repository guidance follows the canonical AGENTS.md format. Portable skills
use the canonical Agent Skills specification and documentation, with the
detailed format specification, under .agents/skills/. See
AGENTS.md for the working rules and current skill catalog.
All routes use paths without trailing slashes (redirect_slashes=False).
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /health |
No | Health check probe |
| GET | /v1/hello |
No | Default greeting |
| POST | /v1/hello |
No | Generate a personalized greeting (200 OK) |
| GET | /v1/items |
No | List items with pagination |
| POST | /v1/profile |
Yes | Create user profile |
| GET | /v1/profile |
Yes | Get user profile |
| PATCH | /v1/profile |
Yes | Update user profile |
| DELETE | /v1/profile |
Yes | Delete user profile |
| GET | /schemas/{schema_name} |
No | Retrieve a generated JSON Schema |
Protected routes require Authorization: Bearer <Firebase ID token> header.
just lint # Check Python style and GitHub Actions security
just typing # Type-check the FastAPI app
just typing-functions # Type-check the separate Functions project
just test-functions # Run isolated Functions tests
just test # Run unit + integration tests
just test-unit # Run unit tests only
just test-integration # Run integration tests only
just test-e2e # Run emulator E2E tests, or skip when emulators are absent
just test-all # Run every test tier
just cov # Generate HTML and JSON coverage reports| Command | Description |
|---|---|
just serve |
Start dev server with hot reload |
just browser |
Open dev server in browser |
just lint |
Check Ruff linting/formatting and GitHub Actions with zizmor |
just typing |
Type checking via ty |
just typing-functions |
Type-check the separate Functions project |
just test-functions |
Test the separate Functions project |
just test |
Unit + integration tests |
just test-e2e |
Firebase emulator E2E tests |
just test-all |
All test tiers |
just cov |
Coverage report (html/json) |
just check |
Full app and Functions lint, type, test, and dependency-manifest checks |
just emulators |
Start Firebase emulators for E2E |
Run just to see all available commands.
just install # Install/sync dependencies
just install-functions # Install/sync Functions dependencies
just update # Upgrade root and Functions dependencies
just fresh # Clean + reinstall- Create models in
app/models/<resource>/with request/response schemas - Add domain exceptions in
app/exceptions/if needed - Implement service logic in
app/services/<resource>/ - Create handler in
app/api/<resource>.pyusingAPIRouter - Register business routers in
app/api/__init__.py; keep health and schema discovery unversioned inapp/main.py - Add unit tests for service, integration tests for routes
just container-build # Build image
just container-up # Run container
just container-logs # View container logs
just container-down # Stop containerOr with Docker/Podman CLI:
docker build -t fastapi-playground:latest .
docker run --rm -p 8080:8080 --env-file .env fastapi-playground:latestProfile requests also require Application Default Credentials or a service-account credential mounted into the
container. The just container-up recipe mounts service_account.json by default; override its creds argument when
using another local path.
# Build and push to Artifact Registry
GIT_SHA="$(git rev-parse HEAD)"
gcloud builds submit --tag "REGION-docker.pkg.dev/PROJECT_ID/REPO/fastapi-playground:${GIT_SHA}"
# Deploy the repository-built standalone image
gcloud run deploy fastapi-playground \
--image "REGION-docker.pkg.dev/PROJECT_ID/REPO/fastapi-playground:${GIT_SHA}" \
--platform managed \
--region REGIONThe repository Dockerfile produces a standalone image containing the operating system and Python runtime. Do not
combine that image with --base-image or --automatic-updates: Cloud Run automatic base image updates
require either a scratch-based application image or a different buildpack source-deployment flow. Adopting either
would change the established build and image convention and requires separate verification. Rebuild the standalone
image to pick up operating-system, Python, and dependency updates.
The image trusts Cloud Run's forwarded proxy headers so application URLs and HTTPS-only security headers reflect the
original client request after Cloud Run terminates TLS. Request logs correlate an incoming valid traceparent; the
middleware does not create tracing spans.
For detailed infrastructure setup, see GCP.md.
cd functions
uv venv --python 3.14 venv
uv pip install --python venv/bin/python -r requirements.txt
firebase deploy --only functions --project PROJECT_IDThe model-backed function is private. Grant intended callers roles/run.invoker on its backing Cloud Run service and
send an ID token when invoking it. The Function deploys in europe-west4; its Vertex AI client uses the global
endpoint and the auto-updating gemini-pro-latest alias. See functions/README.md for the alias
trade-offs and Cloud Functions documentation.
GitHub Actions workflows in .github/workflows/:
| Workflow | Description |
|---|---|
app-ci.yml |
App and Functions checks, container build, and app test coverage |
app-lint.yml |
Fast Ruff linting and formatting feedback |
zizmor.yml |
Upload GitHub Actions security findings to code scanning |
labeler.yml |
Automatic PR labeling |
labeler-manual.yml |
Manual labeling for historical PRs |
dependabot-auto-merge.yml |
Auto-merge Dependabot minor/patch updates |
Dependabot is configured in .github/dependabot.yml for automated dependency updates.
See AGENTS.md for coding guidelines and conventions.
MIT - see LICENSE.