All Projects
Professional
Ongoing

Sportera

An Iranian fitness platform serving four audiences from one backend — public gym discovery, a member panel for body measurements and workout plans, a gym-owner panel, and a back-office dashboard — with a workout player that records every session against frozen target snapshots. Its Django implementation is currently being migrated to NestJS under a formal parity contract.

Overview

An Iranian fitness platform serving four audiences from one backend — public gym discovery, a member panel for body measurements and workout plans, a gym-owner panel, and a back-office dashboard — with a workout player that records every session against frozen target snapshots. Its Django implementation is currently being migrated to NestJS under a formal parity contract.

PythonDjangoDjango REST FrameworkTypeScriptNestJSPrismaBullMQPostgreSQLRedisHueyDockerNext.jsReact.jsViteTanStack QueryTanStack RouterTailwindCSSZodVitestClaude Code

Problem

Gyms in Iran are found by word of mouth and run on paper, while members who train seriously keep their programs in a notes app and their measurements nowhere at all. Serving the public site, the members, the gym owners and the administrators from one system is where such a platform usually goes wrong: four surfaces over the same data tend to grow four divergent permission models, and the one that leaks is rarely the one anybody tested.

Solution

Built the whole platform as three API scopes over one domain model — landing for the public, panel for the resource owner, dashboard for the back-office — with ownership enforced by scoping the query rather than by a permission guard, so a resource belonging to another user returns 404 instead of 403 and never discloses that it exists. The training domain is built on snapshots rather than references: starting a workout materializes every step upfront and freezes the exercise name, reps and targets as they were at that moment, so a session history stays truthful even after the plan behind it is edited or deleted. The Django system that runs today is now being replaced by a NestJS backend, migrated phase by phase against an endpoint-level parity matrix instead of rewritten from the product description.

Architecture

The production backend is Django + DRF over PostgreSQL, with Redis for cache and throttling and Huey carrying the single background task in the system — an SMS dispatch to Kavenegar. Nine domains (users, gym, training, measurements, content, intake, reference, dashboard and a shared toolkit) hold 38 models behind roughly 152 URL patterns split across the three scopes, under a layered pattern where selectors own reads and services own writes. Authentication is phone plus OTP issuing a JWT into HttpOnly cookies. Iran geography — 31 provinces, 1,117 cities and their neighbourhoods — is a static dataset indexed once at import rather than a set of tables. Three clients consume it: a Next.js landing site, a React 19 + Vite panel for gym owners, and a separate superuser admin SPA, both panels on TanStack Router and Query with Vitest suites. The replacement backend is a NestJS 11 modular monolith on Prisma and PostgreSQL with BullMQ for background work, organized as vertical feature slices under controller → service → repository layering, with UUIDv7 keys and a shared transport-validation layer that reproduces DRF field intake exactly, because that intake is part of the contract clients already depend on.

Challenges

  • Serving four audiences from one domain model without four permission systems: ownership is a query filter rather than a guard, which keeps authorization in the same place as the read and makes non-disclosure (404, never 403) the default rather than something each endpoint has to remember.

  • Making workout history immutable while the plans behind it stay editable — every session materializes its steps and target snapshots at start, and the live foreign keys are deliberately never read back for history, so deleting a plan cannot rewrite what somebody actually did.

  • Migrating a live backend to a different language and framework without changing behaviour: every endpoint carries a parity-matrix row with evidence, every field and query-parameter rename is recorded in a contract map, and the forty-three deviations from the source are individually approved — nothing else may differ.

  • Reproducing DRF field intake in TypeScript. What a CharField, ChoiceField or primary-key list actually accepts is contract, and re-implementing it per module produced eleven divergences before it was consolidated into one shared validation layer.

Outcomes

  • A production platform covering gym discovery, membership plans, schedules and facets, a plan bank marketplace, body-measurement series, and a full workout player with session history — 38 models and ~152 endpoints across three scopes.

  • Backend built and owned solo across roughly 490 commits, alongside the gym-owner panel and the superuser admin SPA.

  • A migration run as a contract rather than a rewrite: a phase roadmap with approval gates, an endpoint parity matrix, a field-level contract map, and a register where every deviation from the source system is numbered and signed off.

  • Transaction-sensitive and history-preserving workflows documented individually — which operations are atomic and which fields are snapshots — so the behaviour survives the change of framework instead of being rediscovered from the code.

Lessons Learned

  • Authorization belongs next to the read. Filtering by owner in the selector, rather than checking a permission after fetching, removes a whole class of leak — including the one where a 403 tells an attacker the record exists.

  • History has to be stored, not derived. If a session detail reads through a live foreign key, it is not history — it is the current state wearing a timestamp.

  • A rewrite disguised as a migration is the most expensive kind. Requiring per-endpoint parity evidence and a numbered approval for every deviation is slower per phase and far cheaper overall.

  • The edges of a framework are part of your API. Clients depend on what DRF quietly accepts and rejects, so reproducing that intake was not pedantry — it was the difference between a compatible backend and a nearly compatible one.

Team

Sajjad Fani

Sajjad Fani

You

Software Architect & Full-Stack Engineer

Software Engineer

Mohammad Hadian

Mohammad Hadian

Full-Stack Engineer

Frontend & Mobile Engineer

Pedram Mousapoor

Pedram Mousapoor

Product Designer

Product Designer

Professional
Software Architect & Full-Stack Engineer
Team of 3
October 2025 — Present