Case study 01 · EAV Labs
EAV Insight
A FastAPI backend for document intake, operational reporting and searchable business records.

Built like a team depends on it
Built in EAV Labs as a production-style backend service: typed Python, environment-based configuration, Dockerised development, automated testing, CI and documentation discipline.
I built Insight in EAV Labs to practise the parts of backend work that tutorials tend to skip: configuration, migrations, tests, CI and deployment, done properly on a small but real service.
Records scattered across too many places
Business teams often store reports, documents, invoices and field records in scattered systems.
Searching, categorising and tracking those records gets harder as the organisation grows.
The service is for:
- Operations managers
- Analysts
- Field teams
- SMEs
- Document-heavy teams
What I did
I designed and built the service end to end: the data model and migrations, the versioned API, authentication and organisation scoping, the error and pagination conventions, the tests and CI, and the deployment to Oracle Cloud. There is no frontend; Insight is an API with its documentation.
Small modules, explicit contracts
A modular FastAPI service with versioned routes, Pydantic schemas, SQLAlchemy models and Alembic migrations, deployed from main through GitHub Actions to an OCI ARM64 VM.
I kept the service deliberately plain: clear module boundaries, schemas at every edge and a migration for every schema change, so each part can be tested and changed on its own.
Decisions that shaped it
Records are scoped to the caller’s organisation
Report and document endpoints require a bearer token and resolve the organisation from it, so clients never send organization_id.
Why: Taking the organisation from the token means no client can reach another organisation’s records by changing an ID. The trade-off is that every query has to carry that scope.
One error envelope for every failure
Every error returns code, message and details, with field-level details when validation fails.
Why: Clients handle one shape for every failure, and validation errors point at the exact field. The cost is a little mapping work for each new kind of error.
Pagination clients can build on
List responses carry total, count, has_next, has_previous and the next and previous offsets.
Why: Clients can build paging without guessing or counting rows themselves. The trade-off is a count query on every list request.
Production runs only what the code uses
Redis stays in local development but is left out of production, because the application does not consume it yet.
Why: Running only what the code uses keeps production simpler and cheaper to operate. Redis comes back when a feature actually needs it.
curl -H "Authorization: Bearer <access_token>" \ http://localhost:8000/api/v1/reports
{
"error": {
"code": "validation_error",
"message": "Request validation failed.",
"details": [
{
"field": "body.title",
"message": "String should have at least 2 characters",
"type": "string_too_short"
}
]
}
}Live, documented and deployable from main
The MVP deploys from main to Oracle Cloud through GitHub Actions and is live at insight.env.pm, with OpenAPI docs and a health endpoint.
It is small, but I can change it with confidence: every push runs the checks, and a merge to main reaches production through the pipeline rather than by hand.

