A personal, self-hosted web app to track jobs, internships, hackathons, competitions, and grants — with deadline reminders, search, and statistics. Built with a deliberately retro terminal aesthetic.
- Overview
- Tech Stack
- Architecture
- Features
- Data Model
- Project Structure
- Setup & Installation
- Environment Variables
- Running the App
- Daily Reminder System
- GitHub Actions Automation
- API Routes Reference
- Security Design
- Future Improvements
- Built With
- Acknowledgements
- License
- Contact
Application Tracker is a single-user Flask web application backed by a PostgreSQL database (hosted on Neon). It replaces a spreadsheet with a structured, searchable, and deadline-aware interface for managing every opportunity you're pursuing.
The UI intentionally mimics a retro terminal / BBS aesthetic — monospace fonts, bordered tables, marquee headers — which also keeps it extremely fast and dependency-free on the frontend (zero JavaScript frameworks, zero CSS frameworks).
| Layer | Technology |
|---|---|
| Web Framework | Flask 3.x (Python) |
| Database | PostgreSQL via Neon (serverless Postgres) |
| DB Driver | psycopg2-binary |
| WSGI Server | Gunicorn (for production) |
| Config | python-dotenv + .env file |
| Reminders | smtplib (SMTP over TLS) + rich for terminal output |
| CI / Automation | GitHub Actions (scheduled cron job) |
| Frontend | Vanilla HTML + CSS (Jinja2 templates, zero JS deps) |
┌────────────────────────────────────────────────────┐
│ Browser / Client │
└────────────────────┬───────────────────────────────┘
│ HTTP
┌────────────────────▼───────────────────────────────┐
│ Flask App (app.py) │
│ ┌──────────────┐ ┌───────────────────────────┐ │
│ │ Auth Layer │ │ Route Handlers │ │
│ │ (session) │ │ /, /add, /edit, /delete │ │
│ │ │ │ /search, /stats, │ │
│ └──────────────┘ │ /deadlines, /login │ │
│ └──────────┬──────────────────┘ │
└────────────────────────────────┼───────────────────┘
│
┌────────────────────────────────▼───────────────────┐
│ Data Layer (db.py) │
│ psycopg2 · RealDictCursor · parameterized SQL │
└────────────────────────────────┬───────────────────┘
│ SSL / TLS
┌────────────────────────────────▼───────────────────┐
│ Neon Serverless PostgreSQL │
│ (connection pooling enabled) │
└────────────────────────────────────────────────────┘
┌─────────────────────────┐
│ GitHub Actions │
│ (cron: 3:30 UTC daily)│
│ └── remind.py │
│ └── SMTP email │
└─────────────────────────┘
The app follows a clean three-layer separation:
app.py— Flask routes, session management, form parsing. No SQL here.db.py— All database I/O. Usespsycopg2withRealDictCursorso rows come back as dicts. Every function opens and closes its own connection (simple, safe for low-concurrency personal use).models.py— A singleApplicationdataclass with a computed property (days_until_deadline). No ORM overhead.
- Add a new application with title, organisation, type, platform, URL, deadline, status, contact used, and notes
- Edit any field on an existing record at any time
- Delete a record (POST-only — never exposed as a GET to prevent CSRF via URL)
- Search across title, org, and platform with Postgres
ILIKE(case-insensitive)
- Deadlines are hidden for
jobandinternshiptypes (they typically don't have hard deadlines) - The deadline field is dynamically shown/hidden in the form based on the selected type (vanilla JS)
- The index table colour-codes deadlines:
- 🔴 Past — shown in red
- 🟠 ≤ 3 days — urgent styling
- 🟡 ≤ 7 days — soon styling
- Remaining days shown inline as
(Xd)
/deadlinesroute shows only applications with upcoming deadlines, sorted ascending
Status transitions: pending → applied → interview → offer / rejected
When an application's status is changed to applied for the first time, date_applied is automatically set to today. Subsequent edits preserve the original date_applied.
The /stats page aggregates:
- Total records in the database
- Active count (status is
pendingorinterview) - Breakdown by application type
- Breakdown by status
remind.py queries the database directly and sends a categorised email:
- 🔴 URGENT — deadlines ≤ 3 days
- 🟡 COMING UP — 4–7 days
- 📋 THIS MONTH — 8–30 days
Jobs and internships are intentionally excluded from reminders (no hard deadlines). The terminal output uses rich for colour-coded formatting. The email uses plain text with the same structure.
Single-password authentication using Flask sessions. All routes except /login are protected by a @login_required decorator.
@dataclass
class Application:
id: Optional[int] # SERIAL PRIMARY KEY
title: str # Role / opportunity name
org: str # Company / organisation
type: str # job | internship | hackathon | competition | grant
platform: str # Where you found / applied (LinkedIn, Devpost, etc.)
url: str # Direct link to the listing
deadline: Optional[date] # Submission deadline (nullable)
status: str # pending | applied | interview | offer | rejected
date_applied: Optional[date] # Auto-set when status → applied
notes: str # Free-form notes
contact_used: str # Email / phone used to apply
@property
def days_until_deadline(self) -> Optional[int]: ...PostgreSQL schema (auto-created by db.init_db() on first run):
CREATE TABLE IF NOT EXISTS applications (
id SERIAL PRIMARY KEY,
title TEXT NOT NULL,
org TEXT NOT NULL,
type TEXT NOT NULL,
platform TEXT NOT NULL,
url TEXT NOT NULL,
deadline DATE,
status TEXT NOT NULL,
date_applied DATE,
notes TEXT NOT NULL,
contact_used TEXT NOT NULL DEFAULT ''
);The contact_used column is added via ALTER TABLE ... ADD COLUMN IF NOT EXISTS — safe to run on an existing database without data loss.
Tracker/
├── app.py # Flask application, routes, auth
├── db.py # Database layer (psycopg2)
├── models.py # Application dataclass
├── remind.py # Standalone reminder script
├── requirements.txt # Python dependencies
├── .env # Secrets (never commit this)
├── .gitignore
│
├── templates/
│ ├── base.html # Shared layout (nav, flash messages, footer)
│ ├── index.html # Main table view (all apps / deadlines / search results)
│ ├── add.html # Add new application form
│ ├── edit.html # Edit existing application form
│ ├── stats.html # Statistics dashboard
│ └── login.html # Password login page
│
├── static/
│ ├── style.css # Retro terminal stylesheet
│ ├── favicon.ico
│ └── ... # PWA icons + webmanifest
│
└── .github/
└── workflows/
└── daily_reminder.yml # GitHub Actions cron job
- Python 3.10+
- A PostgreSQL database (a free Neon project works perfectly)
- A Gmail account with an App Password (for reminders)
git clone https://github.com/indiser/AppTrack.git
cd AppTrack
python -m venv venv
# Windows
venv\Scripts\activate
# macOS / Linux
source venv/bin/activatepip install -r requirements.txtCopy the example below into a .env file at the project root and fill in your values:
DATABASE_URL="postgresql://user:password@host/dbname?sslmode=require"
SECRET_KEY="a-long-random-hex-string"
APP_PASSWORD="your-login-password"
# Optional — only needed for remind.py
REMINDER_EMAIL_FROM="you@gmail.com"
REMINDER_EMAIL_PASSWORD="your-app-password"
REMINDER_EMAIL_TO="destination@email.com"# Development
python app.py
# Production (Gunicorn)
gunicorn app:appThe database table is created automatically on startup via db.init_db(). No migration tool required.
| Variable | Required | Description |
|---|---|---|
DATABASE_URL |
✅ Yes | Full PostgreSQL connection string |
SECRET_KEY |
✅ Yes | Flask session signing key (use a long random string) |
APP_PASSWORD |
✅ Yes | Single password to log in to the web UI |
REMINDER_EMAIL_FROM |
⚪ Optional | Gmail address to send reminders from |
REMINDER_EMAIL_PASSWORD |
⚪ Optional | Gmail App Password (not your account password) |
REMINDER_EMAIL_TO |
⚪ Optional | Recipient email address for reminders |
⚠️ Never commit.envto version control. It is listed in.gitignore.
python app.py
# Flask dev server runs on http://127.0.0.1:5000gunicorn app:app --workers 2 --bind 0.0.0.0:8000For deployment on platforms like Railway, Render, or Fly.io, set all environment variables as secrets in the platform dashboard and use the Gunicorn command as the start command.
remind.py is a standalone script — it does not import from app.py or use Flask. It connects to the database directly, queries upcoming deadlines, and sends an email summary.
Run it manually:
python remind.pySchedule it locally:
# Linux / macOS (crontab -e)
30 3 * * * cd /path/to/Tracker && python remind.py
# Windows — use Task Scheduler to run `python remind.py` daily at 9:00 AMOr let GitHub Actions run it automatically (see below).
The workflow at .github/workflows/daily_reminder.yml runs remind.py every day at 3:30 AM UTC (9:00 AM IST) using a scheduled cron trigger. It can also be triggered manually from the GitHub Actions UI.
Required GitHub Secrets (set in your repo → Settings → Secrets → Actions):
| Secret | Value |
|---|---|
DATABASE_URL |
Your Neon PostgreSQL connection string |
REMINDER_EMAIL_FROM |
Gmail address |
REMINDER_EMAIL_PASSWORD |
Gmail App Password |
REMINDER_EMAIL_TO |
Recipient address |
The workflow checks out the repo, sets up Python 3.11, installs psycopg2-binary and rich, and runs the script.
All routes require authentication (redirect to /login if not logged in).
| Method | Route | Description |
|---|---|---|
GET |
/ |
All applications, sorted by deadline (NULLs last) |
GET |
/deadlines |
Upcoming deadlines only (today or future), ascending |
GET/POST |
/add |
Add a new application |
GET/POST |
/edit/<id> |
Edit an existing application |
POST |
/delete/<id> |
Delete an application (POST-only, confirms in UI) |
GET |
/search?q=<query> |
Search title, org, platform via ILIKE |
GET |
/stats |
Aggregated statistics |
GET/POST |
/login |
Password login |
GET |
/logout |
Clear session and redirect to login |
- Single-password auth — session cookie is set
HttpOnlyandSecure. The password is stored only in the environment, never in code or the database. - Parameterized queries throughout —
db.pyuses%splaceholders in everypsycopg2call. No string interpolation in SQL. Zero SQL injection surface. - POST-only deletes — the
/delete/<id>route only acceptsPOST. Attempting aGETto that URL does nothing, preventing accidental or malicious deletion via a crafted link. - CSRF on destructive actions — the delete form includes a JS
confirm()dialog as a UI-level guard. - No secrets in source —
DATABASE_URL,SECRET_KEY,APP_PASSWORD, and email credentials are all loaded from.env/ environment at runtime and are excluded from version control.
- Tags / labels — free-form tagging system to group applications by cohort, season, or custom category
- Bulk actions — select multiple records and update status or delete in one operation
- Notes history — append-only log per application instead of a single editable notes field
- CSV export — download all applications as a spreadsheet for external analysis
- Reminder configuration via UI — let the user set reminder thresholds (e.g., 1 day, 3 days, 7 days) from a settings page instead of hardcoding them in
remind.py - Status timeline — record timestamps for every status change to build an activity history
- Duplicate detection — warn when adding an application with the same title + org as an existing record
- Calendar view — a monthly calendar that visualises deadline distribution at a glance
- Multi-user support — user accounts with isolated data per user; requires reworking the auth layer and adding a
user_idforeign key to the applications table - REST API — a JSON API layer so the data can be consumed by a mobile client or integrated with other tools
- Browser extension — one-click "Add to Tracker" from a job board or hackathon listing page
- Analytics — response rate, average time-to-decision, success rate by platform, type, or source — surfaced on the stats page
- Notification channels — Telegram bot, Slack webhook, or Discord message as alternatives to email reminders
This project is intentionally minimal — no magic frameworks, no frontend build pipeline. Every dependency earns its place.
- Neon — for making serverless Postgres genuinely usable on a free tier. The auto-suspend feature means this app costs nothing to host when idle.
- Flask — for staying out of the way. The micro-framework philosophy is the right call for a project like this.
- Textualize / Rich — the
richlibrary makesremind.py's terminal output genuinely enjoyable to read, and the hyperlink support in modern terminals is a nice touch. - psycopg2 — the oldest and most reliable PostgreSQL adapter in the Python ecosystem. Still the right choice.
- The retro BBS / terminal aesthetic was inspired by the era of Netscape Navigator and ASCII-art bulletin boards — a deliberate design choice to keep the UI fast, accessible, and free of frontend complexity.
This project is licensed under the MIT License.
MIT License
Copyright (c) 2024 Rana Banerjee
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Built and maintained by Indiser.
- 📧 Email: indiser01@gmail.com
- 🐙 GitHub: @indiser
Feel free to open an issue or fork the repo if you want to adapt it for your own use. Pull requests are welcome.