Developer-relations work, logged once and shown to the people paying for it.
A developer advocate logs each piece of work they ship — a post, a talk, a package, a demo. DevRel Studio turns that log into three things at once: a private workspace for the advocate, a live branded dashboard for every client, and a public portfolio of everything they have ever published. No exports, no slide decks, no month-end scramble.
It also runs a DevRel job board at devrel.studio/jobs: every developer-relations role pulled from company careers pages three times a day, with a tracker, alerts, CV matching and, for paying users, tailored applications built from the work already logged here.
Live at devrel.studio. See a real client dashboard at devrel.studio/demo.
- DevRel Studio
The single most useful thing to understand about this codebase is that it serves five different audiences on three different hosts, from one Next.js app. proxy.ts is what decides which.
| Surface | Address | Who opens it | Auth |
|---|---|---|---|
| Advocate workspace | devrel.studio/dashboard |
The DevRel who owns the account | Kinde session |
| Client dashboard | <slug>.devrel.studio, or the client's own domain |
The client being reported to — no account | Access code, or public |
| Public portfolio | devrel.studio/@handle |
Anyone; search engines; agents | None |
| Admin console | admin.devrel.studio |
Whoever operates the platform | Kinde session + admin role |
| Job board | devrel.studio/jobs |
Anyone; search engines; agents. The personal side lives in /dashboard/jobs |
None to browse; Kinde to save, track or tailor |
These are genuinely separate. An admin has no implicit access to anyone's client data. A client manager has no account at all. A portfolio is world-readable and deliberately excludes anything a client commissioned.
| Page | What it does |
|---|---|
| Overview | Live counters (published, in progress, views, downloads, attendees), a six-month published-vs-in-progress chart, month-by-month content with category and status filters, and a getting-started checklist |
| All Content | Every entry across every client: keyword search, category / status / platform / client filters, saved views, bulk actions, CSV import and export, keyboard shortcuts |
| Pipeline | The forward-looking view — what is in flight, due, or slipped, as lanes over the existing status values |
| Analytics | Who actually opened the work: visitors, sessions, bounce rate and dwell time across client dashboards and the public portfolio, with referrers, campaigns (utm_source and ref), entry pages, countries, cities, devices, recent sessions, and a live "reading now" count |
| Clients | Full CRUD for engagements — retainer rate with full rate history, contract type, pause/resume, access codes, logo upload (light and dark), brand colour, and a custom domain |
| Reports | Monthly client reports: written notes and targets, PDF export, per-client recipients, and a schedule (day, hour, timezone) that sends them automatically |
| Members | Invite teammates into a workspace by email with a role (Admin, Editor, Viewer); seat limits enforced by plan |
| Jobs | Discover with CV match, a kanban tracker, alerts, CV and preferences, Market (pay and hiring) and Jobs Pro — see Job board |
| Billing | Plan, access window, term pricing, upgrade paths, and the Jobs Pro pass |
| Settings | Profile, portfolio handle and bio, social links, account pause |
- Opened by a manager who has no account, behind an access code the advocate sets
- Live stats and a content table with category badges, platforms, metrics and direct links
- Expandable reshare history per entry
- Month and category filters
- Monthly reports with the advocate's written summary, plus a feedback box that emails back
- PDF download carrying the client's own logo and brand colour
- White-labelled: the client's logo (per theme), their colour, optionally their own domain
- Server-rendered and indexable, with a generated link-preview card
- Only
Publishedwork. Drafts, scheduled work, notes, tracking links and which client commissioned what never appear - Grouped by category with the metric each category is measured by
- Setting a handle is what publishes it — there is no separate on/off switch
Its own origin, so cookies, storage and any future edge rule stop at the boundary. Overview, accounts, workspaces, revenue and access windows, traffic, content, access requests, abuse signals, an audit log, and read-only impersonation for support.
The host is not the security boundary — every query behind these pages resolves the caller through requireAdmin server-side.
Free, public, and built to be crawled. Every page is static or ISR, so search engines and AI crawlers get real HTML. Filtering runs client-side against Convex.
- Sources. About 130 companies on Greenhouse, Ashby and Lever (
lib/jobs/seed.ts), plus aggregators that carry contract work: RemoteOK, We Work Remotely, the Hacker News "Who is hiring" and "Freelancer?" threads, with r/forhire supported but off by default (add aredditfeed and Reddit credentials to turn it on). Fetched at 06:00, 14:00 and 22:00 UTC. A role that disappears from its feed is marked expired, and deleted 30 days later. - Classification. Nine role families (seven core DevRel: advocacy, DevRel engineering, developer success and support, community, technical content and docs, education, programs; two adjacent: developer marketing and internal DX), nine levels, workplace, remote scope, and an eligibility engine that answers "is this open to me in Nigeria?". Pay is normalised to annual USD.
- Public pages.
/jobs, role hubs, companies, salaries, remote, contract and freelance, and one page per role withJobPostingJSON-LD. Apply links go through/jobs/out/[slug], which counts the click and addsutm_source=devrel.studio. Company logos load by domain with a monogram fallback. - Personal side —
/dashboard/jobs. Discover ranks roles against your CV. Tracker is a drag-and-drop board with stages, notes, next steps, and follow-up nudges. Alerts email new matches. CV and preferences takes a PDF or pasted text, read once for skills, focus and level. Market and Pro are described below. - Admin —
/admin/jobs. Feed health, sync now, add a feed, most-clicked roles, and Jobs Pro requests to grant. - Blog —
devrel.studio/blog,/admin/blog. Monday, Wednesday and Friday a cron (blog:draftNext) chooses a topic from Hacker News, dev.to, the job board's own data and a standing backlog, skipping anything already written. It researches with web search, writes in the house style (lib/blog/prompts.ts), runs style checks (lib/blog/draft.ts, which reuses the slop list from the CV kits), thenblog:factCheckchecks every claim against the web and rewrites what fails. The draft is emailed toBLOG_REVIEW_EMAILwith a link to/blog/review/<id>?t=<token>where it can be edited, published or rejected. Only a hash of the token is stored. Diagrams are SVG from the model and are sanitised twice, on publish and on render./admin/bloglists every post, starts a draft on demand and can email a fresh link. - Job analytics —
/admin/jobs/analytics. Visitors, sessions, searches (and the ones that found nothing), filters, the adjacent toggle, role views, apply clicks, saves, copied links, shares, CVs, tailored kits, alerts, Jobs Pro, every tour step, top roles and companies, referrers, countries and a live feed. The browser queues events and posts them to/api/jobs/track, which forwards them tojobAnalytics.ingestwithMANAGER_CODE_SECRET. Counters are kept for good (jobMetrics: days for totals, whole weeks for lists); raw events and visitor markers are deleted after 14 and 100 days by a daily cron. The event list lives inlib/jobs/analytics.ts.
Everything is deterministic except the tailored applications, which call the Anthropic API.
All routing decisions live in proxy.ts:
admin.devrel.studio/* → rewrite to /admin/* (Kinde session required)
<slug>.devrel.studio/* → rewrite to /<slug>/* (no Kinde; access-code gate in the layout)
reports.acme.com/* → same, resolved via a cached Convex lookup
devrel.studio/@handle → rewrite to /portfolio/handle
devrel.studio/demo → the public demo client dashboard, served on the apex
devrel.studio/* → marketing and the advocate workspace
Worth knowing:
- Reserved subdomains (
www,api,admin,docs,demo, …) live inlib/naming.ts, shared with the Convex mutation that decides whether a slug can be claimed — so the two namespaces cannot drift apart. A reserved name 404s rather than quietly serving the marketing site. - Custom domains cost one Convex HTTP lookup on first request, cached in module scope with a shorter TTL for misses so a domain being set up starts working within the minute.
- View tracking happens in the proxy because it is the only chokepoint in front of both the client dashboard and the portfolio, and it runs before the ISR cache — so a portfolio served from cache is still counted. Bots and prefetches are excluded.
| Layer | Technology |
|---|---|
| Framework | Next.js 16 — App Router, Turbopack |
| UI runtime | React 19 |
| Language | TypeScript 5 |
| Styling | Tailwind CSS v4 with light/dark themes via next-themes |
| Components | shadcn/ui on Radix primitives |
| Charts | Recharts 2 |
| Backend / DB | Convex 1.44 — reactive queries, mutations, actions, crons, file storage |
| Auth | Kinde (@kinde-oss/kinde-auth-nextjs 2) |
| Payments | Stripe — optional; the app runs fully without keys |
| Resend, via Convex actions | |
| AI | Anthropic API, called from Convex, for tailored CVs and cover notes only |
| CV parsing | unpdf, in a Convex Node action |
@react-pdf/renderer + pdf-lib |
|
| Tests | Vitest + Testing Library + jsdom |
| Hosting | Vercel (frontend) + Convex Cloud (backend) |
devrel_studio/
├── app/
│ ├── (main)/
│ │ ├── dashboard/ # Advocate workspace (Kinde-protected)
│ │ │ ├── page.tsx overview · add/ · edit/[id]/ · content/
│ │ │ ├── pipeline/ analytics/ · clients/ · reports/
│ │ │ ├── members/ billing/ · settings/
│ │ │ └── jobs/ discover · tracker/ · alerts/ · market/ · profile/ · pro/ · kit/[slug]/
│ │ ├── admin/ # Platform console, served on admin.devrel.studio
│ │ └── login/ # The console's own front door
│ ├── (subdomain)/
│ │ └── [subdomain]/ # Client dashboard — layout.tsx holds the access gate
│ │ ├── page.tsx report/ · reports/ · llms.txt/
│ │ └── opengraph-image.tsx
│ ├── jobs/ # Public job board: list, role pages, hubs, apply redirect, feed, llms.txt
│ ├── portfolio/[handle]/ # Public portfolio (rewrite target for /@handle)
│ ├── api/ # auth · billing/checkout · export-report
│ │ # manager-access · members · portfolio/revalidate · track
│ ├── llms.txt/ # Site-level agent index
│ ├── sitemap.ts robots.ts # Crawler-facing, generated
│ └── page.tsx # Landing page
│
├── components/
│ ├── brand/ # Logo mark + the loader built from it
│ ├── dashboard/ admin/ # Workspace and console UI
│ ├── subdomain/ # Access gate, report view
│ ├── analytics/ # Dwell-time beacon
│ ├── jobs/ # Job board UI: board, cards, logos, tabs
│ ├── marketing/ invite/ # Nav, footer, invitation flow, pricing blocks
│ └── ui/ # shadcn/ui library
│
├── convex/
│ ├── schema.ts # 16 tables
│ ├── model/ # Shared server logic: auth, workspaces, plans,
│ │ # access windows, rate limiting, admin guards
│ ├── content.ts clients.ts # Core domain
│ ├── reports.ts analytics.ts # Reporting and view data
│ ├── members.ts users.ts # Workspaces, invites, profiles
│ ├── admin*.ts # Console queries, split by section
│ ├── managerAccess.ts # Client access codes and sessions
│ ├── portfolio.ts # Public reads + the llms.txt index
│ ├── billing.ts trials.ts # Plans, access windows, trial notices
│ ├── sync.ts email.ts # npm/GitHub stat refresh; Resend actions
│ ├── crons.ts http.ts # Scheduled jobs; webhooks and ingest
│ ├── jobSync.ts jobs.ts # Feed sync, expiry, public reads
│ ├── jobBoard.ts jobCv.ts # Profiles, CV parsing, tracker
│ ├── jobAlerts.ts jobKit.ts # Alerts (incl. instant), tailored application kits
│ ├── jobPro.ts adminJobs.ts # Jobs Pro access, benchmarks, hiring view, admin console
│ └── migrations.ts
│
├── lib/ # Framework-free helpers, shared by app and convex
│ ├── jobs/ # Pure job-board logic: taxonomy, locations, salary, CV, match,
│ │ # tracker stats, SEO, sources, Pro gate, style checks
│ ├── metrics.ts # Which number belongs to which category
│ ├── naming.ts # Slug/handle rules and the reserved lists
│ ├── llms-txt.ts report.ts # Generated text surfaces
│ ├── view-tracking.ts # Edge-safe hashing, bot and prefetch rules
│ └── manager-auth.ts stripe.ts retainer.ts schedule.ts …
│
├── tests/ # Vitest suites
└── proxy.ts # Host routing, auth gate, view tracking
| Route | Access | Description |
|---|---|---|
/ |
Public | Landing page |
/pricing /contact /privacy /terms |
Public | Marketing and legal |
/sign-in /sign-up |
Public | Branded entry to Kinde's hosted flow |
/demo |
Public | A real, seeded client dashboard |
/@handle |
Public | Portfolio (rewrites to /portfolio/handle) |
/invite/[token] |
Public | Workspace invitation |
/dashboard/** |
Kinde | The advocate workspace |
| Route | Access | Description |
|---|---|---|
/jobs |
Public | All roles, with filters |
/jobs/[slug] |
Public | One role, with pay benchmark and JobPosting JSON-LD |
/jobs/roles/[family] /jobs/companies /jobs/companies/[company] |
Public | Hub pages |
/jobs/salaries /jobs/remote /jobs/contract |
Public | Pay data, remote roles, contract and freelance roles |
/jobs/out/[slug] |
Public | Counts the click, then redirects to the employer |
/jobs/feed.xml /jobs/llms.txt |
Public | RSS and an agent index |
/dashboard/jobs/** |
Kinde | Discover, tracker, alerts, market, CV and preferences, Pro, kit |
| Route | Access | Description |
|---|---|---|
/ |
Gate | Live performance dashboard |
/reports /report |
Gate | Monthly reports, feedback, PDF download |
| Route | Access | Description |
|---|---|---|
/login |
Public | The only unauthenticated page on this host |
/ /users /workspaces /revenue /traffic /content /requests /abuse /audit |
Admin | Console sections |
| Route | Description |
|---|---|
/api/auth/[kindeAuth] |
Kinde login, logout, register, callback |
/api/manager-access, /api/manager-access/code |
Redeem and request a client access code |
/api/members/invite, /api/members/accept |
Workspace invitations |
/api/billing/checkout |
Stripe checkout session (when configured) |
/api/export-report |
Server-rendered PDF |
/api/jobs |
Public JSON for the job board (CORS on, cached 15 minutes) |
/api/jobs/revalidate |
Convex calls this after each sync; guarded by JOBS_REVALIDATE_SECRET |
/api/track/duration |
Dwell-time beacon |
/api/portfolio/revalidate |
Refresh the caller's own portfolio (Kinde-gated revalidatePath) |
/kinde (user sync webhook) · /stripe (payment webhook) · /resolve-domain (custom-domain lookup) · /track (view ingest)
Sixteen core tables in convex/schema.ts, plus nine for the job board (below).
| Table | Purpose |
|---|---|
users |
Profile, portfolio fields, plan and access window, admin role |
contentEntries |
The core record — one row per piece of work |
clients |
Engagements: retainer and rate history, branding, slug, custom domain |
workspaces · memberships · workspaceInvites |
Team accounts and roles |
managerSessions · managerAccessAttempts |
Client dashboard sessions and rate limiting |
reportSchedules · reportNotes · reportFeedback |
Report delivery, written summaries, client replies |
pageViews |
Analytics, pruned after a year |
accessRequests |
Requests to view a gated dashboard |
adminAuditLog · impersonationSessions |
Console accountability |
publicWriteAttempts |
Abuse signal on unauthenticated surfaces |
jobSources · jobs · jobDescriptions · jobStats |
Feeds, listings (with a search index), full descriptions, and cached market stats |
jobProfiles · jobApplications · jobAlerts |
A user's CV and preferences, tracked roles with stage history, and saved alerts |
jobKits · jobProRequests |
Tailored application kits (also how free uses are counted) and Jobs Pro purchase requests |
| Field | Type | Notes |
|---|---|---|
userId / workspaceId |
ids | Creator; the workspace that authorises reads |
client |
string |
Slug, name or company — resolved within one client's owner |
category |
Written | Video | Event | Podcast | Package | Demo |
Absent reads as Written |
title · link · platform · publicationDate |
||
trackingLink · notes |
string |
Internal. Never leaves through a public read |
status |
Published | Draft | Waiting Approval | Scheduled |
|
views · downloads · weeklyDownloads · attendees · stars |
number? |
Only the metric its category owns is ever counted |
packageName · eventName · eventLocation · podcastName · repoUrl · stack |
string? |
Category-specific |
reshares |
{platform, link, date}[]? |
Cross-promotion log |
tags · contentType · updatedAt |
lib/metrics.ts is the single source of truth for which number belongs to which category — an Event's stray views is never added to a views total.
Six, each with its own platforms, sub-types and headline metric:
| Category | Metric | Platforms | Sub-types |
|---|---|---|---|
| Written | Views | Dev.to, freeCodeCamp, Medium, Hashnode, LinkedIn, Newsletter, Blog, GitHub, Docs, X | Tutorial, Guide, Reference Doc, Blog Post, Case Study, Opinion |
| Video | Views | YouTube, Loom, Vimeo, TikTok | Tutorial, Demo, Conference Talk, Interview |
| Event | Attendees | Free text — conference names vary too much for a dropdown | Conference Talk, Workshop, Meetup, Panel, Keynote |
| Podcast | Listeners | Spotify, Apple Podcasts, YouTube Podcasts | Guest Appearance, Host, Solo Episode |
| Package | Downloads + weekly | npm, GitHub | Convex Component, Library, CLI Tool |
| Demo | Stars | Vercel, Netlify, Cloudflare Pages, GitHub | Full App, Starter Kit, Sample |
Package and Demo metrics refresh themselves — a daily cron reads npm's download API and GitHub's star count, with a "Refresh now" button for the impatient. GITHUB_TOKEN is optional and only raises the rate limit.
Three independent models. Confusing them is the easiest way to introduce a hole.
1. Advocate — Kinde. Sign-in and sign-up redirect to Kinde's hosted flow; /api/auth/kinde_callback completes it. A Kinde webhook to <deployment>.convex.site/kinde creates or updates the users row. contexts/user-context.tsx bridges the Kinde session and the Convex profile for every dashboard page. Authorisation resolves through the workspace, not the user.
2. Client manager — access code. Managers have no account. app/(subdomain)/[subdomain]/layout.tsx checks, server-side on every request, whether the visitor holds a valid session cookie or the dashboard is marked public. The dashboard is never sent to the browser otherwise. Codes are hashed with MANAGER_CODE_SECRET; attempts are rate-limited.
Adding a route under
(subdomain)/[subdomain]/? A layout wraps pages, not route handlers. Anything you add as aroute.tsthere inherits the address of a private dashboard and none of its protection — it must run the gate itself. Seellms.txt/route.tsfor the pattern.
3. Platform admin — role, not host. Authority lives in an adminRole column on users, never in an environment variable at request time. admin.devrel.studio is its own origin for isolation, but every console query calls requireAdmin server-side. An account without the role gets the same "not found" as any unknown URL. Impersonation is read-only and logged.
The first administrator is a chicken-and-egg problem — every mutation that grants the role requires the role. It is broken by an internalMutation, unreachable from the browser and runnable only with deploy credentials:
npx convex env set ADMIN_EMAILS "you@example.com" # on the Convex deployment
npx convex run admin:bootstrap --prodIdempotent: a second run is silent rather than filling the audit trail.
Crons in convex/crons.ts:
| Job | Cadence | Purpose |
|---|---|---|
| Refresh package and demo stats | Daily 04:00 UTC | npm downloads, GitHub stars |
| Send scheduled client reports | Hourly :05 | Each schedule carries its own day, hour and timezone — hourly is the only way to honour that |
| Send trial notices | Daily 08:00 UTC | Idempotent; a retry mails nobody twice |
| Prune old page views | Daily 03:30 UTC | Views are kept one year |
| Send weekly digests | Mondays 08:00 UTC | Skipped entirely for a quiet week |
| Prune orphaned uploads | Daily 04:15 UTC | Logos uploaded but never saved |
| Sync job sources | 06:00, 14:00, 22:00 UTC | Fetch every feed, add new roles, expire missing ones |
| Refresh job stats | 25 minutes after each sync | Recompute market stats, refresh /jobs, send instant alerts to Pro users |
| Prune expired jobs | Daily 03:45 UTC | Delete roles that closed more than 30 days ago |
| Send job alerts | Daily 07:30 UTC | Daily and weekly digests, only when there is something new |
| Draft blog post | Mon, Wed, Fri 05:00 UTC | Pick a topic, research, write, fact-check, email for review |
| Prune job analytics | Daily 04:10 UTC | Raw events after 14 days, visitor markers after 100 |
Alongside sitemap.xml, robots.txt and generated OpenGraph cards, the app publishes llms.txt files for agents:
| URL | Contents |
|---|---|
/llms.txt |
Directory of every published portfolio |
/@handle/llms.txt |
One advocate's published work, grouped by category |
/jobs/llms.txt |
The job board: what it covers, and how to read the JSON API |
/api/jobs |
Current roles as JSON |
<slug>.devrel.studio/llms.txt |
A client dashboard — only when the advocate marked it public; otherwise a stub |
The rule they obey: an llms.txt says exactly what the HTML page at the same URL already says, and nothing more. Entries are projected field by field, never spread, so notes and trackingLink cannot surface. Customer-authored text is flattened to a single line and leading markdown markers are escaped — a bio may describe itself, not the file it sits in.
If you front the site with a CDN, check its managed
robots.txt. A default AI-crawler block will stop the exact audience these files exist for.
Priced per month, sold in blocks rather than renewed monthly.
| Plan | Monthly | Clients | Entries | Seats |
|---|---|---|---|---|
| Free Trial | — | 1 | 10 | 1 |
| Starter | $29 | 1 | Unlimited | 1 |
| Pro | $59 | 5 | Unlimited | 1 |
| Agency | $119 | Unlimited | Unlimited | 5 |
Terms: 1 month (0%), 3 months (10%), 6 months (15%), 12 months (20% off). Trial is 14 days.
Access is a timestamp, not a subscription — which makes selling a month, a year or a perpetual licence the same field with a different number in it. convex/model/plans.ts is the one definition of what each plan allows, imported by both the enforcing mutations and the pricing UI, so the two cannot drift.
A separate pass for the job board, sold as one payment for 12 months through the same manual transfer flow ($49, £39 or ₦75,000). It is not part of the plans above; comped accounts include it.
| Free | Jobs Pro | |
|---|---|---|
| Browse, filter, tracker, CV match | Yes | Yes |
| Tailored CV bullets and cover note | 3 in total | Up to 60 a month |
| Saved alerts | 2, daily or weekly | Up to 50, including instant |
| Pay benchmarks for your role, level and region | Market median only | Yes |
| Hiring view (repeat hirers, longest-open roles) | No | Yes |
Tailored kits are written from the user's CV and the published work logged in DevRel Studio, then checked against a list of stock phrases and any number not found in the source, with one repair pass. Limits live in lib/jobs/pro.ts; the Convex gate and the pricing UI read the same file. The owner opens a pass from /admin/jobs after a transfer clears.
Stripe is wired but optional. Without keys, billingIsConfigured() is false and the billing page says so rather than the app failing to boot.
git clone <your-repo-url> devrel_studio
cd devrel_studio
npm installnpx convex devWrites CONVEX_DEPLOYMENT and NEXT_PUBLIC_CONVEX_URL into .env.local and watches convex/ for changes.
- Create an application in the Kinde dashboard
- Allowed callback URL:
http://localhost:3000/api/auth/kinde_callback - Allowed logout redirect URL:
http://localhost:3000 - Add a webhook to
https://<deployment>.convex.site/kinde, subscribed touser.createdanduser.updated
See Environment variables. At minimum you need the Convex and Kinde values plus NEXT_PUBLIC_ROOT_DOMAIN and MANAGER_CODE_SECRET.
npx convex dev # terminal 1 — backend
npm run dev # terminal 2 — frontendOpen http://localhost:3000.
Testing the other hosts locally. Subdomains of localhost work without any hosts-file editing:
curl -H "Host: acme.localhost:3000" http://localhost:3000/
open http://admin.localhost:3000/login| Variable | Where | Description |
|---|---|---|
CONVEX_DEPLOYMENT |
Local | Set by npx convex dev |
NEXT_PUBLIC_CONVEX_URL |
Both | Convex deployment URL |
NEXT_PUBLIC_ROOT_DOMAIN |
Both | Bare host — devrel.studio, localhost:3000. Composed with a slug for subdomain URLs |
KINDE_CLIENT_ID / KINDE_CLIENT_SECRET |
Next | From the Kinde application |
KINDE_ISSUER_URL |
Both | https://yourapp.kinde.com |
KINDE_SITE_URL |
Next | Your origin |
KINDE_POST_LOGIN_REDIRECT_URL |
Next | e.g. /dashboard |
KINDE_POST_LOGOUT_REDIRECT_URL |
Next | e.g. / |
MANAGER_CODE_SECRET |
Both | Hashes client access codes. Rotating it invalidates every manager session |
| Variable | Enables |
|---|---|
KINDE_COOKIE_DOMAIN |
Read by the Kinde SDK; set to .devrel.studio to share a session across subdomains |
RESEND_API_KEY, EMAIL_FROM |
All outbound email — invites, reports, digests, trial notices |
STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, STRIPE_PRICE_{STARTER,PRO,AGENCY} |
Card checkout |
GITHUB_TOKEN |
Raises the GitHub rate limit for star sync |
ADMIN_EMAILS |
Convex-side only. Read by admin:bootstrap to promote the first administrators — see below |
OWNER_EMAIL |
Where operator notifications are sent (defaults to support@devrel.studio) |
NEXT_PUBLIC_APP_URL, SITE_URL |
Absolute URLs where the origin cannot be inferred |
JOBS_REVALIDATE_SECRET |
Set on both Convex and Vercel. Lets Convex refresh /jobs after each sync |
TOURS_SECRET |
Set on both Convex and Vercel (openssl rand -hex 32). Hashes visitor IPs so the job board tour is remembered across browsers. Without it the tour falls back to browser and account memory |
BLOG_REVIEW_EMAIL, BLOG_AI_MODEL |
Convex-side. Where review emails go (default me@sholajegede.com) and the model for blog drafts (defaults to claude-sonnet-5-5). The blog also needs web search enabled for the Anthropic API key's organisation, plus RESEND_API_KEY and SITE_URL |
ANTHROPIC_API_KEY, JOBS_AI_MODEL |
Convex-side. Tailored CVs and cover notes. JOBS_AI_MODEL defaults to claude-sonnet-4-5 |
REDDIT_CLIENT_ID, REDDIT_CLIENT_SECRET |
Convex-side, optional. Only needed if you add a r/forhire feed |
NEXT_PUBLIC_LOGO_DEV_TOKEN |
Sharper company logos from logo.dev; the default is Google's favicon service |
REPORT_REDIRECT_TO |
Diverts every report email to one address — useful in staging |
Convex functions read their own environment (npx convex env set …), which is separate from Vercel's. MANAGER_CODE_SECRET, RESEND_API_KEY, GITHUB_TOKEN, JOBS_REVALIDATE_SECRET and TOURS_SECRET need to be set in both places; ADMIN_EMAILS belongs on Convex only.
npm test # vitest run
npm run test:watch
npm run typecheck # tsc --noEmit
npm run lintConcentrated where a mistake is expensive rather than spread evenly: access control and admin authorisation, impersonation, view-tracking correctness and write contention, metric roll-ups, retainer rate history, report scheduling across timezones, PDF rendering, CSV import, naming rules, and the job board: title classification, locations and eligibility, salary parsing, CV analysis and matching, tracker stats, Jobs Pro gating, style checks, and every source parser.
Two deployments, and they are separate. The frontend goes to Vercel on push to main. Convex does not — it is a manual step.
# 1. Backend — schema, functions, crons
npx convex deploy
# 2. Frontend — automatic on push to main, or:
vercel --prodOrder matters. Deploying the frontend first means new code calling functions that do not exist yet; a route that fetches a missing query will fall back or fail until Convex catches up.
- Point
NEXT_PUBLIC_ROOT_DOMAINat the live domain and update everylocalhost:3000value - Update Kinde's callback and logout URLs, and repoint the webhook at the production Convex deployment
- Set
MANAGER_CODE_SECRETin both Vercel and Convex — the same value - Add a wildcard DNS record (
*.devrel.studio) and a wildcard certificate, or no client subdomain resolves - If the apex is proxied by a CDN, make sure subdomains are not — and check the CDN's managed
robots.txt - Run
npx convex deploy - Job board, first time: set
JOBS_REVALIDATE_SECRETandSITE_URLon Convex and the secret on Vercel, thennpx convex run --prod jobSync:seedSourcesandnpx convex run --prod jobSync:syncAll - For tailored applications, set
ANTHROPIC_API_KEYon Convex
| Command | Description |
|---|---|
npm run dev |
Next.js dev server on :3000 |
npm run build / npm run start |
Production build and server |
npm test / npm run test:watch |
Vitest |
npm run typecheck |
tsc --noEmit |
npm run lint |
ESLint |
npx convex dev |
Watch and push convex/ to the dev deployment |
npx convex deploy |
Push schema and functions to production |
npx convex env set KEY value |
Set a Convex-side environment variable |
Private. All rights reserved.
Built by Shola Jegede.