Never lose a
warranty
again.

Scan a receipt and Reclaima handles the rest — per-item extraction, deadline tracking, and a ready-to-submit claim PDF when something goes wrong.

View on GitHub
android In development for Google Play
Reclaima dashboard
Flutter Flutter
FastAPI FastAPI
PostgreSQL PostgreSQL
AWS AWS
Firebase Firebase
Docker Docker
GitHub Actions GitHub Actions
KrakenD KrakenD
The problem

Warranties are built to be forgotten.

Most people have no idea when their warranties expire. Receipts get lost, warranty cards go unfiled, and return windows close before anyone notices. When something breaks, the process of finding proof of purchase, identifying warranty coverage, and drafting a claim is manual, scattered, and frustrating enough that most people don't bother.

The tools that exist today solve half the problem at best. Business expense apps have strong OCR but treat receipts purely as financial records — no warranty tracking, no return window, no claim support. Dedicated warranty trackers let you log products and set reminders, but rely on manual entry, treat each receipt as a single item, and stop at the notification. None of them generate anything you can actually submit to a retailer or manufacturer.

No consumer app handles the complete lifecycle: scan a receipt, track every item on it, get reminded before deadlines, and produce a claim document when something goes wrong.

The Solution

What Reclaima does differently

Reclaima covers the full purchase-to-claim lifecycle in one place.

Receipts are scanned using AWS Textract with a Claude Haiku cleanup layer that handles noisy, locally formatted receipts — including bilingual layouts. Every line item is extracted individually, so a single receipt with five products creates five independently tracked entries, each with its own warranty period, return window, and reminder schedule.

When a product fails, Reclaima generates a structured PDF warranty claim document — with purchase details, issue description, and defect photos attached — ready to submit directly to a retailer or manufacturer. No competitor in the consumer space does this.

How it works

Four steps.
Zero lost warranties.

1

Capture

Snap a photo, pick from gallery, or upload a PDF. Front and back both sides supported. Up to 20 MB per upload.

2

Extract

AWS Textract reads every line. Claude Haiku fixes OCR noise — garbled names, transposed digits, broken totals.

3

Track

Each line item gets its own warranty and return countdown with configurable FCM push reminders.

4

Claim

One tap generates a claim-ready PDF with purchase details, defect photos, and retailer contact.

Features

Built for the full lifecycle.

From photographing a receipt to filing a warranty claim — Reclaima covers every step at the per-item level.

auto_awesome

AI-powered OCR

Dual-layer extraction using AWS Textract AnalyzeExpense for raw structured data and Claude Haiku for intelligent cleanup, normalization, and field inference across six specialised extraction passes.

AWS Textract · Claude Haiku
receipt_long

Per-unit item extraction

Not just totals — every line item becomes a tracked product with its own expiry dates, category, and product code. If a line item has quantity greater than one, Reclaima creates that many individual records, each independently tracked with its own warranty period, return window, and reminder schedule.

Per-unit tracking · Itemization
timer

Countdown tracking

Warranty and return windows shown as live day countdowns, color-coded by urgency: green → amber → red as deadlines close.

Per-item urgency
wifi_off

Offline-first

Every receipt, line item and claim is stored in Drift/SQLite and synced to the backend when online. Warranty data remains fully accessible without an internet connection.

Drift · SQLite
notifications_active

Per-item smart notifications

APScheduler fires background jobs that push FCM notifications before warranty and return windows close. Each product has independent lead day overrides for warranty and return reminders, individual enable/disable toggles, and tapping a notification deep links directly to that specific product screen.

FCM · APScheduler · Deep links
adf_scanner

Claim PDF generation

Generates a structured, print-ready A4 PDF containing purchase details, warranty dates, store contact information, and full receipt images. Up to 10 defect photos can be attached — each included as a full page — making the document ready to submit directly to a retailer or manufacturer.

ReportLab
layers

Dual-sided receipt scanning

