AI-powered job application tracker that learns from your rejections.
Live in production. Free to use.
Applytic tracks every job application you submit, detects patterns across rejections (which resume version converts best, which source channel works, which company sizes respond), and uses Amazon Bedrock to turn that data into actionable coaching - delivered as a chat interface and a weekly email digest.
Built end-to-end on AWS as a production-grade application. Every service is serverless, infrastructure is code, and every push auto-deploys via GitHub Actions.
I was job hunting and had no data on why I was getting rejected. I had a spreadsheet with company names and "rejected" written next to most of them - but no signal on why. Was it my resume? The channel? The company size?
So I instrumented my own job search. Every application became a data point. After a few weeks I had enough data to see that my v1-generic resume had a 0% response rate from enterprise companies, while v3-ml-focused was getting interviews from startups via referrals. That's the kind of insight you can act on.
Application tracking
- Log applications with company, role, source channel, resume version, company size, job description URL, follow-up date
- Kanban board with drag-and-drop status updates (Applied - Screened - Interview - Offer / Rejected)
- Click any card to view full detail, edit all fields, change status, see timeline
- Search by company/role, filter by source channel
- Color-coded left border per status on kanban cards for instant visual scanning
- Amber "Follow up" badge on overdue cards
Weekly goal tracking
- Set a weekly application target on the Dashboard
- Progress bar showing current week vs goal, turns green when met
- Streak counter - consecutive weeks hitting your goal (fire icon)
- Inline goal editing - click the pencil to update your target anytime
Follow-up reminders
- Attach a follow-up date to any application
- Daily email when applications are overdue for follow-up, grouped by user
- Never miss a follow-up again
Notes timeline
- Add timestamped notes to any application
- Full per-application audit trail
- Notes sorted oldest first for easy reading
CSV export and import
- Export all applications to a CSV with one click - no Lambda, pure client-side
- Import from CSV with full validation, preview, and error reporting
- Download import template to get started quickly
AI insight engine
- Pattern analysis across 6 dimensions: source channel, company size, resume version, role seniority, weekly velocity, status funnel
- Response rate computed per bucket - shows exactly which resume version or source is working
- AI coaching chat powered by Bedrock - answers questions like "why am I getting ghosted?" using your actual data as context
- Markdown rendering in chat responses
- Weekly email digest every Monday with stats + one AI-generated personalised tip
Resume version tracker
- Upload multiple PDF versions to S3 via presigned URLs
- Tag each application with which version was used
- Analytics shows conversion rate per version side-by-side
UI / UX
- Full dark mode with system preference detection, persisted to localStorage
- Mobile responsive - hamburger sidebar on small screens
- Loading skeletons on every page instead of blank states
- Meaningful empty states with calls to action
- Toast notifications (top-center)
Single-table DynamoDB - two entity types (APPLICATION, STATUS_EVENT) in one table with composite keys. Every query is O(1). No joins. Scales to millions of requests/sec with no configuration changes.
Pattern analysis before LLM - the insights Lambda computes structured metrics (response rates per bucket) before calling Bedrock. The LLM receives hard numbers as context, not raw records. This makes advice data-driven and specific rather than generic.
Serverless-first - no EC2, no containers, no idle servers. Lambda + API Gateway + DynamoDB means ~$0 at low volume and automatic scaling. EventBridge replaces cron servers entirely.
ARM64 Lambda - all functions run on Graviton2 (ARM64) for ~20% cost reduction and faster cold starts vs x86.
Presigned S3 URLs for resume upload - the Lambda generates a presigned URL and returns it to the client. The file then uploads directly from the browser to S3 - never passes through Lambda.
Lambda Layer for shared code - single source of truth for CORS, auth extraction, error formatting, Pydantic validation, and X-Ray tracing. All seven Lambda handlers share it.
Client-side CSV - export and import are handled entirely in the browser. No Lambda invocation needed for either operation, keeping them fast and free.
Streak computation - looks back 8 weeks maximum, current week excluded (may be incomplete). Walks back week by week, breaks on first miss.
| Layer | Service |
|---|---|
| Frontend | React 18, TypeScript, Vite, Tailwind CSS, react-markdown |
| Auth | Amazon Cognito (email + JWT) |
| API | API Gateway REST + Lambda (Python 3.12, ARM64) |
| AI / ML | Amazon Bedrock - Amazon Nova Lite |
| Database | DynamoDB - single-table design, PAY_PER_REQUEST |
| Storage | S3 - resume versioning + frontend hosting |
| CDN | CloudFront (450+ edge locations) |
| Scheduling | EventBridge cron (Monday 8am UTC digest, daily 9am UTC follow-ups) |
| Amazon SES | |
| IaC | AWS CDK v2 TypeScript |
| CI/CD | GitHub Actions |
applytic/
├── cdk/ # AWS CDK v2 stack - all infrastructure as code
│ ├── bin/app.ts
│ └── lib/applytic-stack.ts
├── lambdas/ # Python 3.12 Lambda handlers
│ ├── applications/ # CRUD + presigned S3 URL generation
│ ├── insights/ # Pattern analysis + Bedrock AI coaching
│ ├── digest/ # Weekly SES email digest
│ ├── followup/ # Daily follow-up reminder emails
│ ├── settings/ # Weekly goal + streak tracking
│ ├── notes/ # Per-application notes timeline
│ ├── cognito_verify/ # Post Confirmation trigger - SES email verification
│ └── shared_layer/ # Lambda Layer - shared middleware, Pydantic, X-Ray
├── frontend/ # React + Vite + Tailwind CSS
│ └── src/
│ ├── components/ # Kanban, Analytics, Chat, Sidebar, Resume, CSV
│ ├── hooks/ # useApplications, useSettings, useNotes
│ ├── lib/ # API client, Amplify config, theme, csv utils
│ ├── pages/ # Dashboard
│ └── types/ # Shared TypeScript types
├── tests/ # 207 pytest tests (90.75% coverage)
│ ├── test_applications.py
│ ├── test_applications_integration.py
│ ├── test_insights.py
│ ├── test_insights_integration.py
│ ├── test_digest.py
│ ├── test_cognito_verify.py
│ ├── test_followup.py
│ ├── test_settings.py
│ └── test_notes.py
├── scripts/
│ ├── seed_data.py
│ ├── build_layer.sh
│ └── setup_oidc_v11.sh
└── .github/workflows/
├── deploy.yml
└── codeql.yml
Table name: applytic
| Entity | PK | SK | GSI1PK | GSI1SK |
|---|---|---|---|---|
| Application | USER#userId |
APP#appId |
USER#userId |
DATE#timestamp |
| Status event | APP#appId |
EVENT#timestamp |
- | - |
| Rate limit | RATELIMIT#userId |
DATE#YYYY-MM-DD |
- | - |
| User settings | USER#userId |
SETTINGS |
- | - |
| Note | APP#appId |
NOTE#timestamp#noteId |
- | - |
Access patterns:
- List all applications for user - GSI1 query on
USER#userIdsorted by date - Get single application - Main table get on
USER#userId+APP#appId - Get status history for an app - Query
APP#appIdwithEVENT#prefix - Get notes for an app - Query
APP#appIdwithNOTE#prefix - Get user settings - Get on
USER#userId+SETTINGS
GET /v1/applications
POST /v1/applications
GET /v1/applications/{appId}
PUT /v1/applications/{appId}
DELETE /v1/applications/{appId}
POST /v1/applications/{appId}/status
GET /v1/applications/{appId}/notes
POST /v1/applications/{appId}/notes
DELETE /v1/applications/{appId}/notes/{noteId}
POST /v1/resumes/upload-url
GET /v1/resumes/list
GET /v1/insights
POST /v1/insights/chat
GET /v1/users/settings
PUT /v1/users/settings
All routes protected by Cognito JWT authorizer.
| Metric | Value |
|---|---|
| API throughput | 10,000 req/sec (API Gateway default) |
| DynamoDB SLA | 99.999% availability |
| Lambda cold start | ~300-400ms (Python 3.12 ARM64) |
| CloudFront edge locations | 450+ worldwide |
| Cost at 0 users | ~$0/month |
| Cost at 100 users | ~$2-5/month |
| Cost at 1,000 users | ~$15-30/month |
| Backend test suite | 207 tests, 90.75% coverage |
| Frontend test suite | 14 Vitest tests |
| Full CDK deploy from scratch | under 3 minutes |
push to main
├── test (pytest 207 tests, 70% coverage threshold)
├── test-frontend (vitest 14 tests)
└── (both must pass before)
├── deploy-backend (cdk deploy)
├── deploy-frontend-aws (s3 sync + cloudfront invalidation)
└── deploy-frontend-pages (GitHub Pages)
- Node.js 18+
- Python 3.12
- AWS CLI configured (
aws configure) - AWS CDK CLI:
npm install -g aws-cdk
bash scripts/build_layer.sh
cd cdk && npm install && cdk deploycd frontend && npm install && npm run devpip install pytest boto3 pydantic aws-lambda-powertools pytest-cov
pip install "moto[dynamodb,s3,ses,cognitoidp]"
python -m pytest tests/ -v --tb=short --cov=lambdas --cov-fail-under=70cd frontend && npm run testcd scripts && python seed_data.py --user-id YOUR_COGNITO_SUB- Issue:
runtimebecame optional when usingPartial<FunctionProps>, causing TypeScript to reject undefined values. - Fix: Added
runtimeexplicitly to each Lambda and removed the shared defaults spread.
- Issue: esbuild failed due to corrupted dependencies or a Node.js version mismatch.
- Fix: Deleted
node_modulesandpackage-lock.json, reinstalled dependencies, and used Node 18/20 LTS.
- Issue: Multiple Lambda handlers were named
handler.py, causing import collisions during tests. - Fix: Used
importlib.util.spec_from_file_location()with unique module names.
- Issue:
dateAppliedwas stored as aYYYY-MM-DDstring and could not be subtracted from a timezone-aware datetime. - Fix: Detected 10-character date strings and added UTC timezone information before subtraction.
- Issue: Component-level
useStatewas reset whenever the component unmounted. - Fix: Lifted message state to
App.tsxand passed it down as props.
- Issue:
cache: 'npm'requires apackage-lock.jsonfile. - Fix: Removed the cache configuration and used
npm install.
- Issue: Vite client types were not available during CI builds.
- Fix: Added
"types": ["vite/client"]totsconfig.json.
- Issue: The selected Bedrock model reached AWS end-of-life during development.
- Fix: Updated
BEDROCK_MODEL_IDand usedaws logs tailfor debugging.
- Issue: Claude 3.7+ models require cross-region inference profiles using a
us.prefix. - Fix: Updated model configuration to use the correct inference profile identifier.
- Issue: Foundation model ARNs did not cover inference profile ARNs.
- Fix: Used
Resource: "*"for Bedrock IAM permissions.
- Issue: Claude 3.7 and Haiku 4.5 required an AWS Marketplace subscription.
- Fix: Switched to Amazon Nova Lite.
- Issue: Nova models do not accept
anthropic_versionin the request payload. - Fix: Detected the model family from
MODEL_IDand generated the appropriate payload format.
- Issue: External labels overflowed the chart container.
- Fix: Implemented a custom label renderer that draws percentage values inside slices at the midpoint radius.
- Issue:
cognito-idp:AdminGetUserpermission was missing from the IAM policy. - Fix: Added an IAM policy statement targeting the Cognito User Pool ARN.
- Issue: New AWS accounts in the SES sandbox can only send emails to verified recipient addresses.
- Fix: Added a Cognito Post Confirmation trigger that automatically verifies newly registered user emails.
- Issue:
BrowserRouterdefaulted to/while the application was hosted under/applytic/. - Fix: Passed
import.meta.env.BASE_URLas thebasenametoBrowserRouter.
- Issue: Windows automatically added
--user, which conflicts with pip's-toption. - Fix: Added the
--no-userflag to the pip install command.
- Issue: CloudFormation rejected deployment with a "Resource already exists" error.
- Fix: Preserved the original construct IDs when updating existing alarms.
- Issue:
build_layer.shproduced x86_64 wheels while Lambdas ran on ARM64, causingpydantic_coreruntime failures. - Fix: Added
--platform manylinux2014_aarch64 --only-binary=:all:to the pip install command.
- Issue: The test stub crashed on invalid JSON input.
- Fix: Replaced the lambda with a proper function that wraps
json.loads()in a try/except block and returns HTTP 400 on failure. The stub is registered insys.modulesand reused by subsequent test files.
- Issue: TypeScript builds failed after v2.0 introduced
followUpDateto theApplicationtype. - Fix: Added
followUpDate: null as string | nullto thedefaultFormobject.
- Issue: The shell interpreted brackets in
moto[dynamodb,...]as glob expansion. - Fix: Wrapped the package name in quotes during installation.
- Issue:
vi,beforeAll, andafterAllwere not available during standard TypeScript compilation. - Fix: Added
"exclude": ["src/test"]tofrontend/tsconfig.json.
See CHANGELOG.md for full version history.
See CONTRIBUTING.md for setup instructions, branching conventions, and the PR process.
Hardik - Github

