A teaching-focused Flask + MySQL web app for tracking job applications, follow-ups, resume versions, and interviews. It uses direct SQL (no ORM, no SQLAlchemy), environment-based configuration, session login, and Docker Compose for local deployment.
You can register, log in, and manage your own job pipeline: companies, statuses, interviews, and notes. Every query is scoped to your user id so you only ever see your data.
Flask is a lightweight Python web framework. It maps URLs to Python functions (“views”), renders Jinja HTML templates, reads form data, sets flash messages, and signs session cookies so the server remembers who is logged in between requests.
MySQL is a relational database server. Data lives in tables with rows and columns. This project uses foreign keys so that follow-ups, resumes, and interviews stay tied to one application (and applications to one user).
Instead of an ORM (Object-Relational Mapper) building SQL for you, this project:
- Opens a connection with
mysql-connector-python. - Creates a cursor to run SQL strings.
- Uses
%splaceholders and a tuple of values so user input is never concatenated into raw SQL. - Commits after writes (
INSERT/UPDATE/DELETE). - Closes the cursor and connection to free server resources.
See db.py for reusable helpers (fetch_one, fetch_all, execute_write) and comments on why each step exists.
Secrets and per-machine settings (database password, SECRET_KEY) should not be hardcoded in Python files:
- Source code often ends up in git; passwords in code are a common leak.
.envholds local values;.gitignoreexcludes.env** so it is not committed.python-dotenvloads those variables intoos.environwhen the app starts..env.exampledocuments which variables are required without containing real secrets.
Docker Compose can inject the same variable names with different values (for example DB_HOST=mysql_db inside the network).
- Register: email + password → password is hashed with
werkzeug.security.generate_password_hash→ only the hash is stored in MySQL. - Login: load user by email →
check_password_hashcompares your typed password to the stored hash → on success,session['user_id'](and email) is set. - Protected routes: the
@login_requireddecorator inauth.pychecks foruser_idin the session; if missing, you are redirected to login. - Logout:
session.clear()removes the server-side session data associated with your cookie.
Flask signs the session cookie with SECRET_KEY. If someone changes the cookie bytes, the signature fails — that is why the secret must stay private and random in production.
angelica_job_tracker/
├── app.py # Flask app factory, blueprint registration, entrypoint
├── db.py # MySQL connection + safe query helpers
├── auth.py # Register, login, logout, login_required
├── dashboard.py # Dashboard stats (SQL aggregates)
├── applications.py # Applications CRUD + follow-ups, resumes, interviews
├── init_db.sql # Schema (run once / used by Docker on first MySQL start)
├── requirements.txt
├── Dockerfile
├── docker-compose.yml
├── .env.example
├── .gitignore
├── README.md
├── templates/ # Jinja HTML (Bootstrap 5)
└── static/ # css/, js/
From the angelica_job_tracker folder:
python -m venv .venvWindows (PowerShell):
.\.venv\Scripts\Activate.ps1
pip install -r requirements.txtmacOS / Linux:
source .venv/bin/activate
pip install -r requirements.txt- Start MySQL on your machine (localhost).
- Create the database and tables using the provided script.
Option A — MySQL client from a terminal:
mysql -u root -p < init_db.sqlOption B — MySQL Workbench: open init_db.sql, run it against your server.
The script contains CREATE DATABASE IF NOT EXISTS angelica; and USE angelica;, then creates users, applications, follow_ups, resume_versions, and interviews.
copy .env.example .env(On macOS/Linux: cp .env.example .env.)
Edit .env:
DB_HOST=localhostwhen Flask runs on your computer and MySQL is on the same machine.DB_PORT,DB_USER,DB_PASSWORD,DB_NAMEmust match your MySQL login and database name.SECRET_KEY: set a long random string (used to sign sessions).FLASK_ENV=development: enables Flask debug features while learning; useproductionfor real deployment.
Never commit .env.
With .venv activated and MySQL running:
python app.pyOpen http://127.0.0.1:5000 — you should be redirected to login, then the dashboard after signing in.
- On your laptop,
localhostmeans your laptop. - Inside the Flask container,
localhostmeans that container, not the MySQL container.
So in Docker Compose, DB_HOST must be the MySQL service name from docker-compose.yml — here it is mysql_db.
The Compose file sets that for the web service. You can still override values via a .env file next to docker-compose.yml (for example DB_PASSWORD, SECRET_KEY).
From angelica_job_tracker:
docker compose up --build- Flask: http://localhost:5000
- MySQL: port
3306is published to the host (optional use for GUI tools).
mysql_datavolume: persists database files so data survives container restarts.5000:5000: maps host port 5000 to the app container’s port 5000.3306:3306: exposes MySQL to the host (handy for debugging; tighten in production).
On first MySQL startup, the image runs SQL files in /docker-entrypoint-initdb.d. This project mounts init_db.sql there so tables are created automatically.
You might keep two mental “profiles”:
| Setting | Flask on host + MySQL on host | Docker Compose |
|---|---|---|
DB_HOST |
localhost |
mysql_db (service name) |
DB_PASSWORD |
your local root password | must match MYSQL_ROOT_PASSWORD in Compose |
Compose uses ${DB_PASSWORD:-rootroot} so, if you set DB_PASSWORD in .env, both MySQL and the web container share it.
| Symptom | Likely cause | What to do |
|---|---|---|
RuntimeError: SECRET_KEY is missing |
No .env or empty key |
Copy .env.example to .env and set SECRET_KEY. |
Can't connect to MySQL server |
Wrong DB_HOST / port / firewall |
Local: DB_HOST=localhost. Docker: DB_HOST=mysql_db. |
Access denied for user |
Wrong DB_USER / DB_PASSWORD |
Match MySQL credentials; restart app after editing .env. |
Unknown database 'angelica' |
Schema not imported | Run init_db.sql (see section 9). |
| Tables missing inside Docker | Volume already initialized without script | Remove the named volume only in dev (docker compose down -v) and up again — deletes DB data. |
| Registration says email exists | Normal | Use another email or delete the row in users (dev only). |
- CSRF tokens for all POST forms (e.g. Flask-WTF).
- Production WSGI server (Gunicorn/Waitress) behind Nginx or a cloud load balancer.
- Migrations for schema changes (still without ORM — e.g. raw SQL migration files).
- File uploads for real resume PDFs (stored outside the web root).
- Email reminders for follow-ups and interviews.
- Tests (pytest) hitting a disposable MySQL instance in CI.
- File structure:
app.pywires the app;db.pyis the only database layer; feature blueprints split auth, dashboard, and applications; templates live undertemplates/. - Login: hashed passwords in MySQL; successful login sets
session['user_id'];@login_requiredguards private routes;SECRET_KEYsigns the session cookie. - Queries: SQL with
%sparameters → mitigates SQL injection;commitpersists writes; closing connections avoids resource leaks. - Run locally: venv →
pip install -r requirements.txt→ importinit_db.sql→.envwithDB_HOST=localhost→python app.py. - Run with Docker:
docker compose up --build; use service namemysql_dbasDB_HOSTfor the web container (already set indocker-compose.yml).
Built with care for Angelica — read the comments in db.py, auth.py, and applications.py while you step through requests in the debugger.