Both front and back receipt images are processed separately through the OCR pipeline. Results are merged — front image provides header fields (store, date, total), line items from both sides are combined into a single structured output. Both images are included in the generated claim PDF.

Front & back · OCR merge
track_changes

Claim lifecycle management

Every warranty claim has a full status lifecycle: DRAFT → SUBMITTED → IN_PROGRESS → RESOLVED / DENIED. On resolution, three outcomes are supported — REFUNDED (item archived), REPAIRED (item stays active, user prompted to update warranty dates), REPLACED (original archived, replacement item created and linked). Replacement chains are tracked via linked records.

DRAFT → RESOLVED
image_search

Product image auto-fetch

After OCR processing, Reclaima automatically fetches a product image for each line item via Brave Image Search, ranked by domain trust — preferring official brand and retailer sources over marketplaces and aggregators. Images are stored and displayed on the product detail screen.

Brave Search
In the wild

See it in action.

Real screenshots from the app — from scanning a receipt to generating a claim PDF.

Home dashboard Dashboard
Add a receipt Add a Receipt
Crop and rotate receipt Crop & Rotate
Review extracted details Review Details
Confirm and save receipt Confirm & Save
Per-item warranty coverage Per-item Coverage
Product details screen Product Details
New claim form New Claim
Claim PDF ready Claim PDF Ready
Resolve claim outcome Resolve Claim
System design

Architecture at a glance.

Modular monolith designed for microservices extraction. Mobile app and backend are independently deployable; all cloud services sit behind service interfaces.

phone_iphone
Flutter Mobile App — iOS & Android
Riverpod state management · Drift/SQLite offline store · Firebase Auth SDK · Firebase Cloud Messaging · Dio HTTP
Firebase JWT · HTTPS
hub
KrakenD API Gateway
port 8080 · Firebase JWT validation (auth/validator on protected endpoints, RS256 against Google JWKs) · rate limiting (qos/ratelimit on all endpoints) · request routing to backend
proxied requests · port 8000
dns
FastAPI Backend — OCI Compute (Docker)
Python 3.11 · SQLAlchemy ORM · Alembic migrations · APScheduler · Pydantic v2 · Firebase Admin JWT verification (defence-in-depth behind gateway)
Service layer
database
PostgreSQL
Receipts · items · claims · users
cloud_upload
AWS S3
Receipt images · claim PDFs · cascading deletion on record removal
document_scanner
AWS Textract
AnalyzeExpense API · structured expense fields · individual line items
smart_toy
AWS Bedrock
Claude Haiku · OCR cleanup
notifications
FCM
Push reminders · Device tokens
Tech stack

The right tool for each layer.

dns

Backend

Python 3.11 · deployed as a Dockerised FastAPI service on OCI Compute

FastAPI PostgreSQL SQLAlchemy ORM Alembic Pydantic v2 Firebase Admin SDK AWS S3 AWS Textract AWS Bedrock Claude Haiku APScheduler Sentry Docker OCI Compute KrakenD Brave Search API ReportLab
phone_iphone

Mobile

Flutter 3 · cross-platform iOS & Android · offline-first with local SQLite

Flutter 3 Riverpod Drift / SQLite Firebase Auth Firebase Cloud Messaging Dio Google Sign-In
AI pipeline

From photo to structured data.

Typically under ~10s — varies by image size, PDF complexity, and network latency.

01 · Input
1
Receipt file
JPEG · PNG · PDF · up to 20 MB · MIME validated on upload
02 · Storage
2
S3 upload
Pre-signed URL · permanent key · staging path for extraction
03 · OCR
3
AWS Textract
AnalyzeExpense · purpose-built for receipts & invoices · structured expense fields · individual line items · per-field confidence scores
04 · LLM
4
Claude Haiku
Six specialised extraction passes: store name · product names · receipt notes · phone · email · address — each with its own targeted prompt
05 · Output
5
Structured JSON
Vendor · invoice · line items · totals · dates · warranty periods
06 · Persist
6
PostgreSQL + Drift
Synced to backend · cached locally in SQLite for offline access
Python · FastAPIPOST /api/v1/receipts/ocr-extract
# Stateless — no DB write. Caller decides to persist.
@router.post("/ocr-extract", response_model=OcrExtractResponse)
async def ocr_extract(
    front_image: Optional[UploadFile] = File(None),
    back_image: Optional[UploadFile] = File(None),
    current_user: dict = Depends(get_current_user),
    db: Session = Depends(get_db),
):
    # 1 · Upload front + optional back image to S3
    front_key = s3_svc.upload_file(front_image)
    back_key = s3_svc.upload_file(back_image) if back_image else None

    # 2 · Textract AnalyzeExpense — six LLM cleanup passes
    #     run synchronously inside analyze_document()
    data = textract_svc.analyze_document(front_key)

    return OcrExtractResponse(...)
JSON · responseReceiptExtractResponse
{
  "s3ObjectKey":   "users/uid/receipts/abc/front.jpg",
  "ocrStatus":     "success",
  "storeName":     "Best Buy",
  "invoiceNumber": "BBY-20240312-8841",
  "purchaseDate":  "2024-03-12T00:00:00",
  "currency":      "USD",
  "totalAmount":   349.99,
  "lineItems": [
    {
      "rowIndex":        0,
      "itemDescription": "Sony WH-1000XM5",
      "quantity":        1,
      "unitPrice":       349.99,
      "productCategory": "Electronics"
    }
  ]
}
Python · LLM servicellm_service.py · BedrockLLMService — synchronous, no async/await
# Six methods — each formats its own prompt and calls Bedrock directly.
# Prompt constants live in app/core/prompts.py.

def clean_store_name(self, text: str) -> str:
    prompt = STORE_NAME_PROMPT.format(text=text.strip())
    raw = self._client.invoke_model(
        modelId=self._model_id,  # "us.anthropic.claude-haiku-4-5-20251001-v1:0"
        body=json.dumps({"anthropic_version": "bedrock-2023-05-31",
                          "max_tokens": 50, "temperature": 0.1,
                          "messages": [{"role": "user", "content": prompt}]}),
        contentType="application/json", accept="application/json",
    )
    return json.loads(raw["body"].read())["content"][0]["text"].strip()

# extract_product_name() → PRODUCT_NAME_PROMPT  (max_tokens=100)
# clean_receipt_notes()  → CLEANUP_PROMPT        (max_tokens=512)
# clean_phone_number()   → PHONE_NUMBER_PROMPT   (max_tokens=50)
# clean_email()          → EMAIL_PROMPT           (max_tokens=50)
# clean_address()        → ADDRESS_PROMPT         (max_tokens=150)
API reference

Versioned REST API.

All routes under /api/v1 — Firebase JWT verified on every authenticated request via Firebase Admin SDK.

Method Endpoint Description
POST /auth/register Register or retrieve an existing user after Firebase authentication (idempotent)
GET /auth/me Return authenticated user profile
POST /receipts/ocr-extract Upload image/PDF → S3 → Textract → Claude Haiku → structured JSON. Stateless — no DB write.
POST /receipts Create a receipt record from OCR-reviewed data
GET /receipts/{id}/image-url Return a pre-signed S3 URL for the receipt image
GET /warranties List all warranty records with expiry countdowns
GET /warranties/returns List return deadlines with day countdowns — separate from warranty tracking
POST /claims Initiate a warranty or return claim; accepts defect images via multipart
POST /claims/{id}/resolve Resolve a claim — records outcome (refunded / repaired / replaced) and optionally links a replacement line item
POST /claims/{id}/pdf-access Return a pre-signed S3 URL for the stored claim PDF; regenerates via ReportLab only if the file is missing from S3
POST /products/image-search Brave Search API proxy — returns product image URL for a query string
PATCH /notifications/fcm-token Register or clear a device FCM token for push notification delivery
GET /health Liveness probe — OCI health checks and CI gate

Selected endpoints only — the full API spans 30 endpoints across 6 resource domains.

