Skip to content

rcadavos/applicant-tracker

Repository files navigation

Applicant Tracker

A recruiter-facing Applicant Tracking Dashboard built with Refine + Next.js against a real Appwrite REST backend. Recruiters manage applicants through a hiring pipeline, schedule interviews, and see pipeline stats at a glance.

🔗 Live demo: https://applicant-tracker-eta.vercel.app

Create an account at /register (pick a role) to try it — or run it locally with the steps below.

Internal recruiter tool — staff sign in with one of three roles (admin / recruiter / interviewer). There is no jobseeker/applicant login; applicants are records that recruiters manage.


Features

  • Applicants — full CRUD: list with server-side search, status filter, sorting and pagination; create / edit with validation; detail page with related interviews; delete with confirmation.
  • Interviews — linked to an applicant; schedule from the applicant page (prefilled) or globally; per-applicant list + global list; edit / detail / delete.
  • Dashboard — total applicants, applicants per status, recent applicants, and upcoming interviews.
  • Access control — lightweight role matrix (admin / recruiter / interviewer) via Refine's accessControlProvider; unauthorized actions are hidden and pages are guarded.
  • States everywhere — loading skeletons, error states, and success/error toast notifications on every mutation.
  • Accessible & dark — Linear-inspired dark UI (see DESIGN.md), keyboard navigation, visible focus rings, labelled inputs, skip link, WCAG-minded.

Tech stack

Concern Choice
Framework Next.js 16 (App Router, RSC), React 19, TypeScript strict
Data framework Refine v5 (@refinedev/core, nextjs-router, appwrite)
Backend Appwrite Cloud (REST) via the Refine Appwrite data provider
UI Tailwind CSS v4 + hand-authored shadcn/ui (Radix primitives), lucide
Forms React Hook Form + Zod v4
Notifications sonner
Tests Vitest + Testing Library + vitest-axe; Playwright + @axe-core/playwright

Getting started

1. Prerequisites

  • Node.js 20+ (developed on Node 24)
  • An Appwrite project (Cloud or self-hosted)

2. Install

npm install

3. Configure environment

cp .env.local.example .env.local
Variable Required Notes
NEXT_PUBLIC_APPWRITE_ENDPOINT yes e.g. https://sgp.cloud.appwrite.io/v1 (your region)
NEXT_PUBLIC_APPWRITE_PROJECT_ID yes Appwrite project id
NEXT_PUBLIC_APPWRITE_DATABASE_ID yes Database id holding the collections
APPWRITE_API_KEY provisioning only Server-only key used by npm run provision (Databases read+write). Never prefix NEXT_PUBLIC_.

Only the NEXT_PUBLIC_* values reach the browser (safe by Appwrite's design).

Ready-to-use values for this evaluation's backend — paste into .env.local. The NEXT_PUBLIC_* values are publishable (the project id/endpoint ship to the browser by design), so they're shared here to run against the existing project:

NEXT_PUBLIC_APPWRITE_ENDPOINT=https://sgp.cloud.appwrite.io/v1
NEXT_PUBLIC_APPWRITE_PROJECT_ID=6a296875000740032c7d
NEXT_PUBLIC_APPWRITE_DATABASE_ID=6a2a346200126595dd4c

# Server-only key (Databases read+write scope) — used only by `npm run provision`.
APPWRITE_API_KEY=standard_64d40d8a53a2f47f4617900ff6eadad14b93783e11e4dd097b1b7c76a3d9a26063c55b35994a030d7b0fc52a91785ec01569bedf00b30ee139dda5f8efaa275fc71f4006f017802fda02c1af0c28394c965d3577d64c09714ebde3b0795b084256f0eae067a03d6719be4d0314520585a6a5c26e18162512e4600f438dbf1c03

The app itself (login, CRUD, dashboard) runs with just the three NEXT_PUBLIC_* values — APPWRITE_API_KEY is only for the provisioning script. To point at your own project instead, swap the project/database ids and add a Web platform for localhost in the Appwrite console.

The API key above is included for the reviewer's convenience (private repo). If this repo is ever made public, revoke/rotate that key in the Appwrite console — it grants Databases read+write to the project.

4. Provision the backend

Add an API key (Databases read+write scope) to .env.local as APPWRITE_API_KEY, then:

npm run provision

This creates the applicants, interviews and profiles collections (with attributes and indexes) in your database via the REST API. It's idempotent — safe to re-run. Schema reference: appwrite.json (and docs/appwrite-setup.md).

Also add a Web platform for localhost in the Appwrite console so the browser SDK is allowed. You can delete the API key after provisioning.

5. Run

npm run dev

Open http://localhost:3000, go to /register, create a user and pick a role.

Scripts

Script Purpose
npm run dev Dev server
npm run build Production build
npm run lint ESLint (incl. jsx-a11y)
npm run typecheck tsc --noEmit
npm test Vitest component + a11y tests
npm run test:e2e Playwright end-to-end (+ axe)
npm run provision Create the Appwrite schema from REST

Architecture

src/
  app/
    (auth)/        login, register            — public auth pages
    (app)/         dashboard, applicants, interviews
                   — guarded by <Authenticated>, wrapped in the app shell
  components/
    ui/            shadcn primitives (single source of truth)
    applicants/ interviews/ dashboard/ data-table/ layout/ common/ forms/
  lib/
    appwrite/      one shared Appwrite client (all SDK access goes here)
    auth/          session + role cache
    validation/    Zod schemas + form↔payload transforms
    constants.ts   roles, statuses, resource ids, routes
    format.ts      date/number formatters
  providers/       Refine: data, live, auth, accessControl, notification
  types/           Applicant / Interview document shapes
appwrite.json      schema-as-code (repo root)
scripts/provision-appwrite.mjs

Separation of concerns

  • One Appwrite layer (src/lib/appwrite) — the only place the SDK is constructed; the data provider and auth provider share that single client.
  • Data access via Refine hooksuseTable, useList, useOne, useCreate, useUpdate, useDelete. Server state lives in Refine/React Query; there's no duplicated local copy of server data.
  • Validation is shared — the same Zod schema validates a form; thin to…Payload / …ToFormValues transforms convert between the form's string shape and the Appwrite document shape.
  • Resources configured once in the Refine provider; routes under src/app mirror them.

API integration approach

The app talks to Appwrite through the official Refine Appwrite data provider, which implements Refine's DataProvider contract (getList, getOne, create, update, deleteOne, …) over Appwrite's REST API. Resource names map 1:1 to Appwrite collection ids, so the CRUD contract is:

REST-style operation Refine call Appwrite collection
GET /applicants (list) useTable/useList applicants.listDocuments
GET /applicants/:id useOne applicants.getDocument
POST /applicants useCreate applicants.createDocument
PUT /applicants/:id useUpdate applicants.updateDocument
DELETE /applicants/:id useDelete applicants.deleteDocument
GET /interviews?applicantId=… useList + filter interviews.listDocuments
  • Pagination / filtering / sorting are server-side — Refine's currentPage/pageSize, CrudFilters and CrudSorters translate to Appwrite Query.limit/offset/equal/search/orderAsc|Desc. Search uses fulltext indexes on fullName / email.
  • Notifications come from the data layer automatically — a notificationProvider maps Refine's success/error events onto sonner toasts.
  • Auth uses an authProvider over Appwrite Account (email/password sessions); roles live in a profiles collection and drive the accessControlProvider.

Access control

Action admin recruiter interviewer
View applicants/interviews
Create/edit/delete applicants
Create/edit interviews
Delete interviews

Enforced in the UI via <CanAccess> (buttons hidden) and page-level guards. Pick a role at registration; change it later by editing the user's profiles document.

Challenges encountered

  • Appwrite SDK / provider version drift@refinedev/appwrite@8 bundles the Appwrite v15 SDK; the standalone SDK had resolved to v26 (incompatible client shape). Fixed by pinning appwrite@^15 so one SDK version is shared.
  • Appwrite 1.9 "TablesDB" migration — the CLI's push collections is deprecated and push tables performed a destructive resync that deleted the database and stripped the local schema. Resolved by provisioning over the REST API with a small idempotent script (npm run provision) instead of the CLI — deterministic and free of the CLI's sync surprises.
  • Regional Cloud + custom ids — the project lives in the SGP region and the database must use a fixed id; mismatched console-created ids caused failures. The provision script targets the configured endpoint/id directly.
  • Form ↔ document shape — inputs are strings while Appwrite wants numbers/arrays/ISO datetimes. Kept the Zod schema as the input shape and put conversion in explicit to…Payload/…ToFormValues helpers to keep types clean.

Design

DESIGN.md is the source of truth for the UI — a Linear-inspired dark system (near-black canvas, lavender accent, hairline borders). Tokens are mapped into the Tailwind theme in src/app/globals.css.

About

A centralized applicant management system that helps recruiters organize candidate profiles, track application progress, schedule interviews, and manage hiring decisions from one secure platform.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors