Skip to content

Latest commit

 

History

315 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

DACHApply

This is a full-stack Django and React application for collaborative job lead collection, structured job evaluation, manual ChatGPT-based analysis, and application prioritization.

Live app

Hosted on Azure Container Apps: https://dachapply.livelysea-3461ad21.westeurope.azurecontainerapps.io

Access may require an account.

Workflow preview

DACHApply workflow preview: add a job, import structured fit analysis, and prepare application documents

Add a job link → copy the ChatGPT prompt → import structured JSON → review fit → generate application documents in local mode.

The hosted analysis workflow requires no paid LLM API. The document-generation step shown at the end is explicitly labeled Local mode.

Purpose

DACHApply is a private job intelligence dashboard for a Software Engineer job search in Austria/Germany. Friends can submit relevant job links using invite codes. The owner reviews leads, generates reusable ChatGPT prompts without paid LLM APIs, imports strict JSON evaluations, and tracks applications, notes, and follow-ups.

Architecture

  • backend/: Django, Django REST Framework, SQLite, Django auth/admin, pytest tests
  • frontend/: React + TypeScript + Tailwind CSS (Vite)
  • Production: PostgreSQL via DATABASE_URL is supported; SQLite remains suitable only for local/MVP testing
  • React production build is served by Django/WhiteNoise

Core workflow

  1. Friend opens /public-submit, enters invite code once, and may submit either full job details or just a job URL. After a successful submission, the browser remembers the invite code for next time.
  2. Owner logs in at /login and views dashboard.
  3. Owner can paste a list of job links in /prompts and generate a Bulk Links Prompt for ChatGPT. The returned JSON can create new jobs with details and nested evaluations.
  4. Owner can also select URL-only/incomplete existing jobs in /prompts and generate a Missing Details Prompt for ChatGPT.
  5. Owner pastes ChatGPT's job_updates or jobs JSON into /import; the app updates or creates the correct job records.
  6. Owner selects completed jobs in /prompts and generates an Evaluation Prompt with embedded candidate profile.
  7. Owner pastes strict evaluation JSON into /import.
  8. Backend validates required fields and stores JobEvaluation records.
  9. Dashboard/job detail pages show score, priority, recommendation, gaps, status, and next action with clear badges.

This reduces repeated LLM token usage by centralizing reusable prompts and structured imports while avoiding paid LLM API calls.

Backend stack

Django, Django REST Framework, built-in auth/session auth, admin, SQLite, WhiteNoise, pytest.

Frontend stack

React, TypeScript, React Router, Tailwind CSS, fetch-based API client with session cookies.

Local setup and run instructions

From the project root:

Python dependencies are managed with uv. uv run creates and updates .venv from uv.lock on demand, so there is no venv to create or activate by hand — and if .venv is ever lost, the next uv run rebuilds it in seconds.

Before running migrate or any other manage.py command: if the repo-root .env has DATABASE_URL set to a remote database (production or otherwise), manage.py refuses to start rather than risk a local command reaching it silently — you'll see DATABASE_URL came from a .env file.... This is deliberate, not a bug:

  • Serving the app locally (TASK-111, owner decision): runserver and check_mailbox are exempt from the guard and use the .env DATABASE_URL directly, so the local app and the deployed site always show the same remote data. No flag needed.
  • Everything else with an empty DATABASE_URL: manage.py uses backend/db.sqlite3; set DB_NAME=<path> for a different sqlite file (see .env.local.example).
  • Deliberately reaching a remote database (e.g. the .env.local-neon.example workflow): opt in for that one command, e.g. DACHAPPLY_ALLOW_PROD_DB=1 uv run manage.py migrate. The guard only ever looks at values sourced from a .env file — a DATABASE_URL you export in your own shell for a single command is never blocked.
uv sync
cd frontend
npm install
cd ../backend
uv run manage.py migrate
uv run manage.py createcachetable
python manage.py createsuperuser
python manage.py seed_demo
python manage.py runserver 127.0.0.1:8000