CI / CD

Automated quality gates on every push.

Six GitHub Actions jobs gate every push to main — the backend ships only when all pass.

Trigger
Push / PR
main · any open PR · workflow_dispatch
Security
Secret Scan
Gitleaks · gates all downstream jobs
parallel
Quality gate
Backend CI
ruff · black · mypy · pytest ≥70% · alembic · bandit · pip-audit
Quality gate
Mobile CI
build_runner · dart format · flutter analyze · test ≥60% · Android APK
Config check
KrakenD
krakend check · official Docker image
Build · main only
Docker Build
Multi-arch arm64 + amd64 · Trivy · SBOM · Cosign · GHCR sha + latest-prod
Deploy · main only
OCI Deploy
SCP assets · oci_deploy.sh · smoke test · Slack on failure
Decisions log

Key technical decisions.

The reasoning behind the choices that shaped Reclaima — gateway, scheduler, hosting, secrets, OCR pipeline, framework, and the CI/CD checks that gate every deploy.

Architecture & Infrastructure

Why a modular monolith over microservices? expand_more
Microservices introduce network calls between services, distributed tracing, separate deployment pipelines, and eventually separate databases — all of which add operational complexity that has to be justified by the scale or team size. The backend has clean internal module boundaries (receipts, warranties, claims, notifications, users) without any of that overhead. A single deployment unit, a single database with proper schema separation, and straightforward debugging. The scheduler runs in a separate container purely for process isolation, not because it needs to be an independent service. Microservices would be the right call if different modules needed to scale independently or if separate teams owned them — neither applies here.
Why OCI ARM VM instead of AWS EC2? expand_more
OCI's Always Free tier gives a 4-core ARM Ampere VM with 24 GB RAM and no 12-month expiry. AWS Free Tier expires after a year and the equivalent instance size costs money beyond that. For a pre-revenue app, eliminating infrastructure cost entirely while still running a full production stack — gateway, API, scheduler, database, monitoring — is the obvious call.
Why a dedicated secrets manager, and why Infisical specifically? expand_more
Storing credentials in a .env file on the server means they sit on disk permanently, get included in VM snapshots, and require manual SSH access to rotate. AWS Secrets Manager solves the disk problem but charges per secret per month plus per API call — costs that add up quickly when secrets are fetched on every container restart. HashiCorp Vault is powerful but moved from open-source to a source-available licence in 2023, requires running your own server or paying for a managed plan, and has significant engineering overhead to configure workflows and integrations from scratch. Infisical is MIT-licensed, self-hostable, and has a free cloud tier that covers unlimited secrets, environments, machine identity auth, GitHub Actions integration, and a CLI — everything this stack needs at no cost. In practice, the deploy script fetches database credentials from Infisical before containers start, and the application fetches the remaining secrets at startup. No credentials are written to the server filesystem at any point.
Why KrakenD instead of a plain reverse proxy like Nginx or Caddy? expand_more
KrakenD validates Firebase JWTs at the gateway using Firebase's public JWK endpoint — FastAPI never sees an unauthenticated request. Rate limits are also enforced per endpoint at this layer, with tighter limits on auth routes and looser limits on read endpoints. With a plain reverse proxy you'd have to reimplement both in every service or add middleware. The backend port is also intentionally not exposed in the production compose file — all traffic must enter through the gateway.
Why a separate container for the scheduler instead of running it inside the FastAPI process? expand_more
APScheduler running inside the same uvicorn process means a misbehaving job — a memory leak, an unhandled exception loop, a slow DB query — competes directly with request handling and can bring down the API. A separate container isolates that: the API stays up regardless of what the scheduler does. The alternative would be Celery with Redis, which gives you a proper job queue, distributed workers, and retry visibility — but that's significant infrastructure overhead for two nightly reminder jobs and one cleanup job. For the current scale, two containers achieve the failure isolation without the complexity.

Stack & Tools

Why a monorepo? expand_more
The Flutter frontend, FastAPI backend, KrakenD config, and all deployment scripts live in one repository. A feature that touches the backend API and the mobile client can be a single atomic commit and PR — no coordinating changes across multiple repositories. The CI/CD pipeline has one .github/workflows directory that sees the full picture and can run backend and mobile checks in parallel. With polyrepo, you'd be managing separate issue trackers, separate branch protection rules, and the mental overhead of keeping related changes in sync across repos.
Why Python for the backend? expand_more
Python requires significantly less boilerplate for this type of API than most alternatives — a FastAPI endpoint with full Pydantic validation and serialization is a fraction of the code that the equivalent Spring Boot controller, DTO, and service classes would require in Java. The OCR cleanup work is fundamentally string manipulation and data transformation across multiple passes; Python's standard library and data processing idioms handle this more concisely than most other languages. APScheduler is a few lines of setup for background jobs. SQLAlchemy, Alembic, and FastAPI are all mature, well-documented, and designed to work together — the ecosystem fit for a data-processing API with AI service integrations is strong.
Why FastAPI over Django REST Framework or Flask? expand_more
FastAPI generates OpenAPI documentation automatically from Pydantic models — the /docs endpoint is always accurate with no extra configuration. Pydantic v2 handles request validation, serialization, and settings management consistently across the entire codebase. Native async support matters for non-blocking calls to Textract and Bedrock. Flask requires assembling extensions for every piece of functionality; Django REST Framework is opinionated around Django's ORM, which conflicts with the SQLAlchemy and Alembic setup. FastAPI gives the right defaults without forcing a framework's assumptions onto the data layer.
Why Flutter over React Native? expand_more
Flutter compiles to native ARM code rather than bridging to native components at runtime, which eliminates a category of platform-specific rendering quirks and performance inconsistencies. Dart's strong typing catches more bugs at compile time than JavaScript would. Drift, the offline-first SQLite layer, has excellent Flutter integration — the generated Dart code fits naturally into the Riverpod state management pattern used throughout the app. React Native would have worked, but Flutter's consistent rendering model and the Drift ecosystem made it the cleaner choice for an offline-first mobile app.
Why Firebase Auth over rolling your own? expand_more
Authentication is easy to implement incorrectly — token rotation, refresh token security, session invalidation, and OAuth flows each have well-documented failure modes. Firebase Auth handles Google Sign-In, token issuance, refresh, and revocation on a free tier that covers the app's scale. Device tokens for push notifications are also managed through Firebase, so auth and FCM stay unified. The custom work needed is minimal: KrakenD validates the JWT at the gateway using Firebase's public JWK endpoint, and FastAPI verifies it as defence-in-depth using Firebase Admin SDK — both are thin integration layers, not custom auth logic.
Why AWS Textract with a Claude Haiku cleanup layer instead of a single solution? expand_more
Textract's AnalyzeExpense API is purpose-built for receipts — it returns vendor name, total, individual line items, dates, and tax as structured fields without needing a custom model or training data. Google Document AI and Azure AI Document Intelligence both offer custom model training, which is their primary advantage, but that requires labelled training data and ongoing maintenance that's unnecessary when a prebuilt expense model already covers the use case. The entire stack is also already on AWS, so Textract integrates directly with S3 and Bedrock without cross-cloud authentication or egress costs. Where Textract falls short is bilingual receipts and multi-column layouts where text gets garbled or fields get transposed. Claude Haiku on Bedrock handles that with six targeted cleanup passes — store name, product names, warranty notes, phone, email, address — each with its own prompt. Using only an LLM would be slower and more expensive for structured extraction; using only Textract leaves the cleanup problem unsolved.

CI/CD

Why does secret scanning gate every other CI job? expand_more
A credential leak in a commit is a more serious problem than a failing test — it's an immediate security incident that requires key rotation regardless of whether the code works. Running backend CI and mobile CI in parallel with the secret scan means those jobs execute against a potentially compromised commit. Gitleaks as the first job, with all other jobs depending on it, ensures nothing else runs until the commit is confirmed clean. Gitleaks also scans the full git history, not just the diff — catching credentials that were added in an earlier commit and "removed" with a follow-up commit, which still exist in the history.
Why these backend CI checks (ruff, Black, mypy, pytest, alembic check, Bandit, pip-audit)? expand_more
Ruff replaces flake8 and isort in a single fast pass. Black enforces consistent formatting. Mypy catches type errors at the Pydantic model boundaries before they surface as runtime 422 errors. Pytest with a 70% coverage floor ensures core business logic — OCR processing, warranty calculations, claim generation — is tested before anything reaches production. Alembic check verifies migration files are consistent with the SQLAlchemy models, catching the common mistake of updating a model without generating a corresponding migration. Bandit does static analysis for Python security issues — hardcoded credentials, unsafe deserialization, weak hashing — that code review tends to miss. pip-audit checks every dependency against the CVE database; a well-written application running a vulnerable dependency is still a liability.
Why these Flutter CI checks (build_runner, dart format, flutter analyze, test coverage, APK build)? expand_more
build_runner generates Drift database classes, Riverpod providers, and JSON serializers — if the generated files are stale the app won't compile, so regenerating them in CI catches drift between source and generated code early. dart format and flutter analyze enforce style and catch semantic issues. The coverage floor sits at 60% rather than 70% because UI widget testing is significantly harder than unit testing, but 60% covers the data layer and core business logic. The APK build step verifies the release artifact actually compiles — tests can pass while the release build fails due to tree-shaking or build configuration issues.
Why validate KrakenD config in CI? expand_more
krakend.tmpl uses flexible config template syntax — a typo or invalid endpoint configuration doesn't produce a compile error, it produces a gateway that fails to start at deploy time. krakend check run against the official KrakenD Docker image catches syntax errors, invalid route configs, and missing required fields before the config reaches production. Without this check, a misconfigured gateway would pass all CI jobs, deploy successfully, and then silently fail to start — taking down the entire API with no code error to point to.
Why GHCR over Docker Hub? expand_more
GHCR authenticates using GITHUB_TOKEN, which is automatically available in every GitHub Actions run — no separate registry credentials to create, rotate, or store as secrets. Images are stored in the same GitHub organisation as the code, so access control is unified with repository permissions. Docker Hub's free tier rate-limits unauthenticated pulls, which causes intermittent CI failures when pulling base images during builds. Keeping the built image in GHCR means the full pipeline — source code, CI config, and built images — lives in one place with one access model.
Why multi-arch Docker build (arm64 + amd64)? expand_more
The OCI VM runs on ARM Ampere. An amd64-only image would run under emulation on the VM, losing the performance advantage of the ARM architecture entirely. Building both architectures means the same image tag works natively on the OCI ARM VM in production and on amd64 development or CI machines without any emulation overhead. Docker BuildKit handles both architectures in a single build step via --platform linux/arm64,linux/amd64.
Why Cosign, SBOM, and Trivy in the build stage? expand_more
Trivy scans the built image for CVEs in OS packages and application dependencies before it's pushed to the registry — catching vulnerabilities introduced by base image layers that pass code-level security scans. The SBOM is a manifest of every package in the image, useful for auditing exactly what's running in production and for responding quickly if a new CVE affects a specific package. Cosign signs the image with a keyless signature tied to the GitHub Actions OIDC identity — proving the image was built by this specific workflow and hasn't been tampered with between the registry and the VM.
Why smoke test with auto-rollback on deploy? expand_more
A deployment can pass every CI check, build a valid image, and still fail to start in production — a bad migration, a missing environment variable, or a startup exception would leave the app down. The smoke test polls /api/v1/health twenty times at three-second intervals after the new containers start, giving Infisical time to fetch secrets and the database time to accept connections. If the health check doesn't pass within that window, oci_deploy.sh automatically rolls back to the previous image tag and reverts the Alembic migration. Production is restored without manual intervention, and the failed deploy leaves a clear signal in the CI logs.