A role-based freelance and job listing marketplace. Clients post jobs, freelancers pitch with proposals, and payment runs through Stripe Checkout, all managed from one dashboard.
- WorkMatch
workmatch_demo.webm
WorkMatch is the frontend of a full-stack freelance marketplace. It covers the complete hiring loop:
- A client posts a job with a budget, deadline, required skills and experience level.
- Freelancers browse open jobs and submit proposals with a price, timeline and approach.
- The client reviews proposals and accepts one, which redirects them to Stripe Checkout.
- Once payment clears, a contract goes live. When the work is delivered, the contract is marked complete.
- An admin oversees the platform: user management, platform stats and a full audit trail.
The app talks to a separate REST API (workmatch-backend) built with TypeScript, Express, Prisma and PostgreSQL. Every workflow in the UI is backed by that API; there is no mock data.
flowchart LR
C[Client posts job] --> F[Freelancer submits proposal]
F --> R[Client reviews proposals]
R --> A[Client accepts proposal]
A --> S[Stripe Checkout]
S --> K[Contract goes ACTIVE]
K --> M[Contract marked COMPLETE]
| Resource | URL |
|---|---|
| Frontend (live) | https://workmatch.dibbockb.com |
| Frontend repository | https://github.com/dibbockb/workmatch |
| Backend API (live) | https://api.workmatch.dibbockb.com |
| Backend repository | https://github.com/dibbockb/workmatch-backend |
| API documentation | https://documenter.getpostman.com/view/54867039/2sBYHQ2NuZ |
The login page has one-click Demo Login buttons for each role. No sign-up needed. If you prefer typing, the credentials are:
| Role | Password | |
|---|---|---|
| Admin | admin@workmatch.com |
!123QWEe |
| Client | client@workmatch.com |
!123QWEe |
| Freelancer | freelancer@workmatch.com |
!123QWEe |
These accounts exist for demonstration only and hold no real data.
Suggested walkthrough
- Sign in as Client, open Post a job, and publish a job.
- Sign out, sign in as Freelancer, open Find work, and submit a proposal on that job.
- Back as Client, open the job, review proposals and accept one. You'll be sent to Stripe Checkout.
- Pay with a Stripe test card (for example
4242 4242 4242 4242, any future expiry, any CVC). - Sign in as Admin and check Audit logs to see the payment and contract events recorded.
- Dashboard overview with job and contract status breakdowns
- Post, edit, close and delete jobs, with validated forms (title, description, skills, budget range, deadline, duration, experience level)
- Review proposals per job, then accept or reject
- Stripe Checkout redirect on accept, with success and cancel return pages
- Contracts list with search and status filters, and a mark-complete action
- Dashboard overview with proposal and contract statistics
- Browse and search open jobs with server-side pagination
- Job detail page with a proposal form (price, timeline, approach)
- Track proposals by status, withdraw pending ones, and see counter-offer history
- Contracts list showing counterpart, agreed price and payment breakdown
- Platform overview: total users, jobs, contracts and platform commission revenue
- Role and job-status distribution charts
- User management with role and text filters, plus block and unblock (admins are protected from being blocked)
- Audit log viewer with category filters (contracts, payments, counter offers, users) and search
- Role-based access control enforced at two layers: an edge proxy that verifies the JWT, and a client-side guard in each dashboard shell
- Silent session refresh with a single-flight refresh lock, so concurrent 401s trigger exactly one refresh request
- Shareable URL state for search and filters (for example
?category=payment&search=stripe) - Runtime-validated API responses with Zod schemas
- Light and dark themes, reduced-motion support, ARIA roles on interactive filters
- Toast notifications for mutations and API failures
| Layer | Technology |
|---|---|
| Framework | Next.js 16 (App Router), React 19 with React Compiler |
| Language | TypeScript 5 |
| Styling | Tailwind CSS 4, shadcn/ui on Base UI, class-variance-authority, tailwind-merge |
| Server state | TanStack Query 5 |
| Forms | TanStack Form |
| Validation | Zod 4 |
| HTTP | ofetch with cookie credentials |
| Auth | JWT in HTTP cookies, verified at the edge with jose |
| Payments | Stripe Checkout (session created by the backend) |
| Animation | Motion |
| Icons | Phosphor Icons, Lucide |
| Notifications | Sonner |
| Theming | next-themes |
| Tooling | Bun, Biome |
| Hosting | Vercel |
src/
├── app/ # Next.js App Router
│ ├── (auth)/ # login, signup
│ ├── (site)/ # landing, about, contact
│ ├── dashboard/
│ │ ├── admin/ # overview, users, audit-logs
│ │ ├── client/ # overview, jobs (list, detail, new), proposals, contracts
│ │ └── freelancer/ # overview, jobs (list, detail), proposals, contracts
│ ├── payment/ # success, cancel
│ ├── layout.tsx # root layout, providers, toaster
│ ├── loading.tsx
│ └── not-found.tsx
├── features/ # domain modules (one folder per resource)
│ ├── auth/ # api.ts · queries.ts · schemas.ts
│ ├── jobs/ # api, queries, schemas, list/detail/form components
│ ├── proposals/
│ ├── contracts/
│ └── admin/
├── components/
│ ├── ui/ # shadcn/ui primitives
│ ├── dashboard/ # dashboard shell and per-role navigation config
│ ├── landing/ # navbar, user menu, site chrome
│ ├── motion/ # animated sidebar, loader, layout transitions
│ └── shared/ # page header, backdrop, not-found actions
├── lib/
│ ├── api-client.ts # ofetch wrapper with single-flight token refresh
│ ├── env.ts # Zod-validated public env
│ ├── roles.ts # role → home route map
│ └── demo-accounts.ts # one-click demo credentials
├── providers/ # React Query provider
└── proxy.ts # edge route protection (JWT verification)
Each resource follows the same three-file split, so data access stays predictable and testable:
| File | Responsibility |
|---|---|
api.ts |
Plain async functions that call the backend and parse responses with Zod |
queries.ts |
TanStack Query hooks (useQuery / useMutation) and cache invalidation |
schemas.ts |
Zod schemas and inferred TypeScript types for requests and responses |
sequenceDiagram
participant B as Browser
participant P as proxy.ts (edge)
participant N as Next.js page
participant A as Backend API
B->>A: POST /v1/auth/login
A-->>B: Set-Cookie accessToken + refreshToken
B->>P: GET /dashboard/client
P->>P: jwtVerify(accessToken) → role
alt role matches section
P-->>N: continue
N->>A: GET /v1/auth/me
else wrong role
P-->>B: redirect to that role's dashboard
else no valid token
P-->>B: redirect to /login?next=…
end
Note over B,A: On any 401, api-client calls POST /v1/auth/refresh-token once,<br/>queues concurrent requests behind it, then replays them.
Two layers of protection:
src/proxy.tsverifies the access token (HS256) and redirects by role. Signed-in users are bounced away from/loginand/signup, and a visitor to another role's section is sent to their own home.DashboardShellre-checks the current user via/v1/auth/meand redirects on mismatch. The API remains the source of truth for authorization.
The next redirect parameter is sanitized against open-redirect tricks (//, /\).
sequenceDiagram
participant C as Client (browser)
participant F as WorkMatch frontend
participant A as Backend API
participant S as Stripe
C->>F: Accept proposal
F->>A: POST /v1/contracts/accept-proposal
A->>S: Create Checkout Session
A-->>F: { checkoutUrl }
F->>F: Validate URL is https
F-->>C: Redirect to Stripe Checkout
C->>S: Pay
S-->>C: Redirect to /payment/success?session_id=…
S->>A: Webhook (payment event)
A->>A: Activate contract, write audit log
API responses are parsed with Zod before reaching components. A backend contract change fails loudly at the boundary instead of rendering undefined deep in the UI.
| Route | Access | Description |
|---|---|---|
/ |
Public | Landing page |
/about |
Public | About WorkMatch |
/contact |
Public | Contact page |
/login |
Guests | Sign in, with one-click demo login |
/signup |
Guests | Register as Client or Freelancer |
/dashboard |
Authenticated | Redirects to the role's home |
/dashboard/client |
Client | Overview |
/dashboard/client/jobs |
Client | My jobs |
/dashboard/client/jobs/new |
Client | Post a job |
/dashboard/client/jobs/[jobId] |
Client | Job detail, edit, close, delete, review proposals |
/dashboard/client/proposals |
Client | All proposals received |
/dashboard/client/contracts |
Client | Contracts |
/dashboard/freelancer |
Freelancer | Overview |
/dashboard/freelancer/jobs |
Freelancer | Browse and search jobs |
/dashboard/freelancer/jobs/[jobId] |
Freelancer | Job detail and proposal form |
/dashboard/freelancer/proposals |
Freelancer | My proposals |
/dashboard/freelancer/contracts |
Freelancer | Contracts |
/dashboard/admin |
Admin | Platform overview and charts |
/dashboard/admin/users |
Admin | User management |
/dashboard/admin/audit-logs |
Admin | Audit trail |
/payment/success |
Authenticated | Stripe success return page |
/payment/cancel |
Authenticated | Stripe cancel return page |
- Bun 1.4+ (recommended, it is the project's package manager), or Node.js 20.9+ with npm/pnpm
- A running instance of the WorkMatch backend, or use the hosted API at
https://api.workmatch.dibbockb.com
git clone https://github.com/dibbockb/workmatch.git
cd workmatch
bun installCreate .env.local in the project root:
# Base URL of the backend API (no trailing slash)
NEXT_PUBLIC_SERVER_URL=https://api.workmatch.dibbockb.com
# Must be identical to the backend's JWT access-token secret.
# Used by src/proxy.ts to verify tokens at the edge.
JWT_ACCESS_SECRET=replace-with-the-backend-secretUsing the hosted API? You won't know the production
JWT_ACCESS_SECRET, and the edge proxy refuses to start without one. Run the backend locally instead (see its README) so both sides share a secret you control.
bun devOpen http://localhost:3000.
- Clone and start
workmatch-backendfollowing its README, with a Stripe test-mode key and webhook secret. - Point
NEXT_PUBLIC_SERVER_URLat it (for examplehttp://localhost:5000) and set the sameJWT_ACCESS_SECRETin both projects. - Seed or create the three demo accounts, or register new Client and Freelancer users from
/signup.
| Variable | Required | Scope | Description |
|---|---|---|---|
NEXT_PUBLIC_SERVER_URL |
Yes | Browser + server | Backend base URL. Validated at startup by src/lib/env.ts (Zod). |
JWT_ACCESS_SECRET |
Yes | Server (edge proxy) | HS256 secret used to verify the accessToken cookie. The app throws on boot if missing. |
SERVER_URL |
No | Server | If set, next.config.ts rewrites /api/:path* to ${SERVER_URL}/:path*, enabling a same-origin proxy to the API. |
Environment files are git-ignored (.env*). Never commit real secrets.
| Command | Description |
|---|---|
bun dev |
Start the dev server |
bun run build |
Create a production build |
bun start |
Serve the production build |
bun run lint |
Lint and check with Biome |
bun run format |
Format the codebase with Biome |
The frontend is deployed on Vercel.
- Import the repository in Vercel.
- Add
NEXT_PUBLIC_SERVER_URLandJWT_ACCESS_SECRETunder Project Settings → Environment Variables. - Deploy. Vercel detects Next.js and uses
bunfrom thepackageManagerfield.
Cookie and domain requirements
Authentication relies on cookies set by the API. For the edge proxy on the frontend origin to see the accessToken, the cookie must be visible to the frontend host. In practice that means one of:
- Scope the cookie
Domainto the shared parent domain (for example.dibbockb.com), withSecureand an appropriateSameSitevalue, or - Serve the API on the same origin as the frontend (the
/apirewrite supported bySERVER_URLis the first step in that direction).
The production setup uses workmatch.dibbockb.com (frontend) and api.workmatch.dibbockb.com (API).
Honest notes about the current state of the project:
- Payment success page is not server-verified.
/payment/successreads thesession_idquery param and shows a confirmation without asking the backend to confirm the session. The contract is activated by the backend, but the page itself does not prove it. Fixing this is the top roadmap item. - Escrow is simplified. Funds flow through Stripe Checkout and are tracked per contract, but the release and refund logic is not a full milestone-based escrow yet.
- Cross-origin cookies. Frontend and API run on different subdomains, which makes cookie configuration (
Domain,SameSite,Secure) delicate. - Client-side filtering for some lists. The users and audit-log pages fetch the full list and filter in the browser. Fine at demo scale, not at production scale.
- Admin analytics are shallow. Charts show status and role distributions only, with no time series yet.
- Landing page statistics are illustrative. Numbers and testimonials on the marketing page are sample copy, not live platform metrics.
- Contact form is not wired up to a backend endpoint yet.
- No automated tests yet.
Planned next
- Redesign the payment escrow flow in the backend
- Verify payment success with a backend endpoint
- Move the backend behind the same domain as the frontend to eliminate CORS issues
Also on the list
- Profile and settings pages for every role
- Freelancer earnings and client payment history pages
-
error.tsxandglobal-error.tsxboundaries, plus per-routeloading.tsxskeletons - Multi-step job posting wizard
- Server-side pagination and filtering for admin users and audit logs
- Time-series admin analytics (revenue, signups, contracts over time)
- Working contact form
- Unit tests for the API client and Zod schemas, and end-to-end tests for the hiring flow
- Replace landing-page stats with real or clearly labelled content
Have an idea? Open an issue.
Contributions, issues and feature requests are welcome.
- Fork the repo and create a branch:
git checkout -b feat/your-feature - Make your changes and run
bun run lintandbun run build - Commit with a clear message, for example
feat(jobs): add salary filter - Push and open a Pull Request describing what changed and why
Please keep new data access in the features/<domain>/{api,queries,schemas}.ts pattern and validate API responses with Zod.
Distributed under the MIT License. See LICENSE for details.
Dibbo Chakraborty
- GitHub: @dibbockb
- Email: dibbo@dibbockb.com
Built as a full-stack portfolio project with a TypeScript, Express, Prisma and PostgreSQL backend and a Next.js frontend.
This Readme was written with AI assistance and human supervision.