Inspiration
Freelance work breaks down in three places—unclear briefs, disputed deliverables, and payments that never come. We've both been on at least one side of this. A client changes scope mid-project and calls it "just a small tweak." A freelancer delivers something technically correct but creatively off because the brief was two sentences long. An invoice sits unpaid for 60 days because there's no agreement anyone actually signed. The root cause is always the same: there is no shared source of truth between the two parties—something both sides have read, agreed to, and can point back to when things go sideways. Banks solved this for loans. Lawyers solved this for large contracts. Nobody has solved it for the everyday creative engagement—the logo, the website, the video, the campaign. That gap is what PactFlow fills.
What it does
PactFlow is a freelance collaboration platform that takes an engagement from brief to payment in one structured, transparent flow:
Brief → Quotation → Agreement → Milestones → Deliverables → Payment
Clients post a detailed brief describing what they need, their budget, and their timeline.
Freelancers receive an AI-drafted quotation pre-filled with the brief—scope, line items, timeline—which they refine and submit.
Both sides negotiate and sign a legally structured agreement with explicit scope boundaries, a milestone schedule, and payment terms.
Work is delivered milestone by milestone; each deliverable is reviewed against the agreed scope using AI-powered scope classification—flagging anything INCLUDED, OUTSIDE_SCOPE, or AMBIGUOUS
Payments are held in escrow via PayPal and released only when a milestone is approved—no chasing invoices.
Freelancers receive their share instantly via PayPal payouts. the moment a milestone is captured
A full audit trail and change-request system means scope changes are proposed, voted on, and recorded—never silently absorbed
How we built it
PactFlow is a full-stack Next.js 15 application built on the App Router with React 19 server components for data fetching and client components only where interaction demands it.
Auth & Security
Auth.js (NextAuth v5) with JWT sessions, bcrypt password hashing, rate-limited login with account lockout, server-side session revocation, and CSRF protection.
Database
Prisma ORM over SQLite locally and PostgreSQL in production on Render. The schema encodes the full engagement lifecycle as state machines—transitions are enforced at the service layer, never just the UI.
$$\text{DISCOVERY} \rightarrow \text{REQUIREMENTS} \rightarrow \text{REQUIREMENTS_APPROVED} \rightarrow \text{AGREEMENT} \rightarrow \text{ACTIVE} \rightarrow \text{COMPLETED}$$
Payments
PayPal Orders v2 API for milestone escrow with idempotent order creation and capture-on-approval. PayPal Payouts API for freelancer disbursement. Full webhook verification with HMAC signature checks. A mock PayPal driver lets the entire flow run locally without a sandbox account.
AI
An OpenAI-compatible LLM pipeline generates structured JSON quotation drafts, classifies deliverable scope against the signed agreement, summarizes brief comparisons, and analyzes collaboration history. Every output is a recommendation—the user always has the final word. A deterministic heuristic engine runs as a full fallback when no API key is configured, so the demo works out of the box.
Everything else
pdf-lib for exportable agreements and statements of work, Bryntum Gantt for milestone timelines, AG Grid for payment and deliverable views, AWS S3-compatible presigned URLs for file delivery, Tailwind CSS v4, Zod for end-to-end validation, Vitest + fast-check for property-based testing, and Playwright for E2E coverage.
Challenges we ran into
Auth.js v5 behind Render's reverse proxy
This cost us hours. Render's internal routing means the base URL Auth.js detects at runtime is the internal bind address (http://localhost:PORT), not the public HTTPS domain. The default redirect callback was sending browsers to https://localhost:3000/login after sign-out—unreachable. We patched authRedirectUrl to strip every absolute target to its path component.
Then a second bug: next-auth/react's signIn({ redirect: false }) calls new URL(data.url) to extract error params. When the redirect callback returns a relative path like /briefs, that constructor throws TypeError: Invalid URL. The session cookie was already written—so reloading showed the user as logged in—but the UI showed "Unable to sign in right now." Fixed by returning a fully qualified absolute URL from authRedirectUrl when APP_URL is set.
Building a real payment escrow flow
PayPal's Orders v2 API has sharp edges—idempotency keys, order state transitions, capture webhooks, and payout eligibility windows. Getting the capture → payout sequence right, where the client's payment lands in the platform account and the freelancer's share is disbursed, required careful sequencing, retry logic, and webhook replay protection.
Reliable structured output from the LLM
Classifying deliverable scope requires the AI to reason over a legal document and an ambiguous real-world description and return structured JSON—not prose. Getting consistent, schema-valid output required careful prompt engineering, Zod schema validation on every response, timeout handling, and a heuristic fallback that never leaves the user stranded.
Keeping the demo zero-dependency
Judges shouldn't need an OpenAI key, an S3 bucket, or a PayPal sandbox to evaluate PactFlow. Everything has a local fallback: LLM → heuristic engine, storage → local disk, PayPal → mock driver. The seed script creates three demo accounts with a live engagement already in progress.
Accomplishments that we're proud of
A complete end-to-end flow—brief to paid—with no step missing or mocked out in production
AI that degrades gracefully—the heuristic engine produces sensible quotations even without an LLM key, and every AI output is capped, validated, and stored as a draft, never auto-submitted.
A real escrow model—not a "payment button" but a full capture-on-approval, payout-on-capture pipeline built on PayPal's production APIs
State machines at the data layer—every business rule is enforced in the service layer with full audit logging, not just hidden behind a UI button.
Zero external dependencies in demo mode—the entire platform runs locally with npm run dev and npm run db:seed
What we learned
Auth.js v5 is powerful, but beta is beta. Read the source. The docs lag behind the implementation, and proxy deployments expose bugs that localhost never surfaces. State machines belong in the service layer, not the UI. Every time we were tempted to encode a business rule in a React component, it came back to haunt us. Guard your transitions at the data layer. AI as a co-pilot, not a decision-maker. Every AI output in PactFlow is a recommendation—the user always has the last word. This made the product feel trustworthy rather than magic. A good fallback is a feature, not a compromise. The heuristic engine is a guarantee that the product works for every judge, every demo, every time—without depending on an external service being available.
What's next for PactFlow
Real-time notifications via WebSockets or SSE so both parties see status changes the moment they happen
Template library—pre-built brief and agreement templates for common engagement types (branding, development, copywriting, photography)
Dispute resolution flow—a structured mediation process when a scope classification comes back AMBIGUOUS and both sides disagree
Mobile app—the core review-and-approve workflow is perfect for a native mobile experience; freelancers rarely sit at a desk when a client requests a revision.
Multi-currency support—PayPal supports it; PactFlow just needs the currency selector and conversion display.
Platform reputation layer—verified completion history, on-time delivery rate, and scope adherence score for both clients and freelancers
Built With
- ag
- api
- auth.js
- bcrypt
- bryntum
- gantt
- grid
- jwt
- next.js
- node.js
- openai
- orders
- payouts
- paypal
- pdf-lib
- playwright
- postgresql
- prisma
- react
- render
- tailwind
- typescript
- vitest
- zod