← Back to work
2023 · Scholarship platform · Odoo 16 + Django

Avataq

A hybrid Odoo 16 + Django platform managing Indigenous student scholarships, enrollment, billing and document handling. Two stacks on one Postgres database, with documents in S3 behind short-lived presigned download URLs.

Role
Full-stack implementation · architecture
Duration
Since 2023, ongoing
Team
1 Hazenfield engineer
PROJECT HERO · PLACEHOLDER
FIG. 01
Context

Avataq is a hybrid Odoo 16 + Django platform for managing Indigenous student scholarships, enrollment and document handling. Odoo holds the business logic — students, schools, sessions, scholar years, enrollment records, computing sheets for billing, reconciliation. A separate Django application serves the document explorer side: read-only access to uploaded scholarship documents with sixty-second presigned S3 download URLs, gated by role.

We built it end-to-end. Two applications sharing a single PostgreSQL database and communicating via REST. Documents live in AWS S3 under deterministic keys (`{status_no}/{scholar_year}/{slug}.{ext}`) so the key encodes context. Credentials live in `ir.config_parameter`, not in code. An `AvataqDocumentS3Sync` abstract mixin gives any Odoo model uploading capability for free; an `index_document` endpoint on the Django side upserts the index from an `X-API-Key`-gated POST.

Splitting the document explorer out of Odoo wasn't accidental. Odoo is great at modelling the business — students, sessions, computing sheets, reconciliation. It's less great at being a fast, focused viewer for thousands of binary records with per-role access. So the explorer lives in Django, reads the same database, asks S3 for short-lived presigned URLs, and never holds the documents in memory.

We've been on this platform since early 2023. The cadence is steady — new flows as the scholarship workflows expand, document-flow refinements as the cohort grows, S3 backups running nightly.

Scope

What we built.

avataq01

Main Odoo addon — students, inscriptions, sessions, schools, computing sheets, reconciliation, mail templates, S3 document upload, Django sync.

avataq_website02

Public portal and website templates.

avataq_signup03

Auth and signup customisation.

s3_backup04

Scheduled S3 database backups.

Django document explorer05

Read-only viewer over the same Postgres. Permission-gated by `documents.can_view_documents`. Mints presigned S3 URLs on demand.

AvataqDocumentS3Sync mixin06

Abstract Odoo mixin — drop the inherit, get S3 upload + Django index notification for free. Used across three uploader models.

Cross-stack REST sync07

Odoo POSTs document metadata to Django `/api/index/` after every successful upload, gated by `X-API-Key`. Django upserts the index row; never writes back.

Deployment scaffold08

docker-compose for db / odoo / django; shared `avataq_env` conda env across both stacks; one `deploy.sh` for production rollouts.

Approach

What the work looked like, in four pieces.

01

Two stacks, one database

Splitting the document explorer out of Odoo wasn't accidental. Odoo is great at modelling the business; less great at being a fast, focused binary viewer with per-role access. So the explorer lives in Django, reads the same Postgres, asks S3 for short-lived presigned URLs, and never holds the documents in memory.

02

An abstract mixin for the upload pattern

Three Odoo models needed S3 upload behaviour with the same conventions. We wrote `AvataqDocumentS3Sync` as an abstract mixin — drop the inherit, get key generation, S3 upload and Django-side index notification for free. New uploaders are a one-line inherit.

03

Credentials in config_parameter

Everything sensitive lives in `ir.config_parameter`: the S3 bucket, region, access key, secret key, Django API URL and API key. No deploy-time substitution, no committed secrets, no surprise when an environment is missing a key.

04

Documents that don't sit in memory

Uploaded documents go straight to S3 with a deterministic key. Odoo never holds them after the upload finishes. Authenticated Django users get a sixty-second presigned URL when they request a download — long enough to click, short enough to never get shared usefully.

Engineering highlights

A handful of the solves we are proudest of.

01

AvataqDocumentS3Sync abstract mixin

Three Odoo models needed identical S3-upload behaviour. We wrote one abstract mixin: the inheritor declares its files, the mixin handles key generation, S3 upload, and a POST to the Django `/api/index/` endpoint with `X-API-Key`. New uploaders cost a one-line inherit.

02

Cross-stack sync via REST

Odoo is the writer; Django is the indexer. After every successful S3 upload, Odoo POSTs metadata (student_reference, scholar_year, s3_key, filename) to Django's `/api/index/` endpoint, gated by an `X-API-Key` header. Django upserts a `DocumentIndex` row. The Django side never writes to S3 or to Odoo's tables.

03

60-second presigned S3 URLs

Document downloads go through Django, which mints a 60-second presigned S3 URL on demand. Long enough to click, short enough that a shared URL is dead by the time it could be misused. No long-lived public links.

04

Deterministic S3 keys

Object keys encode their context: `{status_no}/{scholar_year}/{slug}.{ext}`. Two operations come for free: per-cohort listing is just an S3 prefix scan, and migrating documents between environments is a key-renaming exercise rather than a metadata join.

Outcomes

A few shapes, in their raw form.

3+ yrs
Continuous since 2023
2 stacks
Odoo + Django on one Postgres
60 s
Presigned S3 URL TTL
End-to-end
One Hazenfield engineer

Stack
Odoo 16Django 4.2PythonPostgreSQLAWS S3RESTDockerGunicorn

Have a project that deserves this kind of care?

Start a conversation