← Back to home
Specialism · Django engineering

Django is where we putthe part Odoo shouldn't carry.

Three platforms in production where Django sits alongside something else — Odoo, S3, OAuth2 clients, a React frontend. Multi-tenant via Postgres RLS, parallel test suites that finish in minutes, and a long memory for the parts of Django that matter when load arrives.

Platforms in production
3
Largest test suite
2,220 (Exiqtive)
Fastest, in parallel
~2.5 min
Status
Open for Q3 2026
Overview

Django is the second pillar of the workshop. Where Odoo is the platform we choose when the business shape is already partly modelled — customers, contracts, accounting — Django is the platform we choose when a system needs its own shape, or when Odoo is right for one part of the stack and wrong for another.

Three of the platforms in our hands today have a Django tier. Exiqtive is Django all the way down — DRF + Celery + Channels, multi-tenant via Postgres RLS, two thousand two hundred tests in two and a half minutes. MySpecialist's Django app sits next to Odoo on a shared Postgres, rebuilt out of a Symfony tier we inherited in 2022. Avataq's Django document explorer reads the same database Odoo writes to, and hands out sixty-second presigned S3 URLs on demand.

The architectural decisions tend to compound. A multi-tenant boundary that lives at the Postgres RLS level can't leak through a forgotten queryset filter; an OAuth2ScopesPermission class on every viewset enforces a scope taxonomy that the frontend cannot route around; a `transaction.on_commit` discipline on Celery cascades means the next stage never runs against half-committed state. Two and a half years of Exiqtive in production, still on the architecture we drew on day one.

What this practice covers

The scope, without the shopping list.

Multi-tenant by row, not by app01

Tenant isolation enforced at the Postgres level by RLS, not just by the application layer. RLSMiddleware sets the session variable per request; permission classes layer on top; the database refuses cross-tenant reads even when an application bug would have let them through.

OAuth2 & scope-gated DRF02

django-oauth-toolkit for issuance; OAuth2ScopesPermission on every viewset; scopes split read / write / admin per resource family (employees:read, employees:write, assignments:recalc). WebSocket connections authenticate the same way. No session cookies on the frontend.

Async cascades that respect the DB03

Celery + `transaction.on_commit` so the next stage only fires after the previous write commits. Retries with backoff; Redis locks to prevent concurrent recalcs of the same record. Five-level cascades that flow bottom-up through an org tree.

API versioning that lets contracts age04

Parallel urls/v1.py, urls/v2.py, urls/shared.py per app; a CustomVersioning class that dispatches by query string; both versions first-class in the test suite until a contract sunsets. Paying customers do not get migrated on our schedule.

Channels & WebSocket05

Django Channels for real-time delivery, OAuth2-authenticated tenant connections, channel routing supporting per-entity paths. WebSocket tests run inside the same parallel infrastructure as HTTP tests.

Hybrid with Odoo on one Postgres06

A Django app reading the Odoo database for its own purposes — a document explorer (Avataq), the marketplace tools that fit better outside Odoo (MySpecialist). Clear ORM boundaries; Django never writes where Odoo owns.

Test infrastructure that pays for itself07

--keepdb --parallel N, ALWAYS_EAGER for Celery in tests, InMemoryChannelLayer for Channels, LocMemCache for cache, a fast password hasher. Thirteen-minute suites down to two and a half on an 8-core machine. 80 % coverage gated in CI.

Migration & rescue08

Symfony → Django (MySpecialist, year one of the engagement). New Django tier on an existing Postgres (Avataq). Architecture written down so a growing team can plug into the system as it scales.

Signature engineering

A handful of solves we still remember.

01

RLS-enforced multi-tenancy (Exiqtive)

Postgres row-level security on tenanted tables; RLSMiddleware sets the session variable before every request; eight permission classes layered on top. A bug in a queryset filter cannot leak across accounts — the database refuses. Belt and braces with the application-layer permissions.

02

Five-level readiness cascade (Exiqtive)

Readiness flows responsibility → role → position assignment → employee → account through Celery tasks gated by `transaction.on_commit`. Retries with max_retries=3, default_retry_delay=60. Public helpers (trigger_readiness_cascade_from_responsibility(id)) are the entry points; signals call those, never the underlying tasks directly.

03

Parallel test infrastructure, 13 min → 2.5 min

--keepdb --parallel N with N clone databases; ALWAYS_EAGER for Celery in tests; InMemoryChannelLayer for Channels; LocMemCache for cache; a faster password hasher. Coverage gate at 80 % in CI. 2,220 tests in two and a half minutes on an 8-core machine.

04

API versioning that lets contracts age (Exiqtive)

Each app carries parallel urls/v1.py, urls/v2.py, urls/shared.py plus parallel serializer and viewset trees. A CustomVersioning class reads ?version= and dispatches. Both versions are first-class citizens of the test suite until a contract sunsets.

05

Symfony → Django rescue (MySpecialist)

Year one of the engagement was largely a rebuild. The Symfony tier — hand-rolled forms, no test suite, internal-tool layer entangled with business logic — was rebuilt as a Django app sharing the Odoo Postgres. New features in days, not weeks. Four years on, same codebase, same team.

06

Django document explorer over Odoo Postgres (Avataq)

Read-only viewer permission-gated by documents.can_view_documents. Mints a sixty-second presigned S3 URL on demand. Django never writes to S3 or to Odoo's tables; documents never sit in memory. Two stacks, one Postgres, one source of truth.

Have a project that deserves this kind of care?

Start a conversation