In a second terminal:

cd frontend
npm run dev

Open http://localhost:5173.

Useful pages:

  • Owner login: http://localhost:5173/login
  • Dashboard: http://localhost:5173/
  • Friend submission: http://localhost:5173/public-submit
  • Prompt generator: http://localhost:5173/prompts
  • Import evaluation JSON: http://localhost:5173/import
  • Data export/import: http://localhost:5173/export
  • Django admin: http://127.0.0.1:8000/admin/

Seed invite code:

FRIEND-DEMO

For a quick non-interactive local demo user, run the seed command below and log in with:

demo@dachapply.com / DemoApply2026!

For same-origin production-style local serving through Django:

cd frontend
npm install
npm run build
cd ../backend
python manage.py collectstatic --noinput
python manage.py migrate
python manage.py createcachetable
python manage.py runserver 127.0.0.1:8000

Open http://127.0.0.1:8000.

Reaching the app from another device on your network

Off by default: with nothing set, the backend binds 127.0.0.1:8000 and only this machine can reach it. To let a phone or a second laptop on the same home network open the board, put one line in the repo-root .env (it survives a reboot, and both the launcher and Django read it):

DACHAPPLY_LAN_ACCESS=1

That single flag moves the three things that have to move together:

  • scripts/dachapply-local-runtime.cmd binds 0.0.0.0:8000 instead of 127.0.0.1:8000;
  • it also runs npm run build, so Django serves the built app itself at http://<this-host>:8000/. Without the build, / redirects to FRONTEND_URL (http://localhost:5173) and a phone resolves that localhost to itself — the one failure that survives a correct bind and a correct ALLOWED_HOSTS. Serving from :8000 also keeps the login POST same-origin, so no CSRF or CORS rule is relaxed;
  • ALLOWED_HOSTS and CSRF_TRUSTED_ORIGINS gain this host's own private IPv4 addresses (detected at startup with socket.gethostbyname_ex) and the names this host answers to (hostname, its FQDN, and <hostname>.local), plus the http://<address-or-name>:8000 origin of each. Never *, never a public address, never a .local wildcard — a name that is not this host's is answered with 400 Bad Request. If detection is wrong (VPN, second NIC, a name only your router knows), list them yourself; addresses and names share the one variable: DACHAPPLY_LAN_HOSTS=192.168.8.130,caren.local.

Then open either form on the other device:

  • http://<this-host-lan-ip>:8000/ — ipconfig prints the address;
  • http://<this-host-name>:8000/ — hostname prints the name.

Which name forms work

Three are trusted, and which one a given device can resolve is decided on the device, not by anything in this repository. On the machine this was built on (hostname → Caren) they are:

Form Example Usually resolved by
bare hostname http://caren:8000/ Windows, over NetBIOS/LLMNR. Not iOS, and not reliably Android
.local (mDNS) http://caren.local:8000/ iPhone/iPad and macOS, Windows 10+, Linux with Avahi. Android has historically been uneven
FQDN http://caren.lan:8000/ only devices your router hands that DNS suffix to

Case does not matter (http://CAREN.local:8000/ works), and a trailing dot is fine.

If a phone cannot open any of them, no server change will fix it. Name resolution happens on the device: if it cannot turn caren.local into 192.168.8.130, the request never reaches this machine, so there is nothing here to widen. Android's mDNS support has historically been uneven, and some networks (guest Wi-Fi, "client isolation", a mesh extender) block mDNS outright. What works instead, in order of effort:

  1. use the IP address — http://192.168.8.130:8000/ still works and is unaffected;
  2. give the router a static DHCP lease for this machine plus a DNS/hosts entry for its name, so the name resolves for every device on the LAN without mDNS;
  3. on a second laptop, add the line 192.168.8.130 caren to its own hosts file (C:\Windows\System32\drivers\etc\hosts, or /etc/hosts).

If you reach the app under a name this repository did not derive (a router-supplied alias, for instance), add it to DACHAPPLY_LAN_HOSTS as above — otherwise Django refuses the request with 400 Bad Request and says which host header it saw.

Opening the Windows firewall for port 8000

Inbound 8000 is blocked by default, so this is required and is a one-time step. Run it in an elevated cmd (Run as administrator). The rule is persistent — it survives a reboot:

netsh advfirewall firewall add rule name="DACHApply LAN 8000" dir=in action=allow protocol=TCP localport=8000 profile=private remoteip=localsubnet

profile=private keeps it off public Wi-Fi networks and remoteip=localsubnet limits it to the local subnet. To remove it again (also elevated):

netsh advfirewall firewall delete rule name="DACHApply LAN 8000"

What this exposes

The local runtime runs with DEBUG=True against the production database, so while LAN access is on, the real board, the real mailbox data and the real CV workspace are reachable by anything on the home network. The only thing in front of them is the Django login. That is why it is opt-in, why the firewall rule is scoped to the private profile and the local subnet, and why it is worth turning back off (remove the .env line, delete the rule) when you no longer need it.

User data export/import

Logged-in users can open /export and export jobs/application data, dashboard preferences, or both. Each export option supports JSON, CSV, and XLSX. Preferences include theme, column visibility, skill overrides, and work-mode badge colors. Exports do not include passwords, sessions, tokens, permissions, admin logs, invite codes, or secrets.

To restore data, open /export and drop or click-to-select a .json, .csv, or .xlsx file. Import automatically detects whether the file contains jobs, preferences, or both. Job imports run server-side in a database transaction and imported records are assigned to the currently logged-in user; uploaded files are processed immediately and are not stored permanently.

API endpoints are also available for authenticated users:

  • GET /api/export/
  • POST /api/import/

Seed data

cd backend
python manage.py seed_demo

Creates one active invite code (FRIEND-DEMO) plus the public demo user (demo@dachapply.com / DemoApply2026!) with rich sample data: multiple active interviews, friend referrals, evaluations, notes, follow-ups, archived/skipped jobs, and a pending friend request. Admin setup instruction: python manage.py createsuperuser.

Tests and verification

Backend tests:

cd backend
uv run pytest -q

Tests run against an in-memory SQLite database via config/settings_test.py; they never touch the production Neon instance or the real CV workspace.

Frontend production build check:

cd frontend
npm install
npm run build

Manual MVP verification checklist:

  1. Start backend and frontend using the local setup commands above.
  2. Go to /public-submit, use invite code FRIEND-DEMO, and submit a job with just a URL if desired.
  3. Invalid invite code should show a validation error.
  4. Log in at /login as the owner.
  5. Confirm the dashboard loads jobs from the backend.
  6. Go to /prompts, select the URL-only job, and click Generate Missing Details Prompt.
  7. Paste ChatGPT job_updates JSON into /import; confirm company/title/details update.
  8. Open a job detail page from the dashboard.
  9. Go to /prompts, select the submitted job, generate and copy the evaluation prompt.
  10. Paste valid ChatGPT-style evaluation JSON into /import and import it.
  11. Confirm the dashboard/job detail show fit score, priority, recommendation, gaps, and next action.
  12. Confirm /export downloads JSON, CSV, and Markdown exports.

Important API endpoints

  • POST /api/public/submit/
  • GET/POST /api/jobs/
  • GET/PATCH/DELETE /api/jobs/{id}/
  • POST /api/prompts/generate/ for evaluation prompts
  • POST /api/prompts/enrich/ for missing-detail prompts
  • POST /api/prompts/bulk-links/ for a list of links that should become jobs plus evaluations
  • POST /api/evaluations/import/ for evaluation JSON, job_updates JSON, or bulk jobs JSON
  • GET /api/stats/
  • GET /api/export/jobs.json, .csv, /api/export/chatgpt-brief.md
  • DELETE /api/auth/account/ to delete the current account and owned dashboard data

Data and privacy notes

DACHApply stores the data needed to provide a private job-search dashboard:

  • Account username/email and Django password hash.
  • Candidate profile, scoring rules, optional structured profile fields, and prompt templates.
  • Job leads, URLs, descriptions, salary/language/work-mode fields, application statuses, status dates, follow-up dates, and source metadata.
  • Imported/generated evaluations, fit scores, recommendation fields, skills, gaps, notes, and follow-ups.
  • Friend-submission relationships and submitted-by metadata when enabled.

Exports are available from the Data page and include the current user's profile/jobs/evaluations/notes/follow-ups plus optional frontend preferences. Invite codes, passwords, sessions, permissions, and other credentials are intentionally excluded from exports.

Users can delete their account from the Data page. Account deletion removes the user, profile, and owned dashboard jobs; evaluations, notes, and follow-ups attached to those jobs are deleted by cascade. Any remaining references created by Django relations are anonymized via nullable user fields.

Production deployment configuration

DACHApply is configured through environment variables. In production (DEBUG=False) the app refuses to start if critical values are missing.

Required production variables:

SECRET_KEY=strong-unique-secret
DEBUG=False
ALLOWED_HOSTS=your-domain.example.com
FRONTEND_URL=https://your-domain.example.com
CSRF_TRUSTED_ORIGINS=https://your-domain.example.com
DATABASE_URL=postgresql://USER:PASSWORD@HOST:5432/DBNAME
DEFAULT_FROM_EMAIL=DACHApply <noreply@your-domain.example.com>
EMAIL_HOST=smtp.example.com

Recommended production variables:

CORS_ALLOWED_ORIGINS=https://your-domain.example.com
SECURE_SSL_REDIRECT=True
USE_X_FORWARDED_PROTO=True
SESSION_COOKIE_SECURE=True
CSRF_COOKIE_SECURE=True
DB_CONN_MAX_AGE=600
DB_SSL_REQUIRE=True
EMAIL_BACKEND=django.core.mail.backends.smtp.EmailBackend
EMAIL_PORT=587
EMAIL_USE_TLS=True
EMAIL_HOST_USER=smtp-user
EMAIL_HOST_PASSWORD=smtp-password

Abuse protection is enabled with Django REST Framework cache-backed throttles on login, registration, password reset requests, public submissions, and import endpoints. Optional rate-limit overrides:

RATE_LIMIT_LOGIN_IP=10/minute
RATE_LIMIT_LOGIN_ACCOUNT=5/minute
RATE_LIMIT_REGISTER_IP=5/hour
RATE_LIMIT_PASSWORD_RESET_IP=5/hour
RATE_LIMIT_PASSWORD_RESET_EMAIL=5/hour
RATE_LIMIT_PUBLIC_SUBMIT_IP=20/hour
RATE_LIMIT_IMPORT_USER=60/hour

Optional hardening after HTTPS is verified:

SECURE_HSTS_SECONDS=31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS=True
SECURE_HSTS_PRELOAD=True

See .env.example for a full template. For email environment separation, see .env.local.example, .env.azure.example, and docs/email-setup.md. Additional beta launch checklists are in docs/production-readiness.md and docs/backup-restore.md.

Deployment checklist

Before public deployment:

  1. Set DEBUG=False.
  2. Generate and set a strong SECRET_KEY; never use dev-only-change-me publicly.
  3. Set ALLOWED_HOSTS to the exact production hostnames.
  4. Set FRONTEND_URL and CSRF_TRUSTED_ORIGINS with https:// origins.
  5. Use PostgreSQL via DATABASE_URL; do not rely on SQLite for public multi-user production.
  6. Set SMTP email variables and test password reset delivery.
  7. Keep SESSION_COOKIE_SECURE=True and CSRF_COOKIE_SECURE=True.
  8. Enable SECURE_SSL_REDIRECT=True and USE_X_FORWARDED_PROTO=True behind a TLS-terminating proxy/load balancer.
  9. Run migrations and collect static files.
  10. Create a non-shared admin account and use strong passwords.
  11. Verify login, CSRF-protected POSTs, export/import, prompt generation, and password reset on the deployed domain.
  12. After HTTPS is stable, enable HSTS.
  13. Configure database/backups outside the app.
  14. Review logs for DisallowedHost, CSRF, email, and database connection errors.

Container image deployment with private GHCR

The Docker image is built by GitHub Actions and pushed to the private GitHub Container Registry package ghcr.io/ermischo/dachapply with both latest and commit-SHA tags. Keep this GHCR package private; do not make the image public.

Azure Container Apps should pull ghcr.io/ermischo/dachapply:latest from GHCR using registry credentials backed by a GitHub PAT that has read:packages permission. Application secrets such as DATABASE_URL and SECRET_KEY must remain Azure Container Apps secrets/environment references and must not be committed to the repository.

Delete the old Azure Container Registry only after Azure Container Apps has successfully pulled and is running the GHCR image.

Azure App Service deployment (retired)

The App Service path and its deploy-azure.yml workflow were retired on 2026-08-15, along with requirements.txt, which existed only to feed them. Its last run was 2026-06-05; Azure Container Apps has been the deploy path since. Both files remain in git history if the path is ever revived — note that reviving it needs a pip-installable dependency list, since App Service's Oryx builder looks for requirements.txt and this repo now resolves solely from uv.lock:

uv export --no-dev --no-hashes --no-emit-project --format requirements-txt -o requirements.txt

Use PostgreSQL for public deployment. SQLite can still be used for local testing or throwaway demos only.

Minimum Azure environment variables:

  • SECRET_KEY
  • DEBUG=False
  • ALLOWED_HOSTS=your-app.azurewebsites.net
  • FRONTEND_URL=https://your-app.azurewebsites.net
  • CSRF_TRUSTED_ORIGINS=https://your-app.azurewebsites.net
  • DATABASE_URL=postgresql://USER:PASSWORD@HOST:5432/DBNAME
  • SECURE_SSL_REDIRECT=True
  • USE_X_FORWARDED_PROTO=True
  • SMTP email variables listed above

Azure cost-control checklist

  • Use Free F1 App Service for testing.
  • For private throwaway demos only, SQLite can reduce cost; for any public deployment use PostgreSQL via DATABASE_URL.
  • Disable always-on features not available/free.
  • Avoid Application Insights paid ingestion until needed.
  • Export backups manually from the app/admin.

Screenshots

Add screenshots here after running locally:

  • Dashboard: searchable/filterable job table with status, priority, and recommendation badges.
  • Friend submission form: simple invite-code + URL-first submission page.
  • Prompt generator: bulk links area, existing-job selector, selected preview, generated prompt, copy button.
  • Import evaluation page: JSON textarea, clear validation errors, success summary listing created/updated jobs.
  • Job detail: clean metadata header, badges, fit score card, match/gap cards, next action, raw description, notes.

Security notes

  • Django CSRF/session authentication enabled.
  • Owner APIs require authentication by default.
  • Public submission requires active invite code and includes a honeypot spam field. The frontend stores the invite code in that browser's localStorage after a successful submission, so trusted friends do not need to retype it every time.
  • URLs are validated by serializers/model fields. Common pasted forms such as https-www.karriere.at-jobs-7794074 are normalized to https://www.karriere.at/jobs/7794074.
  • Secrets are read from environment variables; use .env.example as a template.

Future improvements

  • Azure PostgreSQL
  • Azure Blob Storage for backups/exports
  • GitHub Actions deployment
  • Application Insights logging
  • Browser extension for saving jobs
  • Optional LLM API mode
  • Email reminders
  • CV version management
  • Recruiter message generator
  • Interview preparation module

About

Production-deployed AI-assisted job intelligence platform for DACH applications: Django/DRF, React/TypeScript, PostgreSQL, Docker, CI/CD and Azure.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages