This guide records the behavior of the current implementation. It is intended to shorten onboarding time and make the impact of changes easier to assess.
The application is a three-role placement workflow:
- Administrators approve companies and drives, manage account access, inspect applications, and download generated reports.
- Companies publish drives, control whether they are open, review applicants, advance applications, upload offer letters, and export application data.
- Students maintain academic profiles and resumes, browse eligible drives, apply, track decisions, download offer letters, and export their history.
The repository is a single deployment unit with a Vue single-page application, a Flask JSON API, one SQLite database, Redis, Celery, SMTP, and local file storage.
Browser
|
+-- Vue/Vite :5173
|
+-- /api proxy --> Flask :5001
|
+-- SQLite
+-- Redis DB 1 (HTTP response cache)
+-- Redis DB 0 (Celery broker and results)
+-- local uploads, exports, and reports
+-- SMTP through Flask-Mail
Celery worker <---- Redis DB 0 ----> Celery Beat
| Path | Responsibility |
|---|---|
backend/app.py |
Application factory, configuration, JWT callbacks, cache invalidation, static SPA fallback, and all HTTP endpoints |
backend/eligibility.py |
Shared CGPA, branch-alias, and study-year eligibility rules |
backend/models.py |
SQLAlchemy schema, relationships, constraints, table creation, and development seed users |
backend/tasks.py |
Student/company CSV exports, deadline reminders, and monthly report generation |
backend/celery_app.py |
Celery construction, Flask application context integration, result retention, timezone, and Beat schedule |
backend/extensions.py |
Shared Flask-Caching and Flask-Mail instances |
backend/email_helper.py |
HTML email delivery and ADMIN_EMAIL fallback |
backend/config.py |
Local defaults loaded before environment-specific overrides |
backend/wsgi.py |
Gunicorn entry point for container and production runs |
backend/seed_demo.py |
Optional demo dataset for local demos and screenshots; refuses to run twice |
frontend/src/App.vue |
Application shell, navigation, and logout |
frontend/src/router/index.js |
Public and role-specific routes plus browser-side route guards |
frontend/src/utils/api.js |
Axios base URL, bearer-token attachment, and 401 handling |
frontend/src/utils/auth.js |
Login, registration, and local-storage session state |
frontend/src/views/ |
Admin, company, student, login, registration, and home pages |
frontend/src/components/ |
Shared interface pieces: data table, segmented tabs, status badges, stat cards, empty states, toasts, and the confirmation dialog |
frontend/src/components/AppBackground.vue |
The single full-window backdrop instance mounted by the shell |
frontend/src/components/BeamsBackground.vue |
Lifecycle wrapper for the effect: lazy load, motion preference, visibility pausing, disposal |
frontend/src/components/CountUp.vue |
Numbers that roll up once scrolled into view |
frontend/src/components/SpotlightCard.vue |
Cursor-tracking highlight applied through custom properties |
frontend/src/components/ShinyText.vue |
Sweeping sheen used for accent copy |
frontend/tests/ |
Vitest component, service, router, and lifecycle suites |
frontend/src/components/beams.js |
Framework-free WebGL beams: noise-displaced standard material over one merged geometry |
frontend/src/assets/theme.css |
Design tokens and Bootstrap overrides for the light and dark themes, including the shared glass material |
backend/tests/support.py |
Shared test fixtures: throwaway database, file tree, and registration helpers |
backend/tests/ |
Workflow, authorization, file-access, configuration, and eligibility suites |
.github/workflows/ci.yml |
Continuous integration: tests, coverage gate, boundaries, secret scan, lint, build, container |
Dockerfile |
Three-stage build producing one image for the API, worker, and Beat |
docker-compose.yml |
Local production-shaped stack with Redis and named volumes |
scripts/api-smoke.sh |
Curl smoke test against a running backend: authentication, role boundaries, and the company approval gate |
run.sh |
Starts Flask, Vite, a Celery worker, and Celery Beat as child processes |
backend/app.py:create_app() performs more than object construction:
- Discovers and loads
.envthrough python-dotenv; with the documented launch commands this is normallybackend/.env. - Resolves Flask, JWT, database, Redis, cache, and mail configuration.
- Creates the resume, offer-letter, export, and report directories.
- Initializes SQLAlchemy.
- Calls
db.create_all(). - Seeds the admin, approved test company, and test student if absent.
- Initializes Redis-backed caching and Flask-Mail.
- Registers the routes defined inside the factory.
Importing backend/celery_app.py also creates a Flask application, then wraps
every Celery task in that application's context. Worker and Beat startup
therefore run database initialization and seed checks as import side effects.
The relative default database URL, sqlite:///ppa_db.db, is resolved by
Flask-SQLAlchemy under Flask's instance directory. With the current layout this
is normally backend/instance/ppa_db.db.
| Variable | Default | Used by |
|---|---|---|
APP_ENV |
development |
Selects fail-closed secret handling |
FLASK_SECRET_KEY |
Random per process outside production | Flask session signing |
JWT_SECRET_KEY |
Random per process outside production | JWT signing |
JWT_ACCESS_TOKEN_MINUTES |
60 |
Access-token lifetime |
CORS_ORIGINS |
Three local development origins | Allowed browser origins |
DATABASE_URL |
sqlite:///ppa_db.db |
SQLAlchemy |
CELERY_BROKER_URL |
redis://localhost:6379/0 |
Celery transport |
CELERY_RESULT_BACKEND |
redis://localhost:6379/0 |
Celery task results |
REDIS_HOST |
localhost |
Flask-Caching |
REDIS_PORT |
6379 |
Flask-Caching |
MAIL_SERVER |
smtp.gmail.com |
Flask-Mail |
MAIL_PORT |
587 |
Flask-Mail |
MAIL_USE_TLS |
true |
Flask-Mail |
MAIL_USERNAME |
Empty | SMTP authentication and mail suppression |
MAIL_PASSWORD |
Empty | SMTP authentication |
MAIL_DEFAULT_SENDER |
MAIL_USERNAME |
Outgoing sender |
ADMIN_EMAIL |
Empty | Default recipient for monthly reports |
MAX_UPLOAD_BYTES |
5242880 |
Maximum HTTP upload size |
FLASK_DEBUG |
false |
Development debugger |
PORT |
5001 |
Flask listen port |
Redis database 0 is shared by the broker and result backend. Redis database 1 holds cached HTTP responses only; export ownership lives in the database.
APP_ENV=production requires FLASK_SECRET_KEY and JWT_SECRET_KEY to be
present and at least 32 characters; start-up raises otherwise. Any other value
generates a random secret per process and logs a warning, so no shared default
exists in the source tree. CORS_ORIGINS is a comma-separated list; the Vite
proxy targets http://localhost:5001.
Everything that floats over the animated backdrop draws from one set of glass
tokens in theme.css rather than styling itself: --glass-bg,
--glass-bg-strong, --glass-edge, --glass-shadow, and --glass-specular,
over backdrop-filter: saturate(180%) blur(22px). The navigation capsule, the
cards, the home hero, chips, toasts, and the confirmation dialog all use them,
so a change to the material propagates everywhere at once. A @supports guard
falls back to the opaque surface colour where backdrop-filter is unavailable,
because a translucent pane without the blur is unreadable.
The backdrop itself is theme-aware through --app-beams-opacity,
--app-beams-blend, and --app-beams-tint. The beam field is bright streaks
on black: the dark appearance composites it normally, while the light one
screens it over a faintly tinted canvas, which keeps the highlights, discards
the shadows, and still leaves the effect visible.
| Entity | Important fields | Relationships and constraints |
|---|---|---|
User |
email, password hash, role, active flag | One optional student profile or company profile; email is unique |
StudentProfile |
name, roll number, branch, year, CGPA, resume URL | Belongs to one user; roll number is unique |
CompanyProfile |
name, description, point of contact, website, approval status | Belongs to one user; owns drives and placement records |
PlacementDrive |
role, package, minimum CGPA, deadline, skills, eligibility, approval status, open/closed status | Belongs to one company; owns applications |
Application |
student, drive, status, feedback, interview details, offer-letter URL | One record per student and drive, enforced by a database unique constraint |
Placement |
student, company, position, salary, joining date | Created when an application is marked PLACED |
ExportTask |
Celery task identifier, requesting user, export kind | Written when an export is queued; the authority for task result access |
- User roles:
ADMIN,COMPANY,STUDENT - Company approval:
PENDING,APPROVED,REJECTED - Drive approval:
PENDING,APPROVED,REJECTED - Drive availability:
ACTIVE,CLOSED - Application status:
APPLIED,SHORTLISTED,INTERVIEW,OFFER,SELECTED,PLACED,REJECTED
The company dashboard presents the main application path as:
APPLIED -> SHORTLISTED -> INTERVIEW -> OFFER -> SELECTED -> PLACED
| | | |
+------------+-------------+----------+----> REJECTED
The API validates both the destination status and the transition from the current status. Invalid transitions return HTTP 409.
- Registration creates an active
Userand aPENDINGCompanyProfile. - Login is rejected until the profile is
APPROVED. - An administrator can approve the company, deactivate its user, or remove the company and its related applications, placements, drives, profile, and user.
There is no administrator action that changes a company profile to REJECTED;
the current UI supports approval, account deactivation, and removal.
- An approved company creates an
ACTIVE,PENDINGdrive. - An administrator approves or rejects it.
- Students can see only
APPROVED,ACTIVE, non-expired drives. - The company can close or reopen its own drive.
- Administrators and owning companies can delete a drive and its applications.
Student eligibility checks CGPA, comma-separated branch names, and optional
minimum/maximum study year. The shared implementation accepts common branch
abbreviations such as CS, CSE, and ECE, and is used by drive listing,
application submission, and reminder generation.
- A student needs a resume URL before drive browsing is allowed.
- The drive list returns all currently visible drives and an
is_eligiblevalue for each. - Submission repeats the approval, availability, deadline, CGPA, branch, year, resume, and duplicate-application checks.
- A company advances or rejects the application and can attach feedback, interview details, and an offer-letter PDF.
- Marking an application
PLACEDcreates aPlacementrecord. The route checks that no placement already exists for the student.
Student and company dashboards enqueue a Celery export and poll the common task
status endpoint every two seconds. A completed result contains the generated
filename and record count. The authenticated download endpoint serves the CSV
from backend/exports/.
Task results expire from Redis after one hour. Ownership is an ExportTask row
written when the job is queued, so it survives cache eviction and result
expiry. Generated CSV files do not currently have a cleanup job.
The effective schedule is defined in backend/celery_app.py with the
Asia/Kolkata timezone:
| Job | Effective schedule | Behavior |
|---|---|---|
send_daily_reminders |
Daily at 09:00 | Emails each resume-bearing student about eligible drives closing within 48 hours |
generate_monthly_admin_report |
First day at 08:00 | Writes an HTML summary under backend/reports/ and emails it to ADMIN_EMAIL |
The report aggregates drives, applications, and placements created since the start of the current month and lists the five most-applied drives.
All protected routes require an Authorization: Bearer <token> header. Role
claims are checked again in the backend; browser route guards are not the
security boundary. Every request also resolves the token subject: tokens issued
to accounts that have since been deactivated or deleted are rejected with 401.
| Method and path | Purpose |
|---|---|
POST /api/auth/register |
Register a student or company |
POST /api/auth/login |
Validate credentials and issue an access token |
GET /api/health |
Unauthenticated liveness probe with a database check |
| Method and path | Purpose |
|---|---|
GET /api/admin/stats |
Aggregate platform counts |
GET /api/admin/companies |
Search and list companies |
GET /api/admin/students |
Search and list students |
POST /api/admin/users/:id/toggle-active |
Activate or deactivate a non-admin user |
POST /api/admin/companies/:id/approve |
Approve a company |
DELETE /api/admin/companies/:id/remove |
Delete a company and related records |
GET /api/admin/drives |
Search and list drives |
POST /api/admin/drives/:id/approve |
Approve a drive |
POST /api/admin/drives/:id/reject |
Reject a drive |
DELETE /api/admin/drives/:id/remove |
Delete a drive and its applications |
GET /api/admin/applications |
Filter applications by status, company, student, or position |
GET /api/admin/reports |
List generated HTML reports |
GET /api/admin/reports/:filename |
View or download a report |
| Method and path | Purpose |
|---|---|
GET /api/company/profile |
Read the authenticated company profile |
GET /api/company/drives |
List the company's drives |
POST /api/company/drives |
Create a drive |
PATCH /api/company/drives/:id/status |
Close or reopen an owned drive |
DELETE /api/company/drives/:id/remove |
Delete an owned drive and its applications |
GET /api/company/applications |
List applications for company drives |
PATCH /api/company/applications/:id |
Update status, feedback, or interview data |
POST /api/company/applications/:id/offer-letter |
Attach an offer-letter PDF |
POST /api/company/export-applications |
Enqueue a company CSV export |
| Method and path | Purpose |
|---|---|
GET /api/student/profile |
Read the authenticated student profile |
PATCH /api/student/profile |
Update name, branch, year, CGPA, or resume URL |
POST /api/student/resume/upload |
Upload a resume PDF |
GET /api/student/drives |
Search visible drives and calculate eligibility |
POST /api/student/apply/:driveId |
Apply to a drive |
GET /api/student/applications |
List the student's applications |
GET /api/student/placements |
List placement history |
POST /api/student/export-applications |
Enqueue a student CSV export |
| Method and path | Purpose |
|---|---|
GET /api/resumes/:filename |
Serve an uploaded resume |
GET /api/offer-letters/:filename |
Serve an uploaded offer letter |
GET /api/tasks/:taskId |
Inspect export progress |
GET /api/tasks/:taskId/download |
Download a completed export |
The following GET routes use Redis-backed response caching with a key containing the request path, JWT identity, and query string:
| Endpoint | TTL |
|---|---|
/api/admin/stats |
60 seconds |
/api/admin/companies |
120 seconds |
/api/admin/students |
120 seconds |
/api/admin/drives |
120 seconds |
/api/student/drives |
60 seconds |
Write routes invalidate the affected cache families by scanning the relevant Redis key prefixes. Drive approval, rejection, removal, close, and reopen operations invalidate student drive listings immediately.
| Directory | Contents | Retention |
|---|---|---|
backend/uploads/resumes/ |
Student-uploaded PDFs | Indefinite |
backend/uploads/offers/ |
Company-uploaded offer letters | Indefinite |
backend/exports/ |
Celery-generated CSV files | Indefinite |
backend/reports/ |
Celery-generated monthly HTML reports | Indefinite |
Filenames are generated from internal IDs and UUIDs. Upload validation checks
the .pdf suffix and PDF file signature. Flask rejects requests above the
configurable upload limit, which defaults to 5 MiB.
Serving a stored file follows the same three steps everywhere:
- Normalize the requested name with
secure_filename. - Find the database row that references it — the student profile for a resume, the application for an offer letter, the export-task record for a CSV. A name with no owning row is a 404, so a guessed or crafted filename grants nothing.
- Resolve the path through
resolve_stored_file(), which confines it to the intended directory and refuses anything that escapes.
| Resource | Readable by |
|---|---|
| Resume | The owning student, an administrator, and companies the student has an application with |
| Offer letter | The recipient student, the issuing company, and administrators |
| Monthly report | Administrators only, and only files ending in .html |
| Export CSV | The user who queued the export and administrators |
Because internal resume paths would otherwise be claimable, user-supplied
resume URLs must be absolute http:// or https:// links; the
/api/resumes/ namespace is writable only by the upload endpoint. A profile
update may resubmit the student's own unchanged resume path, so clients that
round-trip the whole profile still work.
Authorization is checked before any task state is disclosed, so the shared task endpoints do not confirm whether an unrelated task exists.
- A schema change touches
backend/models.py, serialization inbackend/app.py, relevant CSV/report code inbackend/tasks.py, and dashboard form/table code. There is no migration mechanism, sodb.create_all()will not alter existing tables. - A new protected endpoint should use
@jwt_required(), enforce a backend role check, scope the database query to the authenticated profile, and invalidate any cached lists it changes. Add it to the role matrix inbackend/tests/test_authorization.py, which asserts that every other role is refused. - A new endpoint that serves a stored file must locate the owning database row
before reading disk and resolve the path through
resolve_stored_file(). Never derive authorization from the filename. - An application status change affects the model vocabulary, backend transition map, company action controls, student/admin badge mappings, and CSV exports.
- Eligibility rules belong in
backend/eligibility.py; drive listing, application submission, and reminders all consume that shared policy. - A new background task must be imported by Celery, run inside the Flask application context, define result retention expectations, and establish ownership before exposing results through shared task routes.
- An internal file link must be fetched through the authenticated Axios client; ordinary anchor navigation does not attach the JWT bearer header.
- Predictable seed credentials are created automatically by every fresh database.
- SQLite assumes a single host. The Compose stack shares one database file across the web, worker, and Beat containers, which is adequate for a demo but should become PostgreSQL before real traffic.
- Uploaded and generated files need durable object storage, malware scanning, retention rules, and cleanup before handling untrusted production traffic.
- Reminder emails link to a hard-coded local development address.
- Frontend component tests and database migrations are still missing.
- Most backend behavior lives in one application-factory function exceeding 1,500 lines.
- List endpoints have no pagination; dashboard searches are debounced on the client but still return unbounded result sets.
- The one-placement rule is enforced in request logic rather than a database constraint, leaving a race condition possible.
Dockerfile builds in three stages: Node compiles the single-page application,
uv resolves the locked Python environment, and the runtime stage copies both
into a slim Python image that runs Gunicorn as the unprivileged portal user.
Flask serves the compiled bundle from frontend/dist, so one image and one
port cover the whole application.
docker-compose.yml runs that image three ways — Gunicorn, a Celery worker,
and Beat — alongside Redis, with named volumes for the database, uploads,
exports, and reports. Compose reads .env; APP_ENV is set to production,
so a missing signing key stops the stack rather than falling back to a default.
.github/workflows/ci.yml runs on every push and pull request to main:
| Job | Checks |
|---|---|
backend-tests |
Ruff, then the full suite under coverage with coverage report enforcing the floor in pyproject.toml |
authorization-tests |
Role boundaries, tenant isolation, protected file access, and secret handling as a standalone gate |
secret-scan |
No fallback values for secret environment variables and no tracked .env file |
frontend |
ESLint, the Vitest suite under its own coverage gate, the Vite production build, and npm audit on runtime dependencies |
shell |
Bash syntax validation and ShellCheck at warning severity |
docker |
Builds the image, boots the container, and polls /api/health |
The coverage gate measures the request-handling path — app.py, models.py,
eligibility.py, and email_helper.py. Celery tasks and the demo seeder are
excluded because they are exercised by scripts/api-smoke.sh against a running
stack rather than by unit tests.
The maintained baseline is:
run.shandscripts/api-smoke.shpass Bash syntax validation and ShellCheck.- Ruff reports no findings and the backend tests pass on Python 3.13, holding the coverage floor.
scripts/api-smoke.shpasses against a locally running backend.npm run lintand the Vite production build succeed.npm auditreports no known dependency vulnerabilities.- The container image builds and answers
/api/health. - Locked dependency environments remain excluded from version control.