- Overview
- Architecture
- Project Structure
- Getting Started
- Development Workflow
- API Documentation
- Database Design
- Authentication & Security
- Deployment
- Testing
- Contributing
Kairos is a bikepacking journey tracking application backend built with FastAPI, MongoDB, and deployed on AWS Lambda. The application allows users to create journeys, add markers (both past and planned), and discover other bikepackers near their routes using geospatial queries.
Current Deployment: https://7zpmbpgf7d.execute-api.eu-west-2.amazonaws.com/docs
- Framework: FastAPI 0.115.13
- Language: Python 3.12+
- Database: MongoDB (Atlas)
- Authentication: JWT (PyJWT)
- Password Hashing: Bcrypt + Passlib
- Email Service: Resend
- Deployment: AWS Lambda (via Mangum)
- Infrastructure: AWS CloudFormation
- Container: Docker (ECR)
- Dependency Management: Poetry
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ Client │─────▶│ API Gateway │─────▶│ Lambda │
│ Application │◀─────│ (HTTP) │◀─────│ (FastAPI) │
└─────────────┘ └──────────────┘ └─────────────┘
│
▼
┌─────────────┐
│ MongoDB │
│ Atlas │
└─────────────┘
-
API Layer (
kairos/api/)- Route handlers organized by resource
- Request validation via Pydantic
- Dependency injection for database and auth
-
Core Layer (
kairos/core/)- Configuration management
- Security utilities (JWT, password hashing)
- Application-wide settings
-
Database Layer (
kairos/database/)- MongoDB connection management
- Driver pattern for collections
- CRUD operations abstraction
-
Models Layer (
kairos/models/)- Pydantic models for validation
- MongoDB document schemas
- Type definitions
kairos/
├── kairos/ # Main application package
│ ├── api/ # API routes and dependencies
│ │ ├── routes/ # Route handlers by resource
│ │ │ ├── auth.py # Authentication endpoints
│ │ │ ├── users.py # User management
│ │ │ ├── journeys.py # Journey CRUD
│ │ │ └── root.py # Health checks
│ │ ├── deps.py # Dependency injection
│ │ └── main.py # Router aggregation
│ │
│ ├── core/ # Core functionality
│ │ ├── config.py # Settings and configuration
│ │ └── security.py # JWT and password utilities
│ │
│ ├── database/ # Database layer
│ │ ├── drivers/ # Collection-specific drivers
│ │ │ ├── users.py # User operations
│ │ │ ├── journeys.py # Journey operations
│ │ │ └── markers.py # Marker operations with geo queries
│ │ └── main.py # Database connection and setup
│ │
│ ├── models/ # Data models
│ │ ├── base.py # Base model with MongoDB helpers
│ │ ├── id.py # Custom ObjectId type
│ │ ├── users.py # User schema
│ │ ├── journeys.py # Journey schema
│ │ ├── markers.py # Marker schema with GeoJSON
│ │ └── security.py # Token schemas
│ │
│ └── main.py # FastAPI app and Lambda handler
│
├── scripts/ # Utility scripts
│ ├── deploy/ # Deployment scripts
│ │ ├── build.sh # Docker build
│ │ ├── push.sh # ECR push
│ │ ├── deploy.sh # CloudFormation deploy
│ │ └── run.sh # Local Docker run
│ └── start_server.sh # Local development server
│
├── .github/workflows/ # CI/CD
│ └── deploy_backend.yml # Automated deployment
│
├── Dockerfile # Lambda container definition
├── template.yaml # CloudFormation template
├── pyproject.toml # Poetry dependencies
├── poetry.lock # Locked dependencies
├── .env.example # Environment variable template
└── README.md # This file
- Python 3.12 or higher
- Poetry (Python package manager)
- MongoDB Atlas account (or local MongoDB)
- Docker (for deployment)
- AWS CLI (for deployment)
-
Clone the repository
git clone <repository-url> cd kairos
-
Install Poetry (if not already installed)
curl -sSL https://install.python-poetry.org | python3 - -
Install dependencies
poetry install
-
Set up environment variables
cp .env.example .env
Edit
.envwith your configuration:# MongoDB MONGO_HOST=cluster0.mongodb.net MONGO_USERNAME=your_username MONGO_PASSWORD=your_password MONGO_DB_NAME=kairos # Email (Resend) RESEND_API_KEY=your_resend_api_key # Security SECRET_KEY=your_secret_key_for_jwt
-
Run the development server
sh scripts/start_server.sh
The API will be available at http://127.0.0.1:8000
API documentation at http://127.0.0.1:8000/docs
The application automatically creates necessary indexes on startup:
- Journey Index:
user_idfor fast user journey lookups - Marker Geo Index:
2dsphereon coordinates for spatial queries - Marker Journey Index:
journey_idfor journey marker lookups
The project uses Black for code formatting and isort for import sorting.
# Format code
poetry run black kairos/
# Sort imports
poetry run isort kairos/
# Format and sort
poetry run black kairos/ && poetry run isort kairos/Configuration in pyproject.toml:
[tool.isort]
profile = "black"
src_paths = ["app"]Example: Adding a "comments" feature
Step 1: Create the model (kairos/models/comments.py)
from datetime import datetime
from typing import Optional
from kairos.models.base import MongoModel
from kairos.models.id import PyObjectId
from pydantic import Field
class Comment(MongoModel):
id: Optional[PyObjectId] = Field(alias="_id", default=None)
journey_id: PyObjectId
user_id: PyObjectId
text: str
created_at: datetime = Field(default_factory=datetime.now)Step 2: Create the driver (kairos/database/drivers/comments.py)
from bson import ObjectId
from kairos.models.comments import Comment
from pymongo.asynchronous.database import AsyncDatabase
class CommentsDriver:
def __init__(self, database: AsyncDatabase):
self.collection = database["comments"]
async def create(self, comment: Comment) -> Comment:
comment_data = comment.to_mongo()
comment_data.pop("id")
result = await self.collection.insert_one(comment_data)
comment.id = result.inserted_id
return comment
async def query(self, query: dict) -> list[Comment]:
cursor = self.collection.find(query)
comments = await cursor.to_list(length=None)
return [Comment.model_validate(c) for c in comments]Step 3: Add driver to database (kairos/database/main.py)
from kairos.database.drivers import CommentsDriver
class Database:
def __init__(self, client: AsyncMongoClient, database: str):
# ... existing code ...
self.comments = CommentsDriver(self.database)Step 4: Create routes (kairos/api/routes/comments.py)
from fastapi import APIRouter
from kairos.api.deps import CurrentUserDep, DatabaseDep
from kairos.models.comments import Comment
router = APIRouter(prefix="/comments", tags=["comments"])
@router.post("/")
async def create_comment(
db: DatabaseDep,
user: CurrentUserDep,
comment: Comment
) -> Comment:
comment.user_id = user.id
return await db.comments.create(comment)Step 5: Register routes (kairos/api/main.py)
from kairos.api.routes import comments_router
api_router.include_router(comments_router)# In a driver class
async def find_active_journeys(self, user_id: str):
return await self.collection.find({
"user_id": ObjectId(user_id),
"active": True
}).to_list(length=None)async def get_journey_stats(self, journey_id: str):
pipeline = [
{"$match": {"journey_id": ObjectId(journey_id)}},
{"$group": {
"_id": "$marker_type",
"count": {"$sum": 1}
}}
]
cursor = await self.collection.aggregate(pipeline)
return await cursor.to_list(length=None)The MarkersDriver includes geospatial query examples:
# Find markers within distance
async def get_coordinates_nearby_journeys(
self,
coordinates: List[float], # [longitude, latitude]
max_distance_meters: int = 100000
) -> List[str]:
pipeline = [
{
"$geoNear": {
"near": {"type": "Point", "coordinates": coordinates},
"distanceField": "distance",
"maxDistance": max_distance_meters,
"spherical": True,
}
},
{"$group": {"_id": "$journey_id"}},
{"$limit": 50}
]
# ... implementationWhen running locally or deployed:
- Swagger UI:
/docs - ReDoc:
/redoc
The application uses MongoDB's geospatial features for finding nearby journeys:
- Coordinates Format: GeoJSON Point format
[longitude, latitude] - Distance Calculations: Spherical (accounts for Earth's curvature)
- Default Search Radius: 100km (100,000 meters)
- Performance: Optimized with 2dsphere index
The application uses a dual-token system:
-
Access Token
- Short-lived (60 minutes)
- Used for API authentication
- Scope:
access
-
Refresh Token
- Long-lived (8 days)
- Used to obtain new access tokens
- Scope:
refresh
{
"exp": 1234567890, # Expiration timestamp
"sub": "user_id", # Subject (user ID)
"scope": "access" # Token type
}Located in kairos/core/config.py:
class Settings(BaseSettings):
SECRET_KEY: str # JWT signing key
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60
REFRESH_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 8
VERIFICATION_TOKEN_EXPIRE_MINUTES: int = 15
PASSWORD_RESET_TOKEN_EXPIRE_MINUTES: int = 15- Algorithm: Bcrypt
- Library: Passlib with bcrypt backend
- Automatic: Passwords hashed on registration and update
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
# Hash password
hashed = pwd_context.hash(plain_password)
# Verify password
is_valid = pwd_context.verify(plain_password, hashed)- User registers with email
- Verification token sent via Resend
- Token valid for 15 minutes
- User clicks link with token
is_verifiedflag set totrue
- User requests reset via email
- Reset token sent (15 min expiry)
- User clicks link and provides new password
- Token validated and password updated
Use the CurrentUserDep dependency:
from kairos.api.deps import CurrentUserDep
@router.get("/protected")
async def protected_route(user: CurrentUserDep):
# user is automatically validated and injected
return {"user_id": str(user.id)}Configured in kairos/main.py:
app.add_middleware(
CORSMiddleware,
allow_origins=settings.CORS_ORIGINS, # ["*"] by default
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)- Container: Docker image built from
Dockerfile - Registry: AWS ECR (Elastic Container Registry)
- Compute: AWS Lambda (via Mangum adapter)
- API Gateway: AWS API Gateway v2 (HTTP)
- IaC: AWS CloudFormation
The application detects its environment automatically:
ENVIRONMENT: Literal["local", "staging", "production"] = "local"sh scripts/deploy/build.shsh scripts/deploy/push.shaws cloudformation deploy \
--capabilities CAPABILITY_IAM \
--stack-name kairos-backend \
--template-file template.yaml \
--region eu-west-2 \
--parameter-overrides \
ImageUri="<ecr-uri>@<digest>" \
MongoUsername="..." \
MongoPassword="..." \
MongoHost="..." \
MongoDbName="..." \
MailUsername="..." \
ResendApiKey="..." \
SecretKey="..."The .github/workflows/deploy_backend.yml workflow automatically:
- Builds Docker image on push to
main - Pushes to ECR
- Deploys via CloudFormation
- Outputs API Gateway URL
- Triggers client repo update with version
Required Secrets:
AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEYMONGO_USERNAMEMONGO_PASSWORDMONGO_HOSTMONGO_DB_NAMEMAIL_USERNAMERESEND_API_KEYSECRET_KEYKAIROS_API_CLIENT_TS_PAT(for triggering client updates)
Defined in template.yaml:
- LambdaExecutionRole: IAM role with CloudWatch Logs permissions
- LambdaFunction: Container-based Lambda (15s timeout, 256MB memory)
- ApiGateway: HTTP API Gateway
- ApiIntegration: Lambda proxy integration
- ApiRoute: Catch-all route
ANY /{proxy+} - ApiDeployment & ApiStage: Default stage deployment
- LambdaPermission: Allows API Gateway to invoke Lambda
Lambda logs are automatically sent to CloudWatch Logs:
- Log Group:
/aws/lambda/<function-name> - Retention: Default (indefinite)
Lambda Configuration:
- Timeout: 15 seconds (API Gateway max is 30s)
- Memory: 256 MB (adjust based on load)
- Cold Start: ~2-3 seconds with Poetry/dependencies
Optimization Tips:
- Keep dependencies minimal
- Use connection pooling for MongoDB
- Consider Lambda SnapStart if available
- Monitor memory usage and adjust
Problem: Database connection failed
Solutions:
- Check MongoDB Atlas IP whitelist (add
0.0.0.0/0for testing) - Verify credentials in
.env - Ensure MongoDB cluster is running
- Check network connectivity
Problem: Could not validate credentials
Solutions:
- Ensure
SECRET_KEYis set and consistent - Check token expiration
- Verify token is in format:
Bearer <token>
Problem: ModuleNotFoundError: No module named 'kairos'
Solutions:
# Reinstall dependencies
poetry install
# Verify you're in the correct directory
pwd # Should be project root
# Run with poetry
poetry run uvicorn kairos.main:appProblem: Poetry installation fails in Docker
Solutions:
- Clear Docker cache:
docker builder prune - Check Poetry version in Dockerfile
- Ensure
poetry.lockis committed
- API Docs: http://localhost:8000/docs (when running)
- MongoDB Issues: Check connection string format
- AWS Issues: Verify CloudFormation stack events
- Dependencies:
poetry showlists all installed packages