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:latestThe 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.