Desktop-first job application tracker scaffold with auth, database, backend API, and a lightweight frontend shell. Email parsing is intentionally not implemented yet.
- Backend: Node.js + Express
- Database: SQLite (better-sqlite3)
- Frontend: Static HTML/CSS/JS served by the API
npm install
npm run devOpen http://localhost:3000.
- Use Node 20.20.0 for native module compatibility (see
.nvmrc). - Package engines:
>=20 <21. - If you see
better_sqlite3.node is not a valid Win32 applicationorERR_DLOPEN_FAILED, fix by:rm -rf node_modules package-lock.jsonnvm use 20(or install Node 20 LTS)npm install- If needed:
npm rebuild better-sqlite3 --build-from-source
- A preflight check runs before dev/test to catch ABI issues early. Skip only if necessary with
SKIP_PREFLIGHT=1.
Copy .env.example to .env and fill in values as needed.
Environment variables control database path, auth, OAuth credentials, and logging.
Logging defaults to info and avoids sensitive content.
Email/password sign-up requires a password (minimum 12 characters). Passwords are hashed (bcrypt) and never stored in plaintext. Sessions are stored in SQLite with httpOnly cookies and SameSite=Lax, and become secure cookies in production.
State-changing API requests require a CSRF token.
- Fetch token:
GET /api/auth/csrf→{ csrfToken } - Send the token on every
POST,PATCH, orDELETEto/api/*(headerX-CSRF-Token). - Requests missing/invalid tokens return
403.
Auth routes are rate-limited by IP (and by IP+email for login/signup):
POST /api/auth/loginPOST /api/auth/signup/api/auth/google/start/api/auth/google/callback
Defaults: 10 requests per 10 minutes. Override with:
JOBTRACK_RATE_LIMIT_MAX=10
JOBTRACK_RATE_LIMIT_WINDOW_MS=600000
Create OAuth credentials (Web application) and set:
GOOGLE_AUTH_CLIENT_ID=...
GOOGLE_AUTH_CLIENT_SECRET=...
GOOGLE_AUTH_REDIRECT_URI=http://localhost:3000/api/auth/google/callbackScopes: openid, email, profile.
Backward-compatible fallback envs are also accepted: GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_REDIRECT_URI.
Legacy Gmail API scanning is disabled. Google auth is used for identity sign-in only (openid, email, profile).
Tokens are encrypted at rest using JOBTRACK_TOKEN_ENC_KEY (base64 32-byte key):
JOBTRACK_TOKEN_ENC_KEY=...Generate a key:
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"You can ingest forwarded emails without Google OAuth by using the Postmark inbound webhook:
POSTMARK_INBOUND_SECRET=...
INBOUND_DOMAIN=mail.applictus.com- Webhook endpoint:
POST /api/inbound/postmark - Required header:
x-applictus-inbound-secret: <POSTMARK_INBOUND_SECRET> - Recipient mapping uses generated per-user addresses stored in
inbound_addresses. - Raw inbound payloads are persisted in
inbound_messagesfor debugging/dedupe.
Local smoke test script:
node server/scripts/postmark-inbound-smoke.js
# optional webhook send (requires running server + secret):
POSTMARK_SMOKE_SEND=1 node server/scripts/postmark-inbound-smoke.jsMigrations run automatically on server start. To run them manually:
node server/src/index.jsRender setup:
- Build Command:
npm install - Start Command:
npm start - Recommended Node: 20.x (set via package.json engines; Render will use this if available)
Provision a Render Disk and mount it at /var/data, then set:
JOBTRACK_DB_PATH=/var/data/applictus.sqlite
Required env vars:
JOBTRACK_TOKEN_ENC_KEY=...(base64 32-byte key)JOBTRACK_DB_PATH=/var/data/applictus.sqlite
If you use Google Sign-In:
GOOGLE_CLIENT_ID=...GOOGLE_CLIENT_SECRET=...GOOGLE_REDIRECT_URI=https://<your-render-url>/api/auth/google/callback
Notes:
- The app listens on
process.env.PORT(Render-provided). - The local default DB path is used when
JOBTRACK_DB_PATHis not set.
Use forwarding-based ingestion and trigger:
POST /api/inbound/sync
Set env vars to enable:
APPLICTUS_LLM_ENABLED=1
APPLICTUS_LLM_PROVIDER=openai_compatible
APPLICTUS_LLM_BASE_URL=https://api.openai.com/v1
APPLICTUS_LLM_API_KEY=...
APPLICTUS_LLM_MODEL=gpt-4.1-mini
APPLICTUS_LLM_MAX_INPUT_CHARS=8000
APPLICTUS_LLM_TIMEOUT_MS=8000
APPLICTUS_LLM_MAX_CALLS_PER_SYNC=20
Safety: bodies are parsed transiently, redacted before LLM, never stored. Caching avoids repeat calls; failures fall back to heuristic classification. Counters in the sync summary show LLM calls/cache hits/failures.
Job-related messages that cannot be confidently matched are kept in the "Unsorted Events" list. Use the UI to attach them to an existing application or create a new one. Auto-created applications require classification confidence >= 0.85 and company confidence >= 0.85. Known ATS senders can auto-create even if the role is unknown, but ambiguous sender domains stay unsorted. API helpers:
GET /api/email/unsorted
POST /api/email/events/:id/attach
POST /api/email/events/:id/create-application
During sync, Applictus extracts job titles from the email subject, snippet, and (when needed) the plain-text body. Only the extracted role string plus confidence/source are stored; message bodies are parsed transiently and discarded.
Debug helpers (dev/admin only):
GET /api/email/sync-debug?limit=20&reason=classified_not_job_related
GET /api/email/skipped-sample?days=30&limit=50&reason=not_job_related
Set JOBTRACK_DEV_MODE=1 or JOBTRACK_ADMIN_EMAIL to enable.
Suggestion actions:
POST /api/applications/:id/suggestion/accept
POST /api/applications/:id/suggestion/dismiss
Edit metadata, override status, archive, and merge applications:
PATCH /api/applications/:id
POST /api/applications/:id/merge
GET /api/applications/archived
Use the Archive view in the UI to restore archived applications.
Paginated list (filters: status, company, recency_days, min_confidence, suggestions_only):
GET /api/applications?limit=25&offset=0
Pipeline columns:
GET /api/applications/pipeline?per_status_limit=15
Application detail with timeline:
GET /api/applications/:id
Set JOBTRACK_DEV_MODE=1 or JOBTRACK_ADMIN_EMAIL=you@domain.com to access:
GET /api/email/events
npm run seedSeeded user: demo@applictus.dev (password defaults to applictus-demo-123).
Optional generator controls:
JOBTRACK_DEMO_COUNT=12
JOBTRACK_DEMO_EXTRA_EVENTS=4
JOBTRACK_DEMO_SEED=123
JOBTRACK_DEMO_PASSWORD=applictus-demo-123
npm testTests live in server/tests and are run with Node's built-in test runner.
- Auto-apply only when confidence >= 0.90.
- Suggest-only for 0.70-0.89 (user must confirm).
- Ghosted suggestion after 21 days of inactivity for APPLIED/UNDER_REVIEW.
- Terminal statuses (REJECTED, OFFER_RECEIVED) are locked unless user override.
- Conservative filtering can miss some job-related emails.
- Forwarding setup still depends on mailbox-provider forwarding UX.
- Email bodies are not stored; only minimal metadata.
APPLIED, UNDER_REVIEW, INTERVIEW_REQUESTED, INTERVIEW_COMPLETED, OFFER_RECEIVED, REJECTED, GHOSTED, UNKNOWN
server/src/index.js: API + session auth + migrations.server/migrations/001_init.sql: Core SQLite schema.server/migrations/002_oauth_tokens.sql: OAuth token storage.server/migrations/003_email_events_metadata.sql: Email event metadata columns.server/migrations/004_job_application_fields.sql: Job application matching fields.shared/types.js: Shared enums.shared/emailClassifier.js: Conservative job-related email classifier.shared/matching.js: Conservative thread identity extraction.shared/statusInference.js: Status inference rules.server/src/matching.js: Event-to-application matching logic.server/src/statusInferenceRunner.js: Applies inference to stored applications.web/: Desktop-first UI shell (auth, Gmail connect, dashboard placeholder).