Game Studio Management Portal
Teams, Releases & Ad-Mediation Tracking
- Python
- Django
- PostgreSQL
- JavaScript
A role-controlled internal web application built with Django for a multi-team game-development company. It is the single source of truth for who builds what: it tracks every team and its developer roster, the games each team owns, every version/release of those games, the per-release changelog, and the ad-mediation monetization stack behind each release, all behind a strict per-team access model.
Role: Full-stack developer who designed the relational data model, built the role-based access-control engine, all server-rendered CRUD UI, cross-entity validation logic, the interactive mediation-stack form, and the hand-written CSS theme.
Table of Contents
- The Problem
- What the Application Does
- Tech Stack
- Architecture Overview
- Roles & Access Model
- Core Features
- Data Model
- Notable Engineering Challenges & Solutions
- Security & Access Control
- Project Stats
- Possible Future Improvements
The Problem
A game studio that runs multiple independent product teams (each building its own portfolio of mobile games) had no shared, structured place to record how its games actually evolve. The operational reality was scattered and informal:
- No release history. Every game ships dozens of versions over its life (a Unity-engine update, a bug-fix pass, a monetization tweak). The version number, the Unity version it was built on, why the update shipped, and whether the objective was actually met lived in chat threads and memory.
- No attribution. When a release went out, there was no record of which developers did the work, making it hard to review workload, credit contributions, or trace a regression back to a release.
- Untracked ad-mediation stacks. The studio monetizes its games through ad-mediation platforms (each wrapping several ad networks). Every release pins specific mediation-SDK versions and assigns each platform a priority. None of that was recorded against the release it belonged to.
- No changelogs. Features added, bugs fixed, and improvements per release were never written down in a consistent, searchable form.
- No data isolation. Each team should only see and manage its own teams, games, and releases, but a shared spreadsheet gives everyone everything, and nothing enforces that a developer is attributed only to a game their team owns.
This application replaces that informality with one structured, role-scoped portal where teams, rosters, games, releases, changelogs, and the ad-mediation stack are all linked, validated, and access-controlled.
What the Application Does
In one sentence: it is a team-scoped catalog of a studio's games and their entire release history, including developer attribution, changelogs, and the ad-mediation configuration of every version.
- Manages teams, their sub-teams, and a developer roster per team (roster records only; developers do not log in).
- Catalogs every game, its supported platform (Android / iOS / both), and the team that owns it.
- Records every game version (release): platform, version number, Unity version, purpose, update-objective outcome, release date, and the developers attributed to it.
- Captures a per-release changelog: features added, bugs fixed, improvements, and notes.
- Tracks the ad-mediation stack per release: which mediation platforms are used, the SDK version of each, a High/Medium/Low priority, and the ad networks under each platform.
- Enforces role-based access: Admins see everything; Team Planners see and edit only their own team's data.
- Provides a dashboard, full-text search, date-range filters, and pagination across every list.
Tech Stack
| Layer | Technology |
|---|---|
| Language | Python 3 |
| Framework | Django 4.2 (LTS) |
| Database | PostgreSQL (psycopg2-binary driver) |
| Auth | Django authentication with a custom User model and a role field |
| Frontend | Server-rendered Django templates, hand-written CSS (a custom monochrome / black-and-white theme, DM Sans typeface), vanilla JavaScript, with no frontend framework |
| Config | python-dotenv for environment-variable-driven settings (.env) |
| Class-based views | Django generic CBVs (ListView, DetailView, CreateView, UpdateView, DeleteView) |
Why these choices: Django's ORM, migrations, generic class-based views, and form layer cover almost all of this app's CRUD with very little boilerplate, and its relational guarantees matter for a domain that is all about correctly linked records. PostgreSQL provides the unique constraints and referential integrity the model leans on heavily. Server-rendered templates keep an internal tool simple to run and fast to use, and vanilla JavaScript (rather than a SPA framework) keeps the one genuinely interactive screen (the mediation-stack form) dependency-free.
Architecture Overview
A clean, conventional Django project split into five focused apps, each owning one slice of the domain:
Management-Portal-MB/
├── config/ # Project configuration
│ ├── settings.py # PostgreSQL, env-driven config, custom user model
│ └── urls.py # Root routing: includes every app
│
├── accounts/ # Authentication, roles & the permission engine
│ ├── models.py # Custom User: Admin / Team Planner roles
│ ├── permissions.py # Queryset-scoping filters: the heart of access control
│ ├── mixins.py # Reusable view mixins + scoped-queryset helpers
│ └── views.py # Dashboard, login/logout, Game Designs page
│
├── teams/ # Team, SubTeam, and Developer (roster) CRUD
├── games/ # Game catalog CRUD + search
├── versions/ # GameVersion (releases) + VersionDetails (changelog)
├── mediation/ # MediationPlatform + AdNetwork registry
│
├── templates/ # 31 server-rendered templates (+ shared base & partials)
└── static/css/app.css # ~1,200-line hand-written monochrome theme
Key design decisions
- One central permission engine. All access scoping lives in
accounts/permissions.pyas a set offilter_*functions (filter_teams,filter_games,filter_game_versions, …). Every view scopes its data through them: the rule "a Team Planner sees only their team" is written once and reused everywhere, instead of being re-implemented per view. - Scoped querysets via a shared mixin.
TeamObjectMixinexposesscoped_teams(),scoped_games(),scoped_game_versions(), etc., each one already wired with the rightselect_related/prefetch_relatedto avoid N+1 queries and already filtered for the current user. - Developers are roster records, not users. The
Developermodel deliberately has no login; it exists only to attribute work. Only Admins and Team Planners authenticate. This keeps the auth surface tiny while still letting releases credit specific people. - Variable-shape data goes in JSON. A release's per-mediation-platform SDK versions and priorities are stored as
JSONFieldmaps keyed by platform ID, the natural model for data whose shape depends on how many platforms a release uses.
Roles & Access Model
The system has exactly two roles, defined on the custom User model:
| Role | Scope | Can do |
|---|---|---|
| Admin | The whole studio | Full CRUD on everything, including creating teams and registering mediation platforms / ad networks. |
| Team Planner | Only their assigned team(s) | Manage the games, releases, changelogs, sub-teams, and developers of their own team(s), but cannot create top-level teams or register new mediation platforms / ad networks. |
A subtle but important detail: a Team Planner can be assigned to one or several teams. The model carries both a legacy single-team foreign key (assigned_team) and a newer many-to-many field (assigned_teams); a planner_team_ids property bridges the two so the rest of the codebase never has to care which one is populated.
Access is enforced at three levels:
- View admission:
PlannerOrAdminMixinandAdminRequiredMixindecide who may open a page at all (e.g. only Admins reach the team-create view). - Queryset scoping: every list, detail, edit, and delete view filters its data through the
filter_*engine, so a Team Planner literally cannot fetch (or even URL-guess their way into) another team's record (it returns a 404, not a 403 leak). - Form narrowing: every form's dropdowns are filtered to the user's scope, and on create forms the team field is pre-filled and disabled for single-team planners, with the value re-enforced server-side on save so a tampered request still cannot reassign ownership.
Core Features
1. Dashboard
A scoped landing page showing at-a-glance counts (teams, sub-teams, developers, games, versions), the most recent games and versions, and, for a single-team planner, a card for their own team. Every number respects the viewer's role.
2. Teams, Sub-Teams & Developer Roster
- Full CRUD for Teams (Admin-only) and Sub-Teams, with a uniqueness constraint guaranteeing sub-team names are unique within a team.
- A Developer roster per team, optionally placed into a sub-team, with validation ensuring a developer's sub-team actually belongs to their team.
- Search and team-filtering on every list.
3. Game Catalog
CRUD for games, each with a title, description, supported platform (Android / iOS / both), and an owning team. List view supports free-text search and a team filter; the detail view shows the game's full version history.
4. Game Versions (Releases)
The richest entity in the system. Each version captures:
- Platform, version number, Unity version, and a short purpose of update.
- An update objective outcome (Achieved, Partially Achieved, Not Achieved, Pending Validation, or Not Applicable) rendered as a color-coded status pill.
- A release date and the developers attributed to the work.
- The full ad-mediation stack (see below).
- A uniqueness constraint on (game, platform, version number) so the same version cannot be recorded twice.
5. The Ad-Mediation Stack
A release's monetization configuration, modeled across mediation and versions:
- A registry of mediation platforms, each containing several ad networks.
- Each game version selects which mediation platforms it uses, records the SDK version of each, and assigns each a priority, one of High (red) / Medium (blue) / Low (green), surfaced as colored chips in the UI.
- Ad networks are cascaded: selecting mediation platforms filters the ad-network choices to only those that belong to the chosen platforms.
- Networks are displayed priority-sorted, so the highest-priority mediation stack always reads first.
6. Changelogs
Each release can have one or more changelog entries (VersionDetails): structured Features added, Bugs fixed, Improvements, and Notes fields, plus its own developer attribution. A changelog inherits its platform from its parent version and can only attribute developers who were already credited on that version.
7. Search, Filtering & Pagination
- Full-text search across the relevant fields of every entity: version search alone spans version number, Unity version, purpose, objective, game title, developer names, mediation platforms, and ad networks.
- Date-range filters (start/end date) on versions and changelogs, with validation that the end date isn't before the start.
- Consistent 20-per-page pagination via a shared partial that preserves active filters across pages.
8. Django Admin
Every model is registered with a customized admin: tailored list_display, list_filter, search_fields, and autocomplete_fields, plus a custom UserAdmin exposing the role and team-assignment fields, giving Admins a power-user back office alongside the main UI.
Data Model
Nine domain models across five apps, connected by foreign keys, many-to-many relations, and JSON maps:
| Model | App | Purpose |
|---|---|---|
User | accounts | Custom user extending Django's AbstractUser; adds a role (Admin / Team Planner) and team assignments (assigned_team FK + assigned_teams M2M) |
Team | teams | A product team; the top-level unit of ownership and access scoping |
SubTeam | teams | A subdivision of a team; unique name per team |
Developer | teams | A roster member (name + optional contact) with no login; exists only to attribute work |
Game | games | A game owned by a team; supported platform + creator |
GameVersion | versions | A release of a game: platform, version no., Unity version, purpose, objective, release date, M2M developers, M2M mediation platforms & ad networks, JSON version/priority maps |
VersionDetails | versions | A changelog entry for a version: features / bugs / improvements / notes |
MediationPlatform | mediation | An ad-mediation platform |
AdNetwork | mediation | An ad network belonging to a mediation platform; unique name per platform |
Relational integrity is enforced both by the database (UniqueConstraints on sub-team names, version tuples, and ad-network names) and by model clean() methods that catch cross-entity rules a foreign key alone can't express. The schema was evolved across 20 migrations, reflecting real, iterative development against changing requirements.
Notable Engineering Challenges & Solutions
1. Per-team data isolation without scattering filter logic
Challenge: every list, detail, edit, and delete view, across five apps, must restrict a Team Planner to their own team's data, and getting it wrong anywhere leaks data.
Solution: a single permission engine (accounts/permissions.py) of composable filter_* functions, surfaced through a TeamObjectMixin that gives every view ready-made scoped_*() querysets. The scoping rule exists in exactly one place; views just call it.
2. Planners who belong to more than one team
Challenge: the system started with one planner ↔ one team, then needed to support planners spanning several teams, without rewriting every query.
Solution: kept the original assigned_team FK, added an assigned_teams M2M, and introduced a planner_team_ids property that returns the right set of IDs from whichever field is populated. All downstream code reads that one property, so the multi-team change was additive, not invasive.
3. Variable-shape mediation data
Challenge: each release uses a different set of mediation platforms, and each needs its own SDK version and priority, a shape that doesn't fit fixed columns.
Solution: two JSONField maps keyed by platform ID (mediation_platform_versions, mediation_platform_priorities), populated by a custom form that validates a version string and a valid priority exists for every selected platform, and normalizes the payload before save. Model @property helpers turn the raw maps back into ordered, human-readable displays.
4. Cascading, dependent dropdowns
Challenge: the ad-network choices on the version form depend on which mediation platforms are selected, and per-platform version/priority inputs must appear and disappear live.
Solution: a self-contained vanilla-JS controller drives custom multi-select dropdowns: it filters ad networks to the chosen platforms, dynamically renders a version field and priority selector per selected platform, keeps a hidden JSON payload in sync, and re-hydrates all selections when an existing release is edited.
5. Referential integrity beyond foreign keys
Challenge: several rules cannot be expressed as a foreign key: a release's developers must belong to the game's team; ad networks must belong to the selected mediation platforms; a version's platform must match the game's; a changelog's platform must match the version's.
Solution: layered validation, combining database UniqueConstraints, model clean() methods, and form-level clean() cross-field checks, so an invalid combination is rejected whether it arrives through the UI, the admin, or a crafted request.
6. Tamper-proof team assignment on create forms
Challenge: a single-team planner shouldn't choose a team when creating a game or developer, but a disabled HTML field submits nothing and could be spoofed.
Solution: the form pre-fills and disables the team field for UX, and the view/form re-derives and re-applies the planner's team ID server-side on save, so the client value is never trusted.
Security & Access Control
- Custom user model with a typed role field (
Admin/Team Planner) and convenience properties (is_admin_role,is_team_planner). - Two-layer view protection:
AdminRequiredMixinfor studio-wide actions,PlannerOrAdminMixin(which also requires a planner to actually have a team) for everything else. - Queryset scoping on every operation: unauthorized objects return a clean 404, never an information-leaking error.
- Scope-narrowed forms: every dropdown is filtered to the user's teams; ownership fields are server-enforced, not client-trusted.
- Standard Django hardening: CSRF protection on all forms, password-validation rules,
SecurityMiddleware, clickjacking protection. - Environment-driven configuration: secret key and full PostgreSQL connection are read from a
.envfile viapython-dotenv, with a committed.env.exampledocumenting every variable and no secrets in source control.
Project Stats
- ~2,250 lines of application Python (views, models, forms, permissions, mixins, excluding migrations).
- 5 Django apps, 9 domain models, 20 migrations.
- ~30 views spanning dashboard, auth, and full CRUD for seven entities.
- 31 server-rendered templates (~1,900 lines) over a shared base layout and reusable partials.
- ~1,200 lines of hand-written, dependency-free CSS: a custom monochrome design system.
- A central permission engine of 8 reusable queryset-scoping functions.
Possible Future Improvements
- Add automated test coverage for the permission engine and the cross-entity validation rules.
- Build out the Game Designs section (currently a navigational placeholder) into a full design-doc module.
- Add an audit log so every create/edit/delete on a release is attributable and reversible.
- Introduce a REST/JSON API to support a future SPA or mobile companion.
- Add CSV/Excel export of release histories and changelogs for reporting.
- Add bulk import for seeding teams, developers, and historical versions.
- Replace the legacy single
assigned_teamfield entirely once all data is migrated to the M2M relation. - Set up a CI/CD pipeline and formal production deployment.
Built with Django · PostgreSQL · server-rendered templates · vanilla JavaScript: a role-scoped management portal for a multi-team game studio.