A self-hosted GPS track manager REST API built with FastAPI and PostgreSQL/PostGIS.
- Docker (for containerised deployment), or Python ≥ 3.11 + Poetry
- PostgreSQL ≥ 14 with the PostGIS extension enabled
- (Optional) Redis for distributed caching
docker build -t ort .The Dockerfile uses a multi-stage Chainguard Wolfi base image and runs the application as a non-root user on port 5000.
Minimum required environment variables are DATABASE_URI, TOKEN_URL, SECRET_KEY, and IMAGE_PATH.
docker run --name ort \ -e DATABASE_URI="postgresql+asyncpg://ort:ort@db-host:5432/ort" \ -e TOKEN_URL="http://localhost:8000/auth/login" \ -e SECRET_KEY="$(openssl rand -hex 32)" \ -e IMAGE_PATH="/tmp" \ -p 8000:5000 \ ort:latest
The API is then reachable at http://localhost:8000.
Copy .env_example to .env and adjust the values before running locally without Docker.
| Variable | Required | Default | Description |
|---|---|---|---|
DATABASE_URI |
yes | — | Async PostgreSQL connection string (postgresql+asyncpg://...) |
TOKEN_URL |
yes | — | Full URL of the login endpoint, e.g. http://localhost:8000/auth/login |
SECRET_KEY |
yes | — | Random secret used to sign JWTs. Generate with openssl rand -hex 32 |
IMAGE_PATH |
yes | — | Directory where uploaded images are stored |
ALGORITHM |
no | HS256 |
JWT signing algorithm |
ACCESS_TOKEN_EXPIRE_MINUTES |
no | 60 |
Token validity in minutes |
LOG_LEVEL |
no | INFO |
DEBUG, INFO, WARNING, ERROR |
SQL_ECHO |
no | False |
Log all SQL statements (useful for debugging) |
CORS_ORIGINS |
no | [] |
JSON array of allowed CORS origins, e.g. ["https://app.example.com"] |
REGISTRATION_ENABLED |
no | true |
Set to false to disable public registration |
EMAIL_CONFIRMATION |
no | false |
Require e-mail verification before login |
MAX_IMAGE_SIZE |
no | 2097152 |
Maximum upload size in bytes (default 2 MB) |
CACHE_ENABLED |
no | true |
Enable response caching |
CACHE_TYPE |
no | local |
local (in-memory TTLCache) or redis |
CACHE_TTL |
no | 3600 |
Cache time-to-live in seconds |
CACHE_MAXSIZE |
no | 1000 |
Maximum entries for the local cache |
REDIS_HOST |
no | 127.0.0.1 |
Redis hostname (only when CACHE_TYPE=redis) |
REDIS_PORT |
no | 6379 |
Redis port |
REDIS_DB |
no | 0 |
Redis database index |
REDIS_PASSWORD |
no | — | Redis password |
REDIS_USERNAME |
no | — | Redis username |
ORT uses JWT Bearer tokens issued via an OAuth2 Password flow. The typical sequence is:
- Register a user account
- Log in to receive an access token
- Include the token in every subsequent request
Registration is rate-limited to 5 requests per 5 minutes per IP.
curl -X POST http://localhost:8000/users/register \ -F "username=johndoe" \ -F "email=john@example.com" \ -F "password=supersecret" \ -F "firstname=John" \ -F "lastname=Doe"
Response 201 Created:
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"username": "johndoe",
"email": "john@example.com",
"firstname": "John",
"lastname": "Doe"
}If
EMAIL_CONFIRMATION=truethe account is disabled until the e-mail address is verified.
Login is rate-limited to 10 requests per minute per IP.
curl -X POST http://localhost:8000/auth/login \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "username=john@example.com&password=supersecret"
You may use either the username or the e-mail address in the username field.
Response 200 OK:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "bearer"
}Tokens expire after ACCESS_TOKEN_EXPIRE_MINUTES minutes (default 60). Re-authenticate to obtain a fresh token.
Pass the token in the Authorization header on every protected request:
export TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." curl http://localhost:8000/users/ \ -H "Authorization: Bearer $TOKEN"
ORT implements the standard OAuth2 Password Grant flow, making it compatible with any OAuth2-aware client.
Token endpoint: POST /auth/login
| Field | Value |
|---|---|
grant_type |
password |
username |
user e-mail or username |
password |
account password |
scope |
(optional) user or admin |
Example using an OAuth2 library (Python httpx):
import httpx response = httpx.post( "http://localhost:8000/auth/login", data={ "grant_type": "password", "username": "john@example.com", "password": "supersecret", }, ) token = response.json()["access_token"] # Use the token client = httpx.Client(headers={"Authorization": f"Bearer {token}"}) me = client.get("http://localhost:8000/users/")
All protected endpoints require the Authorization: Bearer <token> header.
| Method | Path | Auth | Description |
|---|---|---|---|
POST |
/users/register |
No | Register a new user |
GET |
/users/ |
Yes | Get the current user's profile |
| Method | Path | Auth | Description |
|---|---|---|---|
POST |
/tracks/ |
Yes | Upload a GPX track file |
GET |
/tracks/ |
Yes | List tracks (paginated, max 200) |
GET |
/tracks/download |
Yes | Download all tracks as a ZIP archive |
GET |
/tracks/{track_id} |
Yes | Get track summary |
GET |
/tracks/{track_id}/details |
Yes | Get track with waypoints, comments, and images |
GET |
/tracks/{track_id}/points/ |
Yes | Get all track points as GeoJSON |
GET |
/tracks/{track_id}/linestring |
Yes | Get track geometry as a GeoJSON LineString |
GET |
/tracks/{track_id}/download |
Yes | Download a single track as GPX |
DELETE |
/tracks/{track_id} |
Yes | Delete a track |
Upload a GPX file:
curl -X POST http://localhost:8000/tracks/ \ -H "Authorization: Bearer $TOKEN" \ -F "file=@my_track.gpx"
List tracks (with pagination):
curl "http://localhost:8000/tracks/?limit=50&offset=0" \ -H "Authorization: Bearer $TOKEN"
| Method | Path | Auth | Description |
|---|---|---|---|
POST |
/images/{track_id} |
Yes | Upload an image for a track (EXIF GPS extracted automatically) |
GET |
/images/ |
Yes | List images (paginated, max 200) |
GET |
/images/{image_id} |
Yes | Get image details and comments (by ID or MD5 hash) |
GET |
/images/track/{track_id} |
Yes | List all images for a track |
GET |
/images/track/{track_id}/details |
Yes | Get images with comments for a track |
PUT |
/images/{image_id} |
Yes | Update image metadata |
DELETE |
/images/{image_id} |
Yes | Delete an image |
Upload an image:
curl -X POST http://localhost:8000/images/{track_id} \
-H "Authorization: Bearer $TOKEN" \
-F "file=@photo.jpg"ORT ships with Swagger UI. Open the following URL in your browser while the server is running:
http://localhost:8000/api/docs
You can authorise directly in the UI by clicking Authorize and entering your Bearer token, or by using the built-in OAuth2 password form.