Your job search journey, finally in rhythm.
JobPulse is a job application tracker. It gives you an instant pipeline, gentle reminders and zero lost replies. Log every application, move it through your hiring pipeline, see how your search is really going, and keep your mentor or career coach in the loop.
Built with Next.js (App Router), TypeScript, Tailwind CSS, Prisma and PostgreSQL.
- Features
- Tech stack
- Architecture
- Project structure
- Getting started
- Environment variables
- Database
- Authentication
- Live updates
- Collaboration
- Reminders and email
- SEO and accessibility
- Testing
- Deploying
- Roadmap
- Commit convention
- Known limitations
| Area | What you can do |
|---|---|
| Organize | Save the company, role, location, posting link, source, applied date and notes for every application. |
| Status | Move each application through a pipeline: applied, screening, interview, offer, rejected or withdrawn. Use the stage dropdown or the drag-and-drop board (keyboard accessible). |
| Timeline | Every stage change is recorded as an event, so the detail page can always show what happened and when. |
| Follow up | Set follow-up dates and interview dates. Overdue follow-ups are surfaced first. |
| Progress | The dashboard counts applications by stage and works out your response rate and offer rate, and explains how they are calculated. |
| Search and filter | Search by company and role, filter by stage, source and date range, and sort. All of it lives in the URL, so a refreshed or shared link shows the same view. |
| Notes | Keep interviewer names, questions asked and salary ranges with the application they belong to. |
| Next steps | See what is due today and optionally get a daily email with follow-ups and upcoming interviews. |
| Live updates | Changes made in one tab or device appear in your other tabs and devices without a reload. |
| Custom workflows | Define your own pipeline (for example Phone screen, Take-home, Panel) or start from a template for job search, internship or freelance. |
| Collaboration | Invite a mentor or coach, choose which applications are shared, and give them Editor or Viewer access. |
| Export | Download your applications as CSV. |
| Layer | Choice |
|---|---|
| Framework | Next.js (App Router) with server components |
| Language | TypeScript (strict mode, path aliases for src folders) |
| Styling | Tailwind CSS, mobile-first, with a shared type scale and a neutral palette plus a single link accent |
| Icons | lucide-react |
| Database | PostgreSQL (hosted in production, Docker locally) |
| ORM | Prisma |
| Validation | Zod |
| Passwords | Argon2id |
| Resend | |
| Real time | Server-Sent Events over a pluggable event channel |
| Tests | Service tests and Playwright end-to-end tests |
| CI | GitHub Actions (lint, type-check, build) |
| Package manager | pnpm |
Browser: server-rendered HTML plus small client components for interaction
│
Next.js (App Router)
├─ Routes (src/app) pages, layouts, route handlers. Thin: no business logic
├─ Server Actions form mutations, validated with Zod
├─ Services (src/server/services) business rules and ownership checks
├─ Repositories (src/server/db) Prisma queries only
└─ Cross-cutting auth, validation, errors, env checks
│
PostgreSQL (hosted) · email provider · event channel
- Routes stay thin. Pages, layouts and route handlers call into services. They hold no business logic.
- Server Actions validate first. Every mutation checks its input with a Zod schema before doing anything else.
- Services own the rules. Ownership checks, stage transition rules and permission checks live in
src/server/services. - Repositories only talk to the database. Prisma queries live in
src/server/dband nothing else imports Prisma directly. - One permission function. Every read or write asks one question: may this user do this action on this resource? That check lives in a single
can()function. - History is written with the change. Moving an application writes its
StatusEventin the same database transaction.
Adding an application:
- The user fills in the form and presses save.
- The Server Action validates the input with Zod.
- The service attaches the signed-in user's id and checks ownership.
- The repository inserts the row.
- The tagged cache is revalidated, the list refreshes without a full page reload, and a change event is published for the user's other tabs.
src/
├─ app/
│ ├─ (marketing)/ public pages: home, resources, optimize, enterprise
│ ├─ (auth)/ sign up, login, password reset
│ ├─ (app)/ private pages, noindex, protected by the proxy
│ ├─ api/ route handlers (events stream, cron, CSV export)
│ ├─ robots.ts
│ ├─ sitemap.ts
│ └─ opengraph-image
├─ components/ UI components and primitives (Heading, Text, Button, LinkButton)
├─ server/
│ ├─ services/ business rules and ownership checks
│ ├─ db/ repositories (Prisma queries only)
│ └─ realtime/ event channel adapters (in-memory, Postgres)
├─ types/ shared types and navigation data
docs/
prisma/ schema, migrations and seed script
The folder names above follow the architecture rules. Adjust them if your tree differs.
- Node.js (current LTS)
- pnpm
- Docker (for the local PostgreSQL database)
-
Clone the repository and install dependencies:
git clone https://github.com/<your-username>/job-pulse.git cd job-pulse pnpm install
-
Create your environment file and fill in
DATABASE_URL:cp .env.example .env
-
Start the database (see Starting the local database).
-
Run the migrations:
pnpm db:migrate
-
Optionally seed sample data:
pnpm db:seed
-
Start the dev server:
pnpm dev
Open http://localhost:3000.
In development, password reset links print to the terminal, so you do not need an email provider.
| Command | What it does |
|---|---|
pnpm dev |
Start the development server |
pnpm build |
Create a production build |
pnpm start |
Run the production build locally |
pnpm lint |
Run the linter |
pnpm db:migrate |
Create and apply migrations in development |
pnpm db:deploy |
Apply existing migrations to a production database |
pnpm db:seed |
Seed the database with sample data |
Before you push, run pnpm build locally. It catches type errors and missing environment variables that pnpm dev hides.
| Name | Required | Purpose |
|---|---|---|
DATABASE_URL |
yes | PostgreSQL connection string |
DIRECT_URL |
production | Non-pooled Postgres connection used for LISTEN. Without it, the in-memory channel only works on a single server instance |
NEXT_PUBLIC_SITE_URL |
yes in production | Public address, used for links, sitemap and social images |
RESEND_API_KEY |
production only | Sends password reset, reminder and invite emails. In development the link prints to the terminal |
EMAIL_FROM |
no | Sender address for emails |
CRON_SECRET |
production | Secret verified by the cron route that sends reminders |
Environment variables are validated at startup, so a missing required value fails fast with a clear message instead of breaking at runtime. Never commit .env. Keep .env.example up to date when you add a variable.
PostgreSQL with Prisma. Main models: User, Session, Application, StatusEvent, Interview, FollowUp, Workflow, WorkflowStage, Reminder, Team, Membership and Comment.
Indexes cover userId, appliedAt and the search fields.
The local database runs in Docker as a container named jobpulse-db.
docker ps -a # check the container exists and see its state
docker start jobpulse-db # start it
docker ps # confirm it is running- Development:
pnpm db:migratecreates and applies a migration after you changeschema.prisma. - Production:
pnpm db:deployapplies the migrations that already exist. Run it against the production database whenever a migration is added.
Accounts use email and password.
- Passwords are hashed with Argon2id.
- Sessions are random tokens stored in an httpOnly cookie. Only the SHA-256 hash of a token is stored in the database, so a database leak does not expose usable sessions.
- A proxy file (called middleware before Next.js 16) checks the session cookie on every private
(app)route. - Sign-in and sign-up attempts are rate limited.So, will reset password
- Password reset works through an emailed token.
- Helpers
getCurrentUserandrequireUsergive server code a safe way to read the signed-in user. - Sign-up and sign-in input is validated with Zod.
Changes made in one tab or device show up in the user's other tabs and devices without a reload.
How it works in short:
- Stage changes use optimistic updates, then the server confirms. If a save fails, the UI rolls back with a clear message.
- A Server-Sent Events route streams a user's own changes to their other open tabs and devices.
- Tabs in the same browser stay in sync with
BroadcastChannel. - A connection status indicator shows whether live updates are connected.
- A
versioncolumn provides optimistic concurrency, so a stale update is rejected and the user is prompted to refresh.
- A stream lasts at most 240 seconds, then the browser reconnects. Hobby functions are limited to 300 seconds.
- Events are at-most-once. After any gap the browser refreshes once to catch up.
- Each open tab holds one server function while connected. Hidden tabs release it after 60 seconds.
- Run the app and open it in two different browsers (or one normal and one private window), signed in as the same user.
- Add an application in one. It should appear in the other within a few seconds.
- Stop the dev server and start it again. The indicator goes amber, then green, and the page refreshes once.
Swap the Postgres channel for a hosted pub/sub service by implementing EventChannel (publish, subscribe) in src/server/realtime/ and returning it from getChannel().
Invite a mentor or coach from Settings, Team, choose which applications are Shared, and give them Editor or Viewer access. See docs/collaboration.md for the roles, the privacy model and the limits.
| Role | Can do |
|---|---|
| Owner | Everything, including managing members and revoking access |
| Editor | View and edit shared applications, and comment |
| Viewer | View shared applications |
Also to include:
- Invite by email with a token and an accept-invite flow
- Comments and an activity feed on applications
- Read-only share links with an expiry
- An audit log of permission-sensitive actions
- Access revocation and member removal
- Shared pages are marked
noindex
- Users set follow-up dates and interview dates, and overdue follow-ups are detected by the service layer.
- A cron route handler finds due reminders and sends them through Resend. The route verifies
CRON_SECRETbefore doing anything. - Notification preferences control which emails a user receives, including the optional daily summary.
- Emails include an unsubscribe link.
SEO
metadataBase, a title template and a default description- A per-page metadata helper with canonical URLs
- Open Graph and Twitter images
robots.tsandsitemap.tsWebApplicationJSON-LD on the home page- Private
(app)routes and shared pages are markednoindex
Accessibility
- One
h1per page, proper landmarks, a skip link and visible focus styles - Accessible forms with field-level errors
- Keyboard support for drag and drop on the pipeline board
- Charts have text alternatives and a data table fallback
Mobile
- Mobile-first Tailwind: base classes are for phones, and
sm:,md:andlg:add changes for larger screens - The navbar switches to a menu button below 1024px, and mega menus open on tap as well as hover
- Service tests cover validation, session guards, stage transitions, history writes, permissions (the full role matrix) and stats edge cases such as having no responses yet.
- Playwright end-to-end tests cover add, edit and delete, and live updates across two tabs.
- CI runs lint, type-check and build on every push.
- Set the environment variables in your hosting dashboard.
- Run
pnpm db:deployagainst the production database. - Deploy the app. On Vercel, every branch and pull request gets a preview deployment, so test there before merging to
main. - Use separate environment variables for Preview (a test database and test API keys) so testers cannot touch real data.
- Configure the scheduled job that calls the reminders cron route, with
CRON_SECRETset.
Build one feature at a time. Finish and deploy each before starting the next.
- 1. Foundation, typography and SEO: strict TypeScript, typed env validation, type scale, route groups, SEO metadata, app shell, CI
- 2. Database and authentication: Prisma, users and sessions, sign-up and login, protected routes, rate limiting, password reset
- 3. Application tracking: add, edit, delete and list applications, empty states, pending states and toasts
- 4. Stage assignment and timeline: pipeline stages, status history, board with drag and drop, interviews and follow-ups
- 5. Dashboard, search and success rate: stats, charts, URL-based search, filters and sorting, pagination, CSV export
- 6. Real-time updates: optimistic UI, Server-Sent Events, tab sync, concurrency control
- 7. Custom workflows and reminders: user-defined pipelines, templates, automation rules, reminder emails
- 8. Collaboration: teams, roles, comments, share links, audit log
After the eight, in this order:
- Resources (guides and templates): the best SEO payoff, since public useful pages are what rank
- Optimize (resume tailoring, search analytics, market insights)
- Enterprise (university and agency dashboards, reusing the collaboration work)
Commits follow Conventional Commits:
type(scope): short description
Types in use: feat, fix, refactor, perf, style, chore, test, docs.
Examples:
feat(actions): add createApplication server action
fix(forms): keep typed values when validation fails
perf(cache): tag application queries and revalidate on mutation
Commits should be real units of work. If a step is too small for its own commit, merge it into the next one.
- Email verification, two-factor sign-in and a list of active sessions are not built yet.
- Live updates are at-most-once and limited by serverless connection time (see Live updates).
- The in-memory event channel only works on a single server instance. Set
DIRECT_URLin production.