Job Portal (PHP + SQLite)
A tiny, single-file job board + applicant tracker. Public visitors can browse open roles and apply; admins can post/edit jobs, toggle status, review applications, star candidates, and download resumes.
Stack: PHP 8+, SQLite, Bootstrap 5 (CDN).
Data:db.sqlite(auto-created), uploads saved touploads/.
Features
- Public listing of Open roles with search (title/type/location)
- Job details page with Apply form (resume upload optional)
- Admin login (password only), post/edit/close/restore/trash jobs
- Soft-delete for jobs (Trash view), hard-delete for applications
- Review applications per job or in a global table
- Star / unstar candidates
- Resume downloads (PDF/DOC/DOCX), 5 MB max
- CSRF protection, HTML escaping, prepared statements, basic upload validation
- Minimal, single-file deployment
Requirements
- PHP 8.0+ (uses
match,random_bytes, PDO SQLite,fileinfo)- Extensions:
pdo_sqlite,fileinfo
- Extensions:
- Web server (or PHP built-in server)
- Write permissions for the app directory (to create
db.sqliteanduploads/)
Quick start
-
Place the file
- Save the provided PHP as
index.phpin an empty directory.
- Save the provided PHP as
-
Set an admin password (recommended)
# Linux/macOS export JOBPORTAL_ADMIN='change-me-super-secret' # Windows PowerShell setx JOBPORTAL_ADMIN "change-me-super-secret"
If not set, it falls back to
DEFAULT_ADMIN_PASSWORD = 'password'in the file (change it!). -
Run locally
php -S localhost:8000
-
Log in as admin
- Nav → Admin → enter your password.
- Post your first job via Post a Job.
The app auto-creates
db.sqlite(SQLite) and theuploads/folder as needed.
Configuration
All configuration lives at the top of the file:
JOBPORTAL_ADMIN(env var): admin password (recommended way)DEFAULT_ADMIN_PASSWORD: fallback if env var is not setUPLOAD_DIR: defaults to./uploadsMAX_UPLOAD_BYTES: default 5 MBALLOWED_EXTS:pdf, doc, docxALLOWED_MIME: matching MIME types
How it works (high-level)
- Routing via
?action=...query param:- Public:
list(default),view_job&id=…,apply_submit(POST) - Admin:
login(GET/POST),logout,new_job,edit_job&id=…,
create_job(POST),update_job(POST),
toggle_job(POST),delete_job(POST),restore_job(POST),
applications,view_application&id=…,
delete_application(POST),toggle_star(POST),
download&id=…(resume download)
- Public:
- Database (SQLite, WAL mode, foreign keys on):
jobs:id, title, location, type, description, is_active, created_at, deleted_at
applications:id, job_id (FK), name, email, phone, portfolio, resume_path, cover_letter, created_at, starred
- Index:
applications(job_id) - Simple migrations attempt to add new columns if missing.
- Security:
- CSRF tokens for all POSTs
htmlspecialchars()wrapper for all outputs- PDO prepared statements
- Upload validation: ext + MIME + size, randomized filename, saved in
uploads/
Using the app
Public
- Home (
/): Open roles (searchable), card list - Job detail: About the role + Apply form
- Fields: name*, email*, phone, portfolio URL, optional resume file, cover letter
- After submit, application is saved and an admin flash is shown
Admin
- Login/Logout: password only
- Post/Edit Job: title*, description*, type, location, active flag
- Close/Reopen a job, Trash (soft delete) and Restore from Trash
- Applications: per job or global table
- Star/Unstar, Download resume, Delete application
Production notes
- Set
JOBPORTAL_ADMINand consider removing/ignoring theDEFAULT_ADMIN_PASSWORDfallback. - HTTPS: serve behind HTTPS so admin sessions & uploads aren’t exposed.
- Permissions:
- The process must write to the app directory (or wherever
db.sqlite/uploads/live).
- The process must write to the app directory (or wherever
- Uploads security:
- Files are saved with random names and served through
downloadaction with a strictContent-Type. - To be extra cautious, block direct web access to the
uploads/directory or place it outside the web root and updateUPLOAD_DIR. - Example Apache
.htaccessto deny direct access:# uploads/.htaccess Require all denied
- Files are saved with random names and served through
- Backups: back up
db.sqliteanduploads/. - Headers (optional hardening via server config):
X-Content-Type-Options: nosniffContent-Security-Policy(if you tighten CDNs), etc.
- Error display: disable in production:
display_errors = Off log_errors = On
Known limitations / ideas
- No email notifications to admins
- No pagination (jobs/applications)
- No rate limiting / CAPTCHA on Apply
- Single admin password (no users/roles)
- Files stored on local disk (no S3, etc.)
Routes reference
| Action | Method | Purpose |
|---|---|---|
list (default) |
GET | Open roles (public). Admins can switch scopes: open, all, trash. |
view_job&id={jobId} |
GET | Job details + Apply form (if active). |
apply_submit |
POST | Submit application for a job. |
login |
GET/POST | Admin login. |
logout |
GET | End admin session. |
new_job |
GET | New job form (admin). |
create_job |
POST | Create job (admin). |
edit_job&id={jobId} |
GET | Edit job form (admin). |
update_job |
POST | Update job (admin). |
toggle_job |
POST | Open/close job (admin). |
delete_job |
POST | Soft-delete job (admin). |
restore_job |
POST | Restore a soft-deleted job (admin). |
applications |
GET | Global applications table (admin). Optional job_id filter. |
view_application&id={appId} |
GET | Application detail (admin). |
toggle_star |
POST | Star/Unstar application (admin). |
delete_application |
POST | Hard-delete application (admin). |
download&id={appId} |
GET | Download candidate resume (admin). |
Customization
- Update branding in
page_header()(navbar title “Company Jobs”). - Adjust allowed file types and size at the top constants.
- Change the list of job Types in
job_form()if needed.
License
Choose your license (e.g., MIT) and add it here.
On this page
Contributors
Created August 23, 2025
Updated August 23, 2025