This repository contains the backend and frontend services for the amazee.ai application. The project is built using a modern tech stack including Python FastAPI for the backend, Next.js with TypeScript for the frontend, and PostgreSQL for the database.
- Backend: Python FastAPI
- Frontend: Next.js + TypeScript
- Database: PostgreSQL
- Testing: Pytest (backend), Jest (frontend)
- Containerization: Docker & Docker Compose
- Orchestration: Kubernetes with Helm
Three steps, all of them yours to start.
- Run the
Releaseworkflow from the Actions tab. Pickpatch,minorormajor; optionally give a commit ondevto release instead of its tip. - Review and merge the
Deploy vX.Y.Z to productionpull request it opens. - Promote
mainto prod in Lagoon, by hand. Nothing before this touches production.
- Reads the newest
vX.Y.Ztag and raises the part you chose. - Cuts
release/vX.Y.Zfrommain, mergesdevinto it, then writes the new version into every file that carries one. - Tags that commit and creates the GitHub Release, with user-facing notes.
- Publishes the Helm charts to GHCR from that branch, so the chart versions are the bumped ones.
- Opens
release/vX.Y.Zβmainwith a description read from git.
It branches from main rather than dev on purpose. main carries the bump
commit from the previous release and dev does not, so from the second release
onward a branch cut from dev conflicts on all six version files, and would need
a third pull request carrying main back to dev to avoid it.
Because the head is a release branch and not dev, its description keeps
describing the code it was written for however long the review takes.
Generated by .github/scripts/deploy-pr-body.sh from git log and
git diff --name-status. It names the schema migrations that alembic will run,
then every changed file that decides how production runs β .lagoon.yml,
docker-compose.yml, helm/, the Dockerfiles, scripts/ cron entrypoints,
app/core/config.py. No model is involved, so it cannot be truncated and cannot
report a deploy as carrying no migrations when it carries three.
Written by a model through the LiteLLM proxy, from the diff between the previous
tag and the new one. Prose earns its place here: the reader is a user of the API,
not a deploy approver. The prompt is .github/prompts/changelog-user.md and the
step needs the LITELLM_API_KEY secret. Without it the release still happens,
with GitHub's own generated notes. If nothing user-visible changed, the model
says so and only the generated notes are used.
The public GitHub Releases page is the changelog. There is no CHANGELOG.md.
.github/scripts/bump-version.sh writes it into:
app/__version__.pyβ served by/versionand the OpenAPI schemahelm/Chart.yamlβ chart version,appVersion, and the subchart pinshelm/charts/backend/Chart.yaml,helm/charts/frontend/Chart.yamlfrontend/package.json,frontend/package-lock.json
In the YAML and Python files the version lines carry an
x-release-please-version marker, so the script only ever rewrites a value that
is meant to be a version. Do not edit these by hand.
One consequence to know: dev never receives the bump commit, so the dev
environment reports the version of the release before last. The tag and main
are authoritative.
- Docker and Docker Compose
- Make (for running convenience commands)
- Node.js and npm (for local frontend development)
- Python 3.x (for local backend development)
-
Clone the repository:
git clone [repository-url] cd [repository-name] -
Install node dependencies
cd frontend npm install cd ../
-
Environment Setup:
- Copy any example environment files and configure as needed
- Ensure all required API keys are set
- Ensure you have set the
AWS_ACCESS_KEY_IDandAWS_SECRET_ACCESS_KEYvariables
-
Start the services:
docker-compose up -d
This will start:
- PostgreSQL database (port 5432)
- Backend service (port 8000)
- Frontend service (port 3000)
- litellm service (port 4000)
make backend-test # Run backend tests
make backend-test-cov # Run backend tests with coverage report
make backend-test-regex # Waits for a string which pytest will parse to only collect a subset of testsmake integration-test # Against the default proxy image
make integration-test LITELLM_TAG=v1.95.0 # Gate a specific LiteLLM version
make integration-test LITELLM_IMAGE=ghcr.io/amazeeio/litellm-lagoon-base-database LITELLM_TAG=v1.96.2Runs tests/integration/ against real LiteLLM proxies (budgets, spend
tracking, caps, budget cycles incl. the 31d/1mo special cases, lifecycle,
user sync, model sync). Uses docker-compose.integration.yml under its own
compose project, so it never touches your dev containers or volumes.
Requires docker compose >= 2.24. CI: the "LiteLLM Integration Gate" workflow
(manual dispatch to gate a version bump + weekly cron); see
litellm-integration-tests-plan.md for design and prerequisites.
make frontend-test # Run frontend tests if they existmake test-all # Run both backend and frontend testsmake test-clean # Clean up test containers and imagesTo clean up test containers and images:
make test-clean-
Start all services in development mode:
docker-compose up -d
Local Compose automatically picks up
docker-compose.override.yml, which swaps the Postgres services to the pgvector image so private AI key creation can run locally without changing Lagoon deployment config. -
View logs for all services:
docker-compose logs -f
-
View logs for a specific service:
docker-compose logs -f [service] # e.g. frontend, backend, postgres -
Restart a specific service:
docker-compose restart [service]
-
Stop all services:
docker-compose down
The development environment includes:
- Hot reloading for frontend (Next.js) on port 3000
- Hot reloading for backend (Python) on port 8800
- PostgreSQL database on port 5432
Access the services at:
- Frontend: http://localhost:3000
- Backend API: http://localhost:8800
Private key creation supports two ownership modes:
owner_id: user-owned key.team_id: team-owned shared key.owner_idandteam_idare mutually exclusive.- If both are omitted, key ownership defaults to the current user (
owner_id=current_user.id).
Limit controls and precedence:
- Team cap:
PUT /spend/{region_id}/team/{team_id}/budget - Team member cap (user within team):
PUT /spend/{region_id}/team/{team_id}/member/{user_id}/budget - Key cap:
PUT /spend/{region_id}/key/{key_id}/budget - Effective enforcement is the strictest applicable gate for the request context.
- Use team-owned keys (
team_id) for shared team usage. - Create keys via
POST /private-ai-keyswithteam_id. - Set the team budget cap via
PUT /spend/{region_id}/team/{team_id}/budget. - All team keys/users spend from the same team budget pool until the team cap is reached.
- Use user-owned keys (
owner_id) for keys tied to a specific user. - Create keys via
POST /private-ai-keyswithowner_id. - For users inside a team, set user budget with the team-member endpoint (not user-only endpoint).
- Use
PUT /spend/{region_id}/team/{team_id}/member/{user_id}/budget. - The member cap applies across that user's keys in the specified team.
- Set key budgets directly via the key spend endpoint.
- Use
PUT /spend/{region_id}/key/{key_id}/budget. - Each key is enforced independently.
- Team and team-member limits can still apply as additional ceilings.
Note: Spend enforcement in LiteLLM is evaluated on spend updates, so the blocking request is typically the first request after crossing a cap.
Teams go through a three-stage lifecycle managed by background workers.
| Stage | Trigger | What happens |
|---|---|---|
| Active | Team created | Normal operation |
| Soft-deleted | >76 days inactive (no API activity) + 14-day grace after warning email; or manual POST /teams/{id}/soft-delete |
deleted_at set; all LiteLLM keys expired (duration=0d); users deactivated. POOL teams are exempt from automatic soft-delete. |
| Hard-deleted | deleted_at is β₯ 90 days ago |
All data permanently removed (GDPR requirement) |
When hard_delete_expired_teams() runs (daily at 03:00 via cron), it deletes each expired team's data in this order to respect FK constraints:
limited_resources(team + user rows)- LiteLLM keys (remote call, best-effort)
spend_caps(team-, user-, and key-scoped)ai_tokens(private AI keys) from DBapi_tokens,user_admin_regions(user FK tables β noON DELETE CASCADE)audit_logs.user_idset toNULL(rows preserved for audit history)user_spend_cache(email-keyed stale cache)usersteam_products,team_regions- Audit log entry written (
action=team.hard_delete) teams(cascadesteam_metricsautomatically)
A soft-deleted team can be restored by a system admin via POST /teams/{id}/restore. The restore:
- Clears
deleted_atand reactivates all users - Re-provisions the LiteLLM team and users in every active region (idempotent)
- Un-expires all keys in LiteLLM
If LiteLLM re-provisioning fails for any region, the team is still marked restored in the DB and the response includes a "warning" field listing the affected regions. Check the audit_logs table (action=team.restore) for the full litellm_failed_regions detail.
# Trigger the hard-delete job manually on the backend container
python scripts/trigger_hard_delete_job.pyIf you have a database dump, you can restore it into your local PostgreSQL service following these steps:
-
Extract the dump:
mkdir -p ./restore-data tar -xf the-postgres-database-dump.tar -C ./restore-data
-
Prepare the restore script: The dump should contain a
restore.sqlfile, which then contains placeholders and likely a different database name. Update it for your local environment:# Replace the data path placeholder sed -i '' 's/\$\$PATH\$\$/\/tmp\/restore/g' ./restore-data/restore.sql # Replace the dumped database name (`dumped-database-example-name`) with your local one (e.g. `postgres_service`) sed -i '' 's/dumped-database-example-name/postgres_service/g' ./restore-data/restore.sql
-
Stop the backend: To prevent active connections during the restoration, stop the backend container:
docker compose stop backend
-
Get the name of the postgres container Copy the name e.g.
amazeeai-postgres-1and replace<postgres-container-name>in the following commands.docker compose ps
-
Transfer and restore: Copy the files to the database container, fix permissions, and run the restoration:
# Create directory and copy files docker exec <postgres-container-name> mkdir -p /tmp/restore docker cp ./restore-data/. <postgres-container-name>:/tmp/restore/ # Fix permissions so the postgres user can read the .dat files docker exec <postgres-container-name> chown -R postgres:postgres /tmp/restore # Run the restoration script docker exec <postgres-container-name> psql -U postgres -f /tmp/restore/restore.sql
-
Restart and Clean up:
# Start the backend again docker compose start backend # Optional: remove temporary files from the container docker exec <postgres-container-name> rm -rf /tmp/restore
We follow a structured branching and deployment process to ensure stability across environments.
- Default Branch:
devis the default branch, and it is linked to thedevenvironment on Lagoon. - Branching: Always create new feature branches from
dev. Bugfixes can potentially be created from themainbranch if they need to be merged intomainandprodfaster than in-progressdevwork. - Review: Create a Pull Request (PR) back into
dev. All PRs must be reviewed and tested locally before merging.
- Dev Testing: After merging, verify your changes on the
devenvironment. - Staging: Once verified on dev, create a PR from
devtomain. Themainbranch serves as our Stage environment.
- Lagoon: Deployments are managed via Lagoon.
- Promotion: Deploy to Prod by promoting the build from the
mainbranch directly on the Lagoon Dashboard or via Lagoon CLI.
- Create a new branch from
dev:git checkout -b feature/my-feature - Make your changes and commit.
- Run the test suite:
make test-all - Submit a pull request to the
devbranch.
.
βββ app/ # Backend Python code
βββ docs/ # Documentation around design decisions
βββ frontend/ # React frontend application
βββ tests/ # Backend tests
βββ scripts/ # Utility scripts
βββ docker-compose.yml # Docker services configuration
βββ Dockerfile # Backend service Dockerfile
βββ Dockerfile.test # Test environment Dockerfile
βββ Makefile # Development and test commands
DATABASE_URL: PostgreSQL connection stringSECRET_KEY: Application secret keyDYNAMODB_ROLE_NAME: role to assume for accessing DDB resources (created by terraform)SES_ROLE_NAME: Role to assume for SES access (created by terraform)SES_SENDER_EMAIL: Validated identity in SES from which emails are sentENV_SUFFIX: Naming suffix to differentiate resources from different environments. Defaults todev.SES_REGION: Optional, defaults to eu-central-1DYNAMODB_REGION: Optional, defaults to eu-central-2MOAD_API_KEY: API key for MOAD service authenticationPERIODIC_TOPUP_EXPIRY_DAYS: Days before periodic top-up budget expires (default: 365)
NEXT_PUBLIC_API_URL: Backend API URL
- Create a new branch for your feature
- Make your changes
- Run the test suite
- Submit a pull request
This project is licensed under the Apache License, Version 2.0 - see below for details:
Copyright 2024 amazee.io
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
For the full license text, please see http://www.apache.org/licenses/LICENSE-2.0