FastAPI service for the crewsim admin API.
See Architecture for the source layout, dependency rules, and guidance on adding a domain.
UV is the primary package manager for this project. Bootstrap it in a Python virtual environment, then use UV for project dependency management:
python -m venv venv
source venv/bin/activate
python -m pip install uv
uv sync --active --extra devOn Windows, activate the environment with venv\Scripts\activate instead. The --active flag
tells UV to install the locked dependencies into the activated venv environment.
Copy the development environment template before running commands that connect to PostgreSQL:
cp .env.example .envThe template uses DB_HOST=db for Compose. When running the application, migrations, or seed
command directly on the host, set DB_HOST=localhost in .env (or in the command environment).
Apply migrations and start the local API with:
uv run --active alembic upgrade head
uv run --active uvicorn app.main:app --reloadThe API is available at http://localhost:8000, with interactive documentation at
http://localhost:8000/docs.
The Compose stack runs four services:
db: PostgreSQL with data stored in thepostgres_datanamed volume.migrate: a one-shot job that applies all Alembic migrations.api: the FastAPI application, started only after the database is healthy and migrations complete successfully.pgadmin: a local pgAdmin UI, available onhttp://localhost:5051by default.
Docker with the Compose plugin is required. Copy the development defaults and start the stack:
cp .env.example .env
docker compose up --build --waitThe API is available at http://localhost:8000. Check it with:
curl http://localhost:8000/health
curl http://localhost:8000/The interactive API documentation is at http://localhost:8000/docs.
After the database migrations have completed, create 100 deterministic user/eSIM pairs, 300 linked usage events, and 100 linked Stripe notifications. The fixtures include varied profile, device, status, balance, date, network, data, voice, SMS, payment, tax, and credit values to exercise pagination and information-dense tables:
docker compose exec api python -m app.seedThe command is safe to run more than once: existing seed users, eSIMs, usage events, and Stripe
notifications are left unchanged. Use --count to create between 1 and 100 pairs (with three
usage events and one Stripe notification per pair) instead of the default 100:
docker compose exec api python -m app.seed --count 25When running the API directly rather than through Compose, use:
uv run --active python -m app.seedThe script reads the normal DB_* application configuration (or the optional
DATABASE_URL override).
# Show service and health status
docker compose ps
# Follow API logs
docker compose logs --follow api
# Check the current database revision
docker compose exec api alembic current
# Apply migrations again after adding a revision
docker compose run --rm migrate
# Stop containers while preserving database data
docker compose downTo intentionally remove the local database as well, run docker compose down --volumes.
This permanently deletes the Compose-managed PostgreSQL volume.
Alembic configuration lives in alembic.ini, and migration scripts live in migrations/.
With the development environment active and database settings configured, use:
# Show the revision history and current migration heads
uv run --active alembic history
uv run --active alembic heads
# Apply all pending migrations
uv run --active alembic upgrade head
# Confirm that the SQLAlchemy models require no new migration
uv run --active alembic checkTo generate a migration after an intentional model change, run:
uv run --active alembic revision --autogenerate -m "describe the change"Review generated migrations before applying them. Model discovery is configured in
migrations/env.py; a new domain model must be imported there.
The timezone domain maps to time_zone (name TEXT PRIMARY KEY,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()). Its migration automatically seeds
IANA timezone identifiers, including aliases and UTC, from PostgreSQL's
pg_timezone_names catalog, excluding host-local entries and posix/ and right/
duplicates. The seeded identifiers reflect the database server's installed timezone
data when the migration runs; no separate development seed command is needed.
Compose reads development settings from .env. It defaults DB_HOST to the container-safe
hostname db; localhost would incorrectly refer to the API container itself.
The database configuration is assembled at runtime from DB_HOST, DB_PORT, DB_DATABASE,
DB_USERNAME, and DB_PASSWORD. In Dokploy, set all five on the application as runtime
environment variables. The credentials in .env.example are local development defaults;
supply production values through Dokploy's environment or secret manager, and do not copy
.env into the image. DATABASE_URL remains available as an optional override and takes
precedence when it is set.
APP_PORT controls the host port. The application always listens on port 8000 inside the
container.
Set CORS_ORIGINS to a comma-separated list of exact frontend origins. For production, use:
CORS_ORIGINS=https://bss.crewsim.devDo not include paths or a trailing slash. The API accepts cross-origin GET, POST, PATCH,
DELETE, and OPTIONS requests, and permits the Content-Type, CF-Access-Client-Id, and
CF-Access-Client-Secret request headers.
The Cloudflare Access application for core.crewsim.dev must also be configured separately:
- Add a Service Auth policy whose include rule matches the intended service token.
- Enable Bypass OPTIONS requests to origin so browser preflight requests reach this API.
- Keep the service-token values in deployment secrets; never commit them to this repository.
Access policy and preflight settings are Cloudflare account configuration and are not controlled by this FastAPI application.
Warning
A browser bundle cannot keep CF-Access-Client-Secret confidential. If the frontend runs in
users' browsers, inject the service-token headers in a trusted server-side proxy or Cloudflare
Worker instead of exposing the token in frontend code.
When PostgreSQL is managed separately, build the same image and pass a database URL reachable from inside the container:
docker build --tag core-crewsim:local .
docker run --rm \
--publish 8000:8000 \
--env APP_ENV=production \
--env APP_DEBUG=false \
--env DB_HOST=database \
--env DB_PORT=5432 \
--env DB_DATABASE=core_crewsim \
--env DB_USERNAME=user \
--env DB_PASSWORD=password \
core-crewsim:localThe container runs alembic upgrade head before starting Uvicorn. If a migration fails, the API
does not start and the container exits with a failure. This startup approach is intended for a
single API replica, such as a Dockerfile-based Dokploy application.
For deployments with multiple API replicas, run migrations as a separate deployment step to avoid concurrent migration attempts:
docker run --rm \
--env DB_HOST=database \
--env DB_PORT=5432 \
--env DB_DATABASE=core_crewsim \
--env DB_USERNAME=user \
--env DB_PASSWORD=password \
core-crewsim:local alembic upgrade headUse UV to add, update, and remove dependencies so that pyproject.toml and uv.lock stay in
sync. For example:
uv add <package>
uv add --dev <package>
uv remove <package>
uv lock --upgradeThe runtime image installs exact versions from requirements.lock. Regenerate that file with
UV whenever runtime dependencies change:
uv export --format requirements-txt --no-dev --no-emit-project --output-file requirements.lockReview and test dependency changes before rebuilding the image. Use pip only for the initial
UV bootstrap; manage project packages with UV after that.
# Confirm every colocated test is discovered once
uv run --active pytest --collect-only
# Run tests and lint checks
uv run --active pytest
uv run --active ruff check .
# Build and inspect production package artifacts
uv buildTests live beside their owning modules under app/; shared fixtures live in conftest.py.
Production package and Docker builds exclude the colocated test modules and conftest.py.
- Add pre-commit hooks and other development tooling.
- Add a CI/CD pipeline after choosing a deployment platform.