Overview

Sham is a platform product built on a Django + Django REST Framework backend that exposes a single JSON API to more than one consumer. Alongside the API lives sham_mobile, a companion mobile client that talks to the same endpoints the web surface does. The whole thing is containerised, fronted by Nginx, and shipped through a GitLab CI pipeline, with a backups directory kept inside the repository tree so recovery is a first-class concern rather than an afterthought.

Problem

Products that grow a mobile client after the web app usually grow a second backend with it — a thin BFF, a set of mobile-only endpoints, or worse, a parallel set of serialisers that drift from the originals within a release or two. Every business rule then has two homes, and every schema change becomes a coordination problem across two codebases and two release cadences. Sham needed the opposite: one authoritative API, one authentication story, one media pipeline, and one deployment artefact, with the mobile client treated as just another authenticated consumer of the same contract. The constraint was to keep that surface small enough to reason about while still handling the things real products need — token-based auth for stateless clients, image upload and processing, tabular and PDF exports, static asset delivery, and a repeatable path from commit to running container.

System Design

The backend is a Django 5 project with the REST layer isolated into a single api application, keeping models, serialisers, viewsets, and URL routing in one place rather than scattered across a dozen half-populated apps. Authentication runs on djangorestframework-simplejwt, which fits the shape of the problem: the mobile client has no cookie jar and no session affinity to lean on, so access and refresh tokens carried in the Authorization header let both clients authenticate against exactly the same code path. django-cors-headers sits in front of that to let the browser-based surface issue cross-origin requests against the API host without relaxing anything for other callers.

Media handling goes through Pillow, which is what Django's ImageField reaches for when it needs to validate and process uploads — dimension checks, format verification, and any resizing done on the way to storage. Static assets are served by whitenoise from inside the application process, which means the container is self-sufficient for statics and Nginx is free to do what it is actually good at: terminating connections, serving user-uploaded media off disk, and reverse-proxying everything else to gunicorn. Configuration is externalised through python-dotenv against a checked-in .env.example, so the same image runs in every environment with nothing baked in but code. The default persistence layer is SQLite (db.sqlite3), which keeps local development to a single manage.py migrate with no service dependencies.

Two operational concerns are handled as first-class code rather than tribal knowledge. Reporting is built on openpyxl for spreadsheet output and reportlab for PDF generation, both driven server-side so a mobile client can request a document without shipping a rendering engine. And bootstrapping is scripted: create_admin.py provisions the initial superuser non-interactively, while setup_categories.py seeds the category taxonomy the product depends on — two scripts that turn a fresh container into a usable instance without anybody clicking through the admin. Delivery is defined by a Dockerfile, a docker-compose.yml that wires the app to the nginx configuration, and a .gitlab-ci.yml that drives the pipeline, with backups/ holding the recovery tooling.

How It Works

01/04
01

Token-based client authentication

Both the web surface and the sham_mobile client obtain and refresh JWTs against the same DRF authentication path.

  1. 1A client posts credentials to the SimpleJWT token endpoint exposed by the api app
  2. 2The backend validates against Django's auth backend and returns a short-lived access token plus a longer-lived refresh token
  3. 3Subsequent API calls carry the access token in the Authorization: Bearer header, which DRF resolves to a request.user before any permission check
  4. 4When the access token expires, the client exchanges its refresh token for a new one rather than re-prompting for credentials
  5. 5Browser-origin requests additionally pass the django-cors-headers preflight check; the mobile client has no origin and hits the identical view
02

Image upload and media delivery

Uploaded images are validated by Pillow before storage and afterwards served directly by Nginx off a mounted volume.

  1. 1A client submits a multipart request against a DRF serialiser backed by a Django ImageField
  2. 2Pillow opens the uploaded file to verify it is a real image and to read its format and dimensions, rejecting anything malformed before it reaches the model
  3. 3The validated file is written to the media root, a volume mounted into the container so uploads survive image replacement
  4. 4On read, Nginx serves the file directly off that volume without waking the Python process
03

Server-side document export

Spreadsheet and PDF artefacts are generated on the server so neither client needs a rendering engine.

  1. 1A client requests an export from an API endpoint, scoped by the permissions already resolved from its JWT
  2. 2The view queries the relevant records and hands them to a generator
  3. 3For tabular output, openpyxl builds a workbook in memory; for print-ready output, reportlab composes a PDF
  4. 4The result streams back as an HTTP response with the appropriate content type and disposition, giving both clients an identical artefact
04

Build, deploy, and recovery

A GitLab CI pipeline turns a commit into a containerised instance running Gunicorn behind Nginx, with in-tree bootstrap and backup tooling.

  1. 1A push to GitLab triggers the .gitlab-ci.yml pipeline
  2. 2The Dockerfile builds the application image, with collected statics travelling inside it courtesy of whitenoise
  3. 3docker-compose.yml brings up the application under gunicorn alongside the nginx reverse proxy, with environment supplied from a .env file modelled on .env.example
  4. 4create_admin.py and setup_categories.py bootstrap a fresh instance into a usable state
  5. 5Tooling under backups/ captures the database and media so an instance can be restored rather than rebuilt

Key Features

  • Django REST API
  • Mobile client
  • Media handling
  • Containerised deployment
  • Backup tooling

Outcomes

  • One API for multiple clients
  • Container-ready delivery

More work