- TypeScript 51.2%
- Python 47.3%
- Dockerfile 0.7%
- JavaScript 0.4%
- Shell 0.3%
Karrage
Karrage is a full-stack web app for vehicle management, expenses, reminders, Paperless-ngx document linking, optional OCR fuel receipt extraction, and fleet statistics.
Stack
- Backend: Python 3.14, FastAPI, async SQLAlchemy 2.0, PostgreSQL 16, Alembic, fastapi-users, Pillow, Pandas, httpx, optional pytesseract.
- Frontend: Next.js 16 App Router, TypeScript, Tailwind CSS, Framer Motion, Recharts, TanStack Query v5, Zustand, React Hook Form, Zod.
- Infrastructure: Docker Compose with backend, frontend, PostgreSQL, and a local media volume for development.
Quick Start
cp .env.example .env
sudo docker compose up --build
Open:
- Frontend: http://localhost:3000
- Backend API docs: http://localhost:8000/docs
- Liveness: http://localhost:8000/api/health
- Readiness: http://localhost:8000/api/ready
Create the first user:
sudo docker compose exec backend python -m app.cli create-user user@example.com --migrate
Use --password "..." for non-interactive setup.
Backend Overview
The backend is a FastAPI application in backend/app. It is intentionally split into thin HTTP routers, Pydantic schemas, SQLAlchemy models, and service modules for business logic.
flowchart LR
Browser[Next.js frontend] -->|/api/*| API[FastAPI backend]
API --> Auth[fastapi-users JWT auth]
API --> Routers[API routers]
Routers --> Services[Domain services]
Services --> DB[(PostgreSQL)]
Routers --> DB
Services --> Media[(Media volume)]
Services --> Paperless[Paperless-ngx]
Services --> Tankerkoenig[Tankerkoenig API]
Services --> OCR[Tesseract OCR]
Backend Modules
app/main.py: application setup, CORS, logging middleware, static media mount, exception handling, health endpoints, router registration.app/config.py: runtime configuration from environment variables and.env; production guard forSECRET_KEYand CORS.app/database.py: async SQLAlchemy engine/session and shared declarative base.app/dependencies.py: fastapi-users setup, JWT authentication, current-user dependency.app/models/: SQLAlchemy ORM models.app/schemas/: Pydantic request/response schemas.app/routers/: HTTP API endpoints grouped by domain.app/services/: business logic for fuel metrics, data quality, migrations, Paperless, storage, OCR, statistics, and external APIs.app/cli.py: operational commands such as user creation and Hammond migration.
Request Flow
sequenceDiagram
participant UI as Frontend
participant API as FastAPI Router
participant Auth as current_active_user
participant Service as Service Layer
participant DB as PostgreSQL
UI->>API: Request with Bearer token
API->>Auth: Resolve active user
Auth-->>API: User
API->>DB: Load owned vehicle/resource
API->>Service: Validate and process domain logic
Service->>DB: Read/write data
DB-->>Service: Result
Service-->>API: Domain result
API-->>UI: Pydantic response
Main Domains
erDiagram
USER ||--o{ TENANT_MEMBER : joins
TENANT ||--o{ TENANT_MEMBER : has
TENANT ||--o{ VEHICLE : owns
VEHICLE ||--o{ FUEL_LOG : has
VEHICLE ||--o{ MAINTENANCE_LOG : has
VEHICLE ||--o{ INSURANCE_POLICY : has
INSURANCE_POLICY ||--o{ INSURANCE_CLAIM : has
VEHICLE ||--o{ LICENSING_RECORD : has
VEHICLE ||--o{ OTHER_EXPENSE : has
VEHICLE ||--o| TECHNICAL_INSPECTION : has
USER ||--o{ DOCUMENT_LINK : owns
Core behavior:
- Vehicles belong to tenants. Every user has at least one tenant membership, and personal use is represented by a private tenant.
- Tenant roles are
OWNER,ADMIN,MEMBER, andVIEWER. Viewing starts atVIEWER, creating fuel/cost entries starts atMEMBER, editing operational data starts atADMIN, and tenant administration is reserved forOWNER. - Tenant owners manage members and tenant-wide Paperless-ngx settings. Paperless tokens are stored on the tenant and are never returned to the frontend.
- Tenant owners can map OIDC group names to tenant roles. When
OIDC_GROUPS_CLAIMis configured, OIDC logins synchronize matching mapped groups asOIDCmemberships while leaving local memberships untouched. - Existing single-user data is migrated into a shared tenant named by
INITIAL_TENANT_NAMEand owned byINITIAL_TENANT_OWNER_EMAILwhen multiple users exist. - Sold or archived vehicles block new fuel and cost entries while keeping history visible.
- Fuel entries normalize liters, total cost, and price per liter when two of the three values are provided.
- Fuel metrics are recalculated after create/update/delete and after CSV correction imports.
- Maintenance, licensing, insurance, and other expenses contribute to statistics and reminders.
- Technical inspection (HU) is tracked per vehicle with due-date calculation from last inspection or first registration.
- Reminders include insurance deadlines, cancellation windows, vehicle tax, maintenance, and HU milestones with urgency levels.
- Insurance policies are contract-like records: only one active/current policy is allowed until the existing one is marked cancelled.
- Paperless links store document IDs/URLs, not document contents.
- Admins and owners can export a vehicle as CSV or JSON from the vehicle detail view. JSON exports include vehicle data and related fuel, maintenance, insurance, licensing, other cost, and HU records without Paperless credentials.
Reminder and calendar behavior:
- The dashboard shows one unified reminder list, sorted by severity (
danger->warning->success) and urgency. - A signed calendar subscription endpoint is available for fixed-cost due dates (insurance, cancellation deadline, vehicle tax, HU):
GET /api/calendar/subscriptionreturns subscription links for the authenticated user.GET /api/calendar/fixcosts.ics?token=...serves an iCalendar feed that can be subscribed externally.
Runtime Configuration
Important environment variables:
DATABASE_URL: async PostgreSQL connection string.SECRET_KEY: JWT/reset/verification secret. In production it must be strong and at least 32 characters.APP_ENV: set toproductionto enable stricter runtime guards.BACKEND_CORS_ORIGINS: comma-separated allowed origins.MEDIA_ROOT: storage path for uploaded media inside the backend container.AUTO_MIGRATE:1runs Alembic migrations during backend startup; set0when migrations are handled separately.UVICORN_RELOAD:1enables reload for local development.UVICORN_WORKERS: worker count for non-reload Uvicorn startup.TANKERKOENIG_API_KEY: optional global fallback key for nearby fuel-station price lookup; only suitable for single-tenant selfhosting (the free Tankerkoenig API is licensed for personal, non-commercial use, so one shared key across tenants is not allowed). In multi-tenant setups each tenant owner stores their own free key (creativecommons.tankerkoenig.de) in the settings UI.TANKERKOENIG_RATE_LIMIT_REQUESTS/TANKERKOENIG_RATE_LIMIT_WINDOW_SECONDS: per-tenant throttle for station lookups (defaults: 10 requests per 300 s) so a tenant key is not overused and blocked upstream.LOGIN_RATE_LIMIT_ATTEMPTS/LOGIN_RATE_LIMIT_WINDOW_SECONDS: in-memory login throttle per client IP.AUTH_COOKIE_NAME: httpOnly session cookie name.AUTH_COOKIE_SECURE: leave unset to use non-secure cookies in local development and secure cookies in production; settruewhen serving behind HTTPS.AUTH_COOKIE_SAMESITE: defaults tolax; usenoneonly for cross-site deployments and only together with HTTPS.ALLOW_LOCAL_LOGIN_IN_PRODUCTION: production refuses to start with local password login unless this istrue; the expected production setup is OIDC-only (OIDC_ENABLED=trueplusOIDC_DISABLE_LOCAL_LOGIN=true).PAPERLESS_ALLOW_PRIVATE_URLS: leave empty to allow private/internal Paperless URLs in development and block them in production (SSRF protection); settruefor self-hosted setups with Paperless on a private network.INITIAL_TENANT_NAME: shared tenant name used once when existing single-user data is migrated; defaults toFamilie.INITIAL_TENANT_OWNER_EMAIL: required for the tenant migration when more than one user already exists.
Security defaults:
- API and frontend responses set CSP, frame, content-type, referrer, and permissions headers.
- Authentication uses an httpOnly session cookie instead of storing JWTs in
localStorage. - Production mode adds HSTS, rejects weak/default
SECRET_KEYvalues, and expects OIDC-only login unlessALLOW_LOCAL_LOGIN_IN_PRODUCTION=true. - Login attempts are rate-limited before password verification.
- Uploaded vehicle images and receipt PDFs are type-checked and size-limited.
- Vehicle images are served only through the authenticated
/api/vehicles/{id}/imageendpoint; there is no public static media route. - Calendar subscription links are signed with a per-user secret and can be revoked/rotated from the dashboard.
- Paperless URLs must be HTTP(S) URLs without embedded credentials; in production they must not point to private/internal addresses, and proxied previews/thumbnails are size-limited and do not follow redirects.
Privacy (GDPR) features:
- Users can export all of their data as JSON and delete their account (including owned single-member tenants, vehicles, records, and images) from the settings page (
GET /api/account/export,DELETE /api/account). /datenschutzand/impressumpages ship as templates; replace the bracketed placeholders with the operator's details before launch.- Only technically necessary cookies are used; vehicle image uploads are re-encoded, which strips EXIF/GPS metadata.
OpenID Connect Login
Karrage can add OIDC as a second login path next to the local email/password login. OIDC users are matched by their verified email address and receive the same internal JWT as local users.
Set these variables in .env:
OIDC_ENABLED=trueOIDC_PROVIDER_NAME: button label, for exampleKeycloakorAuthentik.OIDC_CLIENT_IDandOIDC_CLIENT_SECRET: client credentials from the provider.OIDC_DISCOVERY_URL: provider.well-known/openid-configurationURL.OIDC_REDIRECT_URI: callback registered with the provider, for local compose usuallyhttp://localhost:8000/api/auth/oidc/callback.OIDC_FRONTEND_REDIRECT_URL: frontend login URL, usuallyhttp://localhost:3000/login.OIDC_ALLOW_SIGNUP: whentrue, first OIDC login creates a user automatically.OIDC_REQUIRE_VERIFIED_EMAIL: whentrue, the provider must returnemail_verified: true.OIDC_GROUPS_CLAIM: optional claim name containing OIDC groups, for examplegroups.OIDC_GROUPS_PREFIX: optional prefix filter for OIDC groups, for examplekarrage:.
If the provider has no discovery document, configure OIDC_AUTHORIZATION_ENDPOINT, OIDC_TOKEN_ENDPOINT, and OIDC_USERINFO_ENDPOINT instead of OIDC_DISCOVERY_URL.
Migrations
The backend image starts through backend/entrypoint.sh. By default it runs:
alembic upgrade head
uvicorn app.main:app --host 0.0.0.0 --port 8000
You can run migrations manually:
sudo docker compose exec backend alembic upgrade head
For Kubernetes, prefer a dedicated migration Job or release step and set AUTO_MIGRATE=0 on the API Deployment when running multiple replicas.
flowchart TD
Start[Container start] --> Check{AUTO_MIGRATE=1?}
Check -->|yes| Alembic[alembic upgrade head]
Check -->|no| Skip[skip migrations]
Alembic --> Uvicorn[Start Uvicorn]
Skip --> Uvicorn
Uvicorn --> API[FastAPI serving traffic]
Container Deployment
The backend image has a useful default command, so it can run outside Docker Compose, for example with Podman Quadlets or Kubernetes.
Local Compose keeps development behavior separate:
- Backend source is bind-mounted.
UVICORN_RELOAD=1is set only in Compose.- Postgres and media are local development dependencies.
Production/container guidance:
- Inject secrets as environment variables or orchestrator secrets.
- Do not bake
.envinto images. - Mount persistent media storage at
MEDIA_ROOT, or replace it later with object storage. - Use
/api/healthfor liveness. - Use
/api/readyfor readiness because it verifies DB connectivity. - Run migrations once per release when using more than one backend replica.
Kubernetes Readiness
Karrage is close to cloud-native for a small stateful web app, but production Kubernetes should provide the following pieces:
Deploymentfor backend.Deploymentfor frontend.- External PostgreSQL or a Postgres operator.
SecretforDATABASE_URL,SECRET_KEY, API keys, and Paperless token-related configuration.ConfigMapfor non-sensitive settings.PersistentVolumeClaimforMEDIA_ROOT, unless media is moved to object storage.- Migration
Jobfor Alembic when running multiple backend replicas. - Ingress or Gateway with TLS termination.
Example probe shape:
livenessProbe:httpGet:path:/api/healthport:8000readinessProbe:httpGet:path:/api/readyport:8000Redis
Redis is not required for the current application. The backend is stateless apart from PostgreSQL and media storage.
Add Redis only when one of these appears:
- background jobs or scheduled workers,
- server-side caching,
- rate limiting,
- distributed locks,
- websocket fan-out,
- shared session/state coordination.
If Redis is added, keep it optional behind REDIS_URL so single-node, Quadlet, and small Compose deployments remain simple.
Fuel Data Quality
Rules for automatic outlier handling, smoothing, and CSV correction:
- Fuel metrics are recalculated after each relevant write.
- Full-tank routes produce raw consumption values.
- Implausible values are marked and smoothed.
- Manual corrections and CSV correction imports trigger recalculation.
Smart Mobile Fueling
The fuel form supports one-tap location lookup for mobile usage:
- Browser geolocation gets current coordinates.
- Backend requests nearby stations from the Tankerkoenig API using the tenant's own API key (settings UI), falling back to the global
TANKERKOENIG_API_KEYfor single-tenant selfhosting. - Form auto-fills
station_nameandprice_per_literbased on vehicle fuel type. - You only enter odometer and either liters or total cost.
Lookups are cached (about 1 km grid, 1 h TTL) and rate-limited per tenant so the upstream key is not overused. Price data: Tankerkoenig / MTS-K, licensed CC BY 4.0 (attribution shown in the settings UI and privacy page).
Paperless and OCR
Paperless integration is optional.
- Settings store Paperless URL/token on the user record.
- Maintenance and insurance forms can pick matching Paperless documents.
- The backend fetches Paperless previews through authenticated requests.
- OCR accepts authenticated image uploads, limits upload size/type, and extracts fuel receipt hints with Tesseract.
Users
There is no public registration flow in the UI. Create users from the backend CLI:
sudo docker compose exec backend python -m app.cli create-user user@example.com
Useful options:
--password "..."sets a password non-interactively.--migrateruns migrations before creating the user.
Migration from Hammond
Karrage includes a staged migration path from Hammond backups:
- Export a JSON dump from Hammond backup (
.tar.gz) orhammond.db. - Import vehicles from dump.
- Import fuel fillups into Karrage.
- Import expenses into maintenance, licensing, and other expense records.
1. Create Dump
sudo docker compose exec backend python -m app.cli dump-hammond-fillups \
--source /app/media/hammond_backup_2026年05月15日_120000.tar.gz \
--output /app/media/hammond_fillups_dump.json
2. One-Step Migration
Create dump, show summary, then confirm import with [y/N]:
sudo docker compose exec backend python -m app.cli migrate-hammond \
--source /app/media/hammond.db \
--user-email user@example.com
Optional flags:
--import-vehiclesimports vehicles first.--vehicle-map /app/media/vehicle_map.jsonapplies manual mapping.-yskips confirmation prompt.
3. Vehicle Mapping
If no vehicles exist yet in Karrage, import them from the dump first:
sudo docker compose exec backend python -m app.cli import-hammond-vehicles \
--dump /app/media/hammond_fillups_dump.json \
--user-email user@example.com \
--dry-run
Then execute without --dry-run.
If license plate or make/model/year cannot be matched automatically for fillups, create a map file:
{
"<hammond_vehicle_id>": "<karrage_vehicle_uuid>"
}
4. Import Fillups
sudo docker compose exec backend python -m app.cli import-hammond-fillups \
--dump /app/media/hammond_fillups_dump.json \
--user-email user@example.com
5. Import Expenses
Expenses from Hammond are imported into Karrage categories with a heuristic mapping:
- Maintenance-related expenses, such as inspection, tires, or repair, become maintenance logs.
- Tax/registration-like expenses become licensing records.
- Remaining expenses become other expenses.
sudo docker compose exec backend python -m app.cli import-hammond-expenses \
--dump /app/media/hammond_fillups_dump.json \
--user-email user@example.com \
--dry-run
Then execute without --dry-run.
The importer is idempotent for existing entries and skips duplicates based on vehicle, date, odometer, and total cost.
Recommended order:
- Vehicles
- Fillups
- Expenses
Insurance policies are managed directly in Karrage because Hammond expense exports do not contain enough contract metadata for provider, policy number, contract status, and cancellation deadlines.