Drop-in device tracking for FastAPI - persistent IDs without user accounts
Project description
FastAPI Device ID
Track devices across sessions without user registration. Perfect for analytics, rate limiting, and A/B testing.
Drop-in device tracking for FastAPI apps. Get persistent device IDs without user accounts, databases, or privacy concerns.
Why FastAPI Device ID?
The Problem
Modern web applications often need to:
- Track user behavior across sessions without requiring login
- Implement rate limiting per device to prevent abuse
- Provide consistent A/B testing experiences
- Analyze usage patterns and user journeys
- Differentiate between devices for security or UX purposes
Traditional solutions are either too complex (full user accounts), privacy-invasive (fingerprinting), or unreliable (IP addresses change, shared networks).
The Solution
FastAPI Device ID provides a privacy-friendly middle ground:
✅ No personal data required - Just a unique identifier per browser
✅ Persistent across sessions - Survives browser restarts
✅ Secure by default - HTTP-only cookies with CSRF protection
✅ Scalable - Works across multiple server instances
✅ Compliant - Respects user privacy, no tracking across domains
When to Use
Perfect for applications that need:
- Anonymous analytics without user accounts
- Rate limiting by device rather than IP
- Session continuity across page reloads
- A/B testing with consistent user experiences
- Fraud prevention through device recognition
- Usage quotas for anonymous API access
When NOT to Use
Consider alternatives if you need:
- Cross-domain tracking (this is single-domain only)
- User authentication (use proper auth systems instead)
- Long-term user identification (users can clear cookies)
- Foolproof uniqueness (sophisticated users can bypass)
Features
- 🔒 Secure by default: HTTP-only, secure, and SameSite cookies
- 🆔 Modern UUID: Uses UUID7 (time-ordered) with UUID4 fallback for older Python versions
- 📦 Zero dependencies: Only requires FastAPI/Starlette (which you already have)
- ⚙️ Highly configurable: Customize cookie names, expiration, and security settings
- 🪶 Lightweight: Minimal overhead with pluggable architecture
- 🔧 Type safe: Full type hints with dependency injection support
- 🔌 Pluggable: Custom ID generators and security strategies
- 🛡️ Multiple security strategies: Plaintext, JWT, and AES encryption support
- ⚡ Performance optimized: Constant-time comparisons and efficient encoding
- 🌐 Production ready: Distributed deployment support with consistent encryption
🚀 Quick Start
Installation
pip install fastapi-device-id
In production in 2 minutes:
- Add the middleware:
app.add_middleware(DeviceMiddleware) - Use the dependency:
async def handler(device_id: DeviceId): - Start tracking:
analytics.track(device_id, event)
Basic Usage
from fastapi import FastAPI
from fastapi_device_id import DeviceMiddleware, DeviceId
app = FastAPI()
# Add the middleware
app.add_middleware(DeviceMiddleware)
@app.get("/")
async def read_root(device_id: DeviceId):
# This device_id is automatically persistent across browser sessions
# No database setup required!
return {"message": f"Welcome back, device {device_id}"}
@app.get("/analytics")
async def track_visit(device_id: DeviceId):
# Log analytics, track user behavior, etc.
print(f"Device {device_id} visited /analytics")
return {"status": "visit tracked"}
Custom Configuration
from fastapi import FastAPI
from fastapi_device_id import DeviceMiddleware
app = FastAPI()
# Customize the middleware
app.add_middleware(
DeviceMiddleware,
cookie_name="my_device_id", # Custom cookie name
cookie_max_age=30 * 24 * 60 * 60, # 30 days instead of 1 year
cookie_secure=False, # Allow HTTP in development
cookie_samesite="strict", # Stricter cookie policy
)
Custom ID Generator
import secrets
from fastapi import FastAPI
from fastapi_device_id import DeviceMiddleware
def custom_id_generator() -> str:
"""Generate a custom device ID."""
return f"device_{secrets.token_hex(16)}"
app = FastAPI()
app.add_middleware(
DeviceMiddleware,
id_generator=custom_id_generator,
)
Security Strategies
FastAPI Device ID supports multiple security strategies for device ID storage:
Plaintext Strategy (Default)
from fastapi_device_id import DeviceMiddleware, PlaintextStrategy
app.add_middleware(
DeviceMiddleware,
security_strategy=PlaintextStrategy(),
)
JWT Strategy
Provides tamper detection with cryptographic signatures:
from fastapi_device_id import DeviceMiddleware, JWTStrategy
# JWT with signature verification
app.add_middleware(
DeviceMiddleware,
security_strategy=JWTStrategy(
secret="your-secret-key",
algorithm="HS256",
expiration_hours=24 # Optional expiration
),
)
Encrypted Strategy
Provides confidentiality - device ID is hidden from client:
from fastapi_device_id import DeviceMiddleware, EncryptedStrategy
# AES encryption with Fernet (recommended)
app.add_middleware(
DeviceMiddleware,
security_strategy=EncryptedStrategy(
key="your-encryption-key",
algorithm="fernet" # or "aes-256-gcm", "aes-128-gcm"
),
)
# Production example with environment variable
import os
app.add_middleware(
DeviceMiddleware,
security_strategy=EncryptedStrategy(
key=os.environ["DEVICE_ID_ENCRYPTION_KEY"],
algorithm="fernet"
),
)
Security Strategy Comparison
| Strategy | Confidentiality | Integrity | Performance | Use Case |
|---|---|---|---|---|
| Plaintext | ❌ | ❌ | ⚡ Fastest | Development, non-sensitive |
| JWT | ❌ | ✅ | 🟨 Medium | Tamper detection needed |
| Encrypted | ✅ | ✅ | 🟨 Medium | Production, sensitive data |
API Reference
DeviceMiddleware
The main middleware class that handles device identification.
Parameters
cookie_name(str, default:"device_id"): Name of the cookie to store the device IDcookie_max_age(int, default:365 * 24 * 60 * 60): Cookie expiration time in seconds (1 year)cookie_expires(str | None, default:None): Cookie expiration date stringcookie_path(str, default:"/"): Cookie pathcookie_domain(str | None, default:None): Cookie domaincookie_secure(bool, default:True): Whether the cookie should only be sent over HTTPScookie_httponly(bool, default:True): Whether the cookie should be inaccessible to JavaScriptcookie_samesite(str, default:"lax"): SameSite policy ("strict","lax", or"none")id_generator(Callable[[], str], default:default_id_generator): Function to generate device IDssecurity_strategy(DeviceIDSecurityStrategy, default:PlaintextStrategy()): Security strategy for encoding device IDs
DeviceId Type
A FastAPI dependency that extracts the device ID from the request:
from fastapi_device_id import DeviceId
@app.get("/my-endpoint")
async def my_handler(device_id: DeviceId):
# device_id is automatically extracted and typed as str
return {"device_id": device_id}
get_device_id Function
Direct function to extract device ID from a request:
from fastapi import Request
from fastapi_device_id import get_device_id
@app.get("/manual")
async def manual_extraction(request: Request):
device_id = get_device_id(request)
return {"device_id": device_id}
compare_device_ids Function
Secure constant-time comparison of device IDs to prevent timing attacks:
from fastapi_device_id import compare_device_ids
@app.get("/secure-compare")
async def secure_comparison(device_id: DeviceId, stored_id: str):
# ❌ VULNERABLE to timing attacks
# if device_id == stored_id:
# ✅ SECURE constant-time comparison
if compare_device_ids(device_id, stored_id):
return {"access": "granted"}
return {"access": "denied"}
Performance
- Zero dependencies beyond FastAPI/Starlette
- <1ms overhead per request
- Memory efficient: No server-side storage required
- Scales horizontally: Stateless design works across instances
- Cookie-based: Works with CDNs and load balancers
Use Cases
Analytics and Tracking
@app.get("/track-page-view")
async def track_page_view(device_id: DeviceId, page: str):
# Store page view with device ID
analytics.track_page_view(device_id, page)
return {"status": "tracked"}
A/B Testing
@app.get("/feature")
async def get_feature_flag(device_id: DeviceId):
# Consistent A/B testing based on device ID
variant = "A" if int(device_id.replace("-", ""), 16) % 2 else "B"
return {"variant": variant}
Rate Limiting
from collections import defaultdict
from time import time
rate_limits = defaultdict(list)
@app.get("/api/data")
async def get_data(device_id: DeviceId):
now = time()
device_requests = rate_limits[device_id]
# Clean old requests (1 hour window)
device_requests[:] = [req_time for req_time in device_requests if now - req_time < 3600]
if len(device_requests) >= 100: # 100 requests per hour
return {"error": "Rate limit exceeded"}
device_requests.append(now)
return {"data": "your data here"}
Common Patterns
Anonymous User Analytics
from fastapi_device_id import DeviceId
import logging
analytics_logger = logging.getLogger("analytics")
@app.get("/page/{page_name}")
async def track_page_view(page_name: str, device_id: DeviceId):
analytics_logger.info(f"Device {device_id} viewed {page_name}")
return {"content": f"Welcome to {page_name}"}
@app.post("/event")
async def track_custom_event(event: dict, device_id: DeviceId):
event["device_id"] = device_id
analytics_logger.info(f"Custom event: {event}")
return {"status": "tracked"}
Shopping Cart Persistence
from typing import Dict, List
carts: Dict[str, List[dict]] = {}
@app.post("/cart/add")
async def add_to_cart(item: dict, device_id: DeviceId):
if device_id not in carts:
carts[device_id] = []
carts[device_id].append(item)
return {"cart_size": len(carts[device_id])}
@app.get("/cart")
async def get_cart(device_id: DeviceId):
return {"items": carts.get(device_id, [])}
Feature Flag Management
import hashlib
@app.get("/feature/{feature_name}")
async def get_feature_flag(feature_name: str, device_id: DeviceId):
# Consistent feature assignment based on device ID
hash_input = f"{feature_name}:{device_id}"
hash_value = int(hashlib.md5(hash_input.encode()).hexdigest()[:8], 16)
enabled = hash_value % 100 < 50 # 50% rollout
return {"feature": feature_name, "enabled": enabled}
Configuration Examples
Development Setup
# Relaxed settings for local development
app.add_middleware(
DeviceMiddleware,
cookie_secure=False, # Allow HTTP
cookie_samesite="lax", # Relaxed policy
security_strategy=PlaintextStrategy(), # No encryption overhead
)
Production Setup
import os
from fastapi_device_id import EncryptedStrategy
# Strict security for production
app.add_middleware(
DeviceMiddleware,
cookie_secure=True, # HTTPS only
cookie_httponly=True, # No JavaScript access
cookie_samesite="strict", # Strict policy
security_strategy=EncryptedStrategy(
key=os.environ["DEVICE_ID_ENCRYPTION_KEY"],
algorithm="fernet"
),
)
Short-lived Sessions with JWT
from fastapi_device_id import JWTStrategy
# JWT tokens that expire after 24 hours
app.add_middleware(
DeviceMiddleware,
security_strategy=JWTStrategy(
secret="your-jwt-secret",
expiration_hours=24
),
)
High-Security Setup
import os
from fastapi_device_id import EncryptedStrategy
# Maximum security configuration
app.add_middleware(
DeviceMiddleware,
cookie_secure=True,
cookie_httponly=True,
cookie_samesite="strict",
cookie_max_age=7 * 24 * 60 * 60, # 1 week
security_strategy=EncryptedStrategy(
key=os.environ["DEVICE_ID_ENCRYPTION_KEY"],
algorithm="aes-256-gcm", # Strong encryption
iterations=200000, # Higher security
),
)
Requirements
- Python 3.8+
- FastAPI 0.68.0+
- Starlette 0.14.0+
Optional Dependencies
For JWT strategy support:
pip install "fastapi-device-id[jwt]"
For encryption strategy support:
pip install "fastapi-device-id[crypto]"
For development:
pip install "fastapi-device-id[dev]"
For all features:
pip install "fastapi-device-id[jwt,crypto,dev]"
Production Checklist
Before deploying to production, ensure:
- Security Strategy: Use
EncryptedStrategyorJWTStrategyinstead ofPlaintextStrategy - Environment Variables: Store encryption keys in
os.environ, never in code - Cookie Security: Set
cookie_secure=Truefor HTTPS-only cookies - SameSite Policy: Use
cookie_samesite="strict"for maximum security - Key Rotation: Plan for periodic encryption key rotation
- Monitoring: Log device ID operations for debugging
- Privacy Policy: Update your privacy policy to mention device tracking
- GDPR Compliance: Implement cookie consent if required in your jurisdiction
Example Production Configuration
import os
from fastapi_device_id import DeviceMiddleware, EncryptedStrategy
app.add_middleware(
DeviceMiddleware,
cookie_secure=True,
cookie_httponly=True,
cookie_samesite="strict",
security_strategy=EncryptedStrategy(
key=os.environ["DEVICE_ID_ENCRYPTION_KEY"],
algorithm="fernet"
),
)
Contributing
We welcome contributions! Please see our Contributing Guide for details.
Development Setup
# Clone the repository
git clone https://github.com/ideatives/fastapi-device-id.git
cd fastapi-device-id
# Install development dependencies
pip install -e ".[dev]"
# Run tests
pytest
# Format code
black src tests
ruff --fix src tests
# Type checking
mypy src
License
This project is licensed under the MIT License - see the LICENSE file for details.
Changelog
See CHANGELOG.md for version history.
Security
For security issues, please see our Security Policy.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file fastapi_device_id-0.1.0.tar.gz.
File metadata
- Download URL: fastapi_device_id-0.1.0.tar.gz
- Upload date:
- Size: 28.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cbf294b1071fab2dec8d19630ec6b2dacf01ff63ad397583fb549da6ea5beed2
|
|
| MD5 |
d97602b1f9454798b396d874929b87ff
|
|
| BLAKE2b-256 |
a3279618ad9ce64032beceb05c0e34bbc3a59092c629511b18e32a41097d8082
|
File details
Details for the file fastapi_device_id-0.1.0-py3-none-any.whl.
File metadata
- Download URL: fastapi_device_id-0.1.0-py3-none-any.whl
- Upload date:
- Size: 13.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0b0ab0a439e9bcfd5af94fa09e2a0117487fa06865a533f4e6e45e5466180b58
|
|
| MD5 |
b76e7c71094ff2e58160cfdbffc5b199
|
|
| BLAKE2b-256 |
b0824a07db6314203d4db56e41722b87c3718cb0e40004f4c49e3e0d9d29e0d8
|