Rust port of a Flask geolocation microservice. Accepts image uploads, runs GeoCLIP ML inference via ONNX Runtime to predict GPS coordinates, and reverse-geocodes results to city/region/country strings.
Project is a WIP but aims to be a production-ready backend API for geolocation tasks.
Client
│
▼
Axum HTTP server
├── tower-http: CORS, request size limit, tracing
├── governor: per-IP rate limiting
├── API key middleware (X-API-Key header)
└── CSRF middleware (X-CSRF-Token, HMAC-SHA256)
│
├── POST /api/v1/geolocate
│ ├── image decode (base64 or multipart)
│ ├── CLIP preprocessing (resize 224×224, normalize)
│ ├── ONNX Runtime (ort) → clip_encoder.onnx
│ ├── cosine similarity vs gps_gallery.npy
│ └── reverse_geocoder (offline GeoNames)
│
├── GET /api/v1/csrf-token
└── GET /api/v1/health
No Python at runtime. GeoCLIP model exported to ONNX once via scripts/export_geoclip_onnx.py.
- Rust 1.75+
- Python 3.10+ with
geoclip,torchinstalled (model export only) models/directory populated by export script (see Setup)
pip install geoclip torch numpy
python scripts/export_geoclip_onnx.pyProduces:
models/clip_encoder.onnx— CLIP ViT-L/14 vision encodermodels/gps_gallery.npy— GPS coordinate gallery[N, 2]models/gps_embeddings.npy— pre-computed normalized embeddings[N, 512]
cp .env.example .env
# Edit .env — set API_KEY, CSRF_SECRET, NEXTJS_DOMAINcargo build --release
cargo run --releaseServer starts on PORT (default 3000).
| Variable | Required | Default | Description |
|---|---|---|---|
API_KEY |
Yes | — | Secret key sent in X-API-Key header |
CSRF_SECRET |
Yes | — | HMAC signing key for CSRF tokens |
NEXTJS_DOMAIN |
No | http://localhost:3000 |
Allowed CORS origin |
PORT |
No | 3000 |
HTTP listen port |
MODEL_PATH |
No | models/clip_encoder.onnx |
Path to ONNX encoder |
GALLERY_PATH |
No | models/gps_gallery.npy |
Path to GPS gallery |
EMBEDDINGS_PATH |
No | models/gps_embeddings.npy |
Path to gallery embeddings |
RUST_ENV |
No | development |
Set to production to disable debug logging |
RUST_LOG |
No | warn |
Tracing filter (e.g. info, debug) |
All responses use envelope { "data": {...} } on success or { "error": "message" } on failure.
No authentication required.
Response 200:
{ "data": { "status": "healthy" } }Headers: X-API-Key: <key>
Rate limit: 10 req/min per IP
Response 200:
{ "data": { "csrf_token": "<token>" } }Headers:
X-API-Key: <key>X-CSRF-Token: <token from /csrf-token>
Rate limit: 10 req/min per IP
Max body: 5 MB
Body (multipart/form-data):
image— file field (png, jpg, jpeg, webp)
Or body (application/x-www-form-urlencoded):
image_data— base64 data URI (data:image/jpeg;base64,...)
Response 200:
{
"data": {
"predictions": [
{ "location": "Paris, Île-de-France, FR", "confidence": 0.72 },
{ "location": "Lyon, Auvergne-Rhône-Alpes, FR", "confidence": 0.18 },
{ "location": "Marseille, Provence-Alpes-Côte d'Azur, FR", "confidence": 0.10 }
],
"coordinates": [
{ "lat": 48.8566, "lon": 2.3522 },
{ "lat": 45.7485, "lon": 4.8467 },
{ "lat": 43.2965, "lon": 5.3698 }
]
}
}Error responses:
| Status | Body |
|---|---|
| 400 | { "error": "No image provided" } |
| 400 | { "error": "Invalid file type" } |
| 400 | { "error": "Invalid image data" } |
| 401 | { "error": "Unauthorized" } |
| 403 | { "error": "CSRF token invalid or expired" } |
| 413 | { "error": "File too large. Maximum upload size is 5MB." } |
| 429 | { "error": "Rate limit exceeded" } |
| 500 | { "error": "Internal server error" } |
- API key: All protected endpoints require
X-API-Keyheader (constant-time comparison) - CSRF: Stateless HMAC-SHA256 tokens with 30-minute expiry
- CORS: Restricted to
NEXTJS_DOMAINandhttps://hacknexus.io - Rate limiting: 10 req/min per IP on
/csrf-tokenand/geolocate - Body limit: 5 MB enforced by tower-http layer
- No secrets in logs: IP addresses logged at WARN level only on auth failures
cargo check # fast type check
cargo clippy # lints
cargo fmt # format
cargo test # unit tests
RUST_LOG=debug cargo run # verbose logging