Skip to content

Repository files navigation

JobFilter

CURRENT PUBLIC OPPORTUNITIES. CLEAR FIT. CLEAR NEXT ACTION.

JobFilter is a qualification layer for small UK construction and maintenance firms. The current /find-jobs procurement path uses Find a Tender OCDS releases and ranks public notices by trade, location evidence, stage, deadline, and buyer context. It does not sell exclusive access or promise an award.

Live product · Test scenarios

JobFilter desktop homepage

Current status

  • Live scans may return no result when no verified opportunity matches the trade and patch.
  • Internal sample records are blocked from production, even if a runtime toggle is set incorrectly.
  • Planning, energy, company, paid checkout, and WhatsApp coverage depend on provider credentials and activation checks; they are not treated as live merely because code exists.
  • /test, /test/intake, /dev-portal, and /api/status are development-only surfaces and return 404 in production.

Source readiness

Readiness Sources Customer use
Live/current Find a Tender (FTS) OCDS release packages Primary current-notice scan
Legacy/backfill Contracts Finder Pre-February-2025 history/transition only; excluded from current scans
Experimental Planning Data, Public Contracts Scotland, Sell2Wales and other unproven adapters Disabled by default and absent from primary promises
Credential-required EPC, Companies House, Stripe and WhatsApp Only available after credentials, configuration and live verification

What this repository demonstrates

  • A Next.js production application with public opportunity ingestion, normalisation, deduplication, scoring, and free/paid response shaping.
  • Server-side redaction that keeps buyer, deadline, exact value, action route, and exact scoring depth out of free responses.
  • Source health and partial-failure handling: one failed provider does not invent a fallback job or crash the scan.
  • Supabase account/data plumbing, Stripe checkout/webhook routes, and Meta WhatsApp delivery code with explicit configuration failures.
  • Release gates for dependency security, type safety, lead-quality rules, postcode handling, free-tier privacy, production-only route protection, and a full production build.

Architecture

Official sources
  -> fetchers
  -> normalise
  -> deduplicate
  -> score and rank
  -> redact by access tier
  -> dashboard / delivery

Key areas:

  • leadEngine/ — source registry, fetchers, postcode logic, normalisation, scoring, quality audit, and scan orchestration.
  • server/routes/ — Express-compatible APIs mounted through the Next.js catch-all API route.
  • app/api/ — native Next.js handlers for account, alerts, payments, waitlist, and delivery workflows.
  • src/pages/ and src/components/ — product UI, scanner, account surfaces, trust pages, and trade landing pages.
  • supabase/migrations/ — versioned database changes and row-level access policies.
  • tests/regression/ — runnable product, privacy, quality, and runtime checks.

Known limitations

  • Source volume and trade/location fit vary with what buyers publish; an empty scan is a valid outcome.
  • EPC, Companies House, Stripe pricing, and WhatsApp delivery require provider credentials and live environment verification.
  • Public tenders may be pursued by other suppliers; territory routing must never be presented as exclusive notice access.
  • The current route surface is larger than the intended flagship journey and still needs product simplification.

Environment Variables

No API key is required for the live Find a Tender search.

Optional:

PORT=3000
NODE_ENV=development
NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_ANON_KEY=
SUPABASE_SERVICE_ROLE_KEY=

Production data is moving to Supabase:

Run supabase/migrations/20260522_vercel_supabase_saas.sql
Keep SUPABASE_SERVICE_ROLE_KEY server-only.

Install

npm install

Run Frontend + API Locally

npm run dev

Open:

http://localhost:3000
http://localhost:3000/find-jobs

Build + Test Commands

npm run lint
npm audit
npx tsx tests/regression/production-source-safety-regression.mjs
npx tsx tests/regression/postcode-filter-regression.mjs
node tests/regression/free-scanner-redaction-regression.mjs
npx tsx tests/regression/lead-engine-quality-regression.mjs
npm run build
node tests/regression/production-runtime-regression.mjs

API

Live lead scanner endpoint:

POST /api/leads/search

Request:

{
  "postcode": "B14 7QH",
  "trade": "electrical",
  "radiusMiles": 25
}

Response shape:

{
  "ok": true,
  "source": "lead_engine",
  "count": 0,
  "region": "West Midlands",
  "outward": "B14",
  "leads": [],
  "errors": []
}

Waitlist endpoint:

POST /api/waitlist
{
  "name": "A Tradesman",
  "trade": "Electrician",
  "contact": "name@example.com",
  "source": "site"
}

Expected pattern:

{ "ok": true }

Intake scoring endpoint:

POST /api/intake/score
{
  "jobType": "Plumbing",
  "urgency": "Emergency",
  "postcode": "B14 7QH",
  "phone": "07123456789",
  "details": "Leaking boiler",
  "hasPhotos": true
}

Curl Examples

Success query:

curl -s -X POST http://localhost:3000/api/leads/search \
  -H "Content-Type: application/json" \
  -d "{\"postcode\":\"B14 7QH\",\"trade\":\"electrical\",\"radiusMiles\":25}"

Expected pattern:

{
  "ok": true,
  "source": "lead_engine",
  "count": 1,
  "region": "West Midlands",
  "outward": "B14",
  "leads": [{ "source": "FTS" }],
  "errors": []
}

Empty result query:

curl -s -X POST http://localhost:3000/api/leads/search \
  -H "Content-Type: application/json" \
  -d "{\"postcode\":\"BT1 5GS\",\"trade\":\"roofing\",\"radiusMiles\":10}"

Expected pattern:

{
  "ok": true,
  "source": "lead_engine",
  "count": 0,
  "region": "Northern Ireland",
  "outward": "BT1",
  "leads": [],
  "errors": []
}

Invalid postcode query:

curl -s -X POST http://localhost:3000/api/leads/search \
  -H "Content-Type: application/json" \
  -d "{\"postcode\":\"BAD\",\"trade\":\"electrical\",\"radiusMiles\":25}"

Expected pattern:

{
  "ok": false,
  "source": "lead_engine",
  "count": 0,
  "region": "",
  "outward": "",
  "leads": [],
  "errors": ["valid UK postcode required"]
}

Waitlist:

curl -s -X POST http://localhost:3000/api/waitlist \
  -H "Content-Type: application/json" \
  -d "{\"name\":\"A Tradesman\",\"trade\":\"Electrician\",\"contact\":\"name@example.com\",\"source\":\"readme\"}"

Intake score:

curl -s -X POST http://localhost:3000/api/intake/score \
  -H "Content-Type: application/json" \
  -d "{\"jobType\":\"Plumbing\",\"urgency\":\"Emergency\",\"postcode\":\"B14 7QH\",\"phone\":\"07123456789\",\"details\":\"Leaking boiler\",\"hasPhotos\":true}"

Routes

/              Home
/find-jobs     Live scanner
/pricing       Pricing
/codex         Technical editorial page
/free-tools    Free quote, job, and diesel tools
/tips          Tips for tradesmen
/vantage       Bid/presentation advantage
/vicinity      Past-work marketing advantage
/privacy       Privacy policy
/terms         Terms
/health        Frontend health page
/api/health    API health JSON

Production Deploy

npm run lint
npm run test:fts
npm run build

Vercel requirements:

  • Framework preset: Next.js
  • Project root: JobFilterV1
  • Build command: npm run build
  • Environment variables from .env.example
  • Supabase migration applied before production data writes

Known Limitations

  • Find a Tender notices do not always include exact delivery postcodes, values, contact details, or eligibility evidence.
  • Radius filtering uses delivery-postcode centroids and fails closed when a notice does not provide enough delivery evidence.
  • Contracts Finder is retained only for documented legacy/backfill use and is not queried by the current scanner.
  • Planning, EPC and other adapters stay outside the primary promise until live coverage and mappings are proven.

About

UK public-procurement qualification for small construction and maintenance teams: official notices, fit scoring, and actionable bid workflow.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages