A job & internship application tracker for students and junior developers.
ApplyTrack replaces the scattered spreadsheet-plus-notes-app approach to job hunting with one organized, private place to track every application — its status, deadline, and outcome — from "Wishlist" all the way to "Accepted."
Built with Django, PostgreSQL, and Bootstrap 5, structured the way a small production SaaS app is structured: isolated apps by responsibility, a custom user model, query-level multi-tenancy, environment-based configuration, and a real test suite.
- Features
- Tech Stack
- Architecture
- Data Model
- Getting Started
- Environment Variables
- Demo Data
- Running Tests
- Project Structure
- Deployment Notes
- Roadmap
- License
Accounts
- Registration, login, logout with a custom
Usermodel (extendsAbstractUser) - Editable profile: avatar, university/bootcamp, bio, GitHub/LinkedIn/portfolio links
- In-app password change
Application tracking
- Full CRUD on applications, scoped so a user can only ever see or touch their own data
- 7-stage status pipeline:
Wishlist → Applied → Interview → Technical Interview → Offer → Rejected / Accepted - Company, position, location, type (
Internship/Junior/Remote), date applied, deadline, job posting link, salary (optional), notes - Search by company/position/location, filter by status and type, sort by date/company/deadline/status, pagination
- Business-rule validation (e.g. an "Applied" status requires a date applied; a deadline can't precede it)
Dashboard
- Total applications, interview count, offer count, acceptance rate, rejection rate, applications this month
- Status breakdown chart and a 6-month application trend chart
- Upcoming deadlines (next 14 days) and a recent-activity feed
Engineering
- Environment-driven settings (
django-environ) — no secrets in source control - Custom 404 / 500 error pages
- WhiteNoise for static files, Docker +
docker-composefor one-command local setup - GitHub Actions CI (checks, migration drift detection, test suite)
- A real test suite covering models, forms, and — most importantly — cross-user data isolation
| Layer | Choice |
|---|---|
| Backend | Django 5.2 (LTS) |
| Database | PostgreSQL 16 |
| DB adapter | psycopg 3 |
| Frontend | Django Templates, Bootstrap 5, vanilla JS |
| Forms | django-crispy-forms + crispy-bootstrap5 |
| Charts | Chart.js |
| Config | django-environ (12-factor .env) |
| Static files | WhiteNoise |
| Prod server | Gunicorn |
| Containerization | Docker / docker-compose |
The project is split into three focused Django apps instead of one monolithic app — each has a single, clear responsibility, which keeps models.py/views.py short and makes the codebase easy to navigate for anyone joining the project cold:
accounts/ → Custom User model, auth views, profile
applications/ → The core domain model (Application) + CRUD + list filtering
dashboard/ → Read-only aggregation views over applications (no models of its own)
config/ → Project-wide settings, URLs, WSGI/ASGI, error handlers
A few decisions worth calling out:
- Custom user model from commit #1. Swapping
AUTH_USER_MODELafter a project has real data is a painful migration. Since this project extendsAbstractUserfrom the start, adding fields likeuniversityoravatarlater is a normal migration, not a rewrite. - Multi-tenancy enforced at the query layer, not just the UI. Every view that touches
Applicationfilters throughApplication.objects.for_user(request.user). Trying to view, edit, or delete another user's application by guessing a URL returns a 404, not a permission error that leaks the object's existence. This is covered explicitly inapplications/tests.py. statusis a single current-state field, not a history log. That keeps the schema simple for v1 — see Roadmap for the natural extension.- Server-rendered, not an SPA. Search/filter/sort live in the URL's query string (bookmarkable, shareable, works with JS disabled). For a CRUD-heavy internal tool like this, a well-built multi-page app is a legitimate architecture choice, not a legacy one.
User (accounts)
├─ id, username, email (unique), password
├─ first_name, last_name
├─ avatar, university, bio
├─ github_url, linkedin_url, portfolio_url
└─ date_joined, created_at
Application (applications)
├─ id
├─ user_id ───────────────────► User (FK, CASCADE)
├─ company_name, position, location
├─ job_type [internship | junior | remote]
├─ status [wishlist | applied | interview | technical_interview
│ | offer | rejected | accepted]
├─ date_applied, deadline
├─ job_link, salary, notes
└─ created_at, updated_at
Indexes: (user_id, status), (user_id, -created_at)
dashboard intentionally owns no models — it's a read-only aggregation layer over Application, which keeps stat calculations in one place instead of scattering Application.objects.filter(...) logic across templates.
This starts PostgreSQL and the app together, runs migrations automatically, and needs nothing installed locally except Docker.
git clone https://github.com/<your-username>/ApplyTrack.git
cd ApplyTrack
cp .env.example .env
docker compose up --buildVisit http://localhost:8000. To load realistic sample data:
docker compose exec web python manage.py seed_demo_data
docker compose exec web python manage.py createsuperuserPrerequisites: Python 3.12+, PostgreSQL 16 running locally.
git clone https://github.com/<your-username>/ApplyTrack.git
cd ApplyTrack
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install -r requirements.txt
# Create the database (adjust user/password to taste)
createdb applytrack
cp .env.example .env # then edit .env — see below
python manage.py migrate
python manage.py createsuperuser
python manage.py runserverVisit http://localhost:8000.
All configuration lives in .env (copy .env.example to start). Nothing environment-specific is hardcoded in settings.py.
| Variable | Description | Example |
|---|---|---|
SECRET_KEY |
Django's cryptographic signing key. Generate a real one for anything beyond local dev. | django-insecure-... |
DEBUG |
True locally, always False in production. |
True |
ALLOWED_HOSTS |
Comma-separated hostnames Django will serve. | localhost,127.0.0.1 |
DATABASE_URL |
Full PostgreSQL connection string. | postgres://applytrack:applytrack@localhost:5432/applytrack |
CSRF_TRUSTED_ORIGINS |
Comma-separated origins allowed to POST cross-site (needed behind HTTPS in production). | https://applytrack.example.com |
Generate a real SECRET_KEY with:
python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"To see the dashboard and list populated with realistic data instead of an empty state:
python manage.py seed_demo_dataThis creates (or reuses) a demo / demopass123 account with ~14 sample applications spread across every status, company, and deadline scenario — useful for screenshots, demos, or just poking around the UI immediately after cloning.
python manage.py testThe suite focuses on the things that actually matter in a multi-tenant app: model behavior, form validation rules, and — most heavily — that no user can ever view, edit, or delete another user's applications. CI runs this on every push via .github/workflows/ci.yml, alongside manage.py check and a migration-drift check (makemigrations --check).
ApplyTrack/
├── accounts/ # Custom User model, auth & profile views
│ ├── migrations/
│ ├── admin.py
│ ├── forms.py
│ ├── models.py
│ ├── urls.py
│ └── views.py
├── applications/ # Core domain: Application model, CRUD, filtering
│ ├── management/commands/ # seed_demo_data
│ ├── migrations/
│ ├── templatetags/
│ ├── admin.py
│ ├── forms.py
│ ├── models.py
│ ├── urls.py
│ └── views.py
├── dashboard/ # Stats aggregation (no models of its own)
│ ├── urls.py
│ ├── utils.py
│ └── views.py
├── config/ # Project settings, URLs, WSGI/ASGI, error handlers
│ ├── settings.py
│ ├── urls.py
│ └── views.py
├── templates/ # base.html + app_base.html shells, per-app templates
│ ├── partials/
│ ├── accounts/
│ ├── applications/
│ ├── dashboard/
│ └── errors/ # custom 404 / 500
├── static/
│ ├── css/style.css
│ ├── js/main.js
│ └── img/
├── media/ # user-uploaded avatars (git-ignored)
├── Dockerfile
├── docker-compose.yml
├── requirements.txt
├── .env.example
└── manage.py
The app is deployable as-is to any platform that runs a Docker container or a standard Python/WSGI app (Render, Railway, Fly.io, a plain VPS with Gunicorn behind Nginx, etc.):
- Set
DEBUG=Falseand a realSECRET_KEYin the environment. - Point
DATABASE_URLat a managed PostgreSQL instance. - Set
ALLOWED_HOSTS(andCSRF_TRUSTED_ORIGINSif behind a custom domain over HTTPS). - Run
python manage.py migrateandpython manage.py collectstatic --noinputas part of your deploy step (the Dockerfile already does the latter at build time). - Serve with
gunicorn config.wsgi:application. WhiteNoise handles static files directly from the app process, so no separate static file server is required for a small-to-medium deployment.
When DEBUG=False, SECURE_SSL_REDIRECT, SESSION_COOKIE_SECURE, and CSRF_COOKIE_SECURE are all on by default (see config/settings.py) — disable them only if you have a specific reason (e.g. TLS terminated somewhere that doesn't set X-Forwarded-Proto).
Deliberately left out of v1 to keep the core model easy to reason about, but natural next steps:
ApplicationStatusChangehistory model — record every status transition with a timestamp, enabling real funnel analytics (e.g. "average days from Applied to Offer") instead of only current-state snapshots.- Email reminders for upcoming deadlines.
- REST API (Django REST Framework) for a future mobile client or browser extension that captures job postings directly.
- Tags/labels for finer-grained organization than the three built-in job types.
- OAuth login (GitHub/Google) alongside username/password.
MIT — free to use, fork, and adapt.