Job board aggregator with GraphQL API, powered by Next.js 16 and Cloudflare D1.
- Next.js 16 - App Router with Node.js runtime
- Cloudflare D1 - Serverless SQLite database
- GraphQL - Apollo Server with type-safe resolvers
- Drizzle ORM - Type-safe database queries
- Trigger.dev - Background job processing
- Clerk - Authentication
pnpm installFor production-grade performance, deploy the D1 Gateway Worker:
# Generate API key
openssl rand -hex 32
# Deploy gateway
wrangler deploy --config wrangler.d1-gateway.toml
# Set API key secret
wrangler secret put API_KEY --config wrangler.d1-gateway.tomlFull instructions: DEPLOY_D1_GATEWAY.md
Copy .env.example to .env.local and add your credentials:
cp .env.example .env.localRequired for D1 Gateway mode:
D1_GATEWAY_URL=https://d1-gateway.your-subdomain.workers.dev
D1_GATEWAY_KEY=your-api-key-from-step-2Or for Direct API mode (dev only):
CLOUDFLARE_ACCOUNT_ID=your-account-id
CLOUDFLARE_D1_DATABASE_ID=632b9c57-8262-40bd-86c2-bc08beab713b
CLOUDFLARE_API_TOKEN=your-api-tokenpnpm devVisit:
- App: http://localhost:3000
- GraphQL Playground: http://localhost:3000/api/graphql
Production (Recommended):
Next.js on Vercel → D1 Gateway Worker → D1 Database (binding)
Benefits:
- ✅ 10-100x faster than REST API
- ✅ No Cloudflare API rate limits
- ✅ Batching support (multiple queries in one request)
- ✅ Built-in caching with
s-maxage
Development (Alternative):
Next.js → Cloudflare REST API → D1 Database
We use runtime = "nodejs" for API routes because:
- Vercel recommends Node.js over Edge for reliability + performance
- Node.js gets fluid compute optimizations (important for I/O operations)
- Full access to Node.js APIs and packages
- Better for calling external APIs like the D1 Gateway
The dominant cost is the network hop to Cloudflare, not the runtime.
Purpose-built resolvers with proper batching:
query GetJobs {
jobs(limit: 20, status: "active") {
jobs {
id
title
company_key
location
url
}
totalCount
}
}The D1 Gateway supports batching to reduce round trips:
// ❌ Slow: 3 separate requests
const jobs = await fetch(`${GATEWAY}/jobs`);
const count = await fetch(`${GATEWAY}/jobs/count`);
const companies = await fetch(`${GATEWAY}/companies`);
// ✅ Fast: 1 batched request
const { total, jobs, company_total } = await gateway.jobs.batch({
status: "active",
company_key: "stripe",
limit: 20,
});- Jobs list: 5 sec fresh, 60 sec stale-while-revalidate
- Job detail: 10 sec fresh, 120 sec stale-while-revalidate
- Companies: 30 sec fresh, 5 min stale-while-revalidate
- User settings: 30 sec private cache
See migrations/schema.ts for the full schema.
Key tables:
jobs- Job postings from various ATS platformscompanies- Company profiles with scoringuser_settings- User preferences and filtersjob_skill_tags- Skill extraction results
# Database migrations
wrangler d1 migrations apply DB --remote
# Scrape jobs (local)
pnpm tsx scripts/ingest-jobs.ts
# Enhance job with ATS data
pnpm tsx scripts/enhance-specific-job.ts <job-id>
# Extract skills from jobs
pnpm tsx scripts/extract-job-skills.ts
# Classify remote EU jobs
pnpm tsx scripts/classify-remote-eu-jobs.tsvercel deployMake sure to add environment variables in Vercel dashboard:
D1_GATEWAY_URLD1_GATEWAY_KEYNEXT_PUBLIC_CLERK_PUBLISHABLE_KEYCLERK_SECRET_KEY- etc. (see
.env.example)
wrangler deploy --config wrangler.d1-gateway.tomlSee DEPLOY_D1_GATEWAY.md for details.
- Use batching for multiple related queries
- Leverage caching - GET endpoints have aggressive
s-maxage - Select only needed columns - Gateway uses
.raw()for compact payloads - Add indexes for WHERE + ORDER BY queries
See DEPLOY_D1_GATEWAY.md for optimization details.
wrangler tail --config wrangler.d1-gateway.tomlcurl https://d1-gateway.your-subdomain.workers.dev/health \
-H "Authorization: Bearer YOUR_API_KEY"Check the console for detailed error messages:
❌ [GraphQL] Error in context setup: Missing D1 configuration
Make sure environment variables are set in .env.local.
- Add indexes for frequently queried columns
- Use batching to reduce round trips
- Check Wrangler logs for query execution time
- Verify
D1_GATEWAY_KEYmatches the Worker'sAPI_KEYsecret - Ensure
Authorization: Bearer ...header is set
- Create a feature branch
- Make your changes
- Test locally with
pnpm dev - Submit a PR
MIT