What's new on Employbl.

The shipping log, newest first — built in public, one entry per change. For what the product is, start with the homepage; for what agents can do with our data, see the MCP docs.


July 2026 (2026-07-12) — Public changelog page

This document went public: new /changelog page renders it with the editorial layout kit (Masthead + typography pass, .md cross-references neutralized to plain text), linked from the footer. Static at build time — the changelog only gains entries when we deploy anyway. Part of the GTM activation plan (docs/content/gtm-activation-plan.md): build-in-public trust signal, standing feed for the newsletter's "what we shipped" section and Friday LinkedIn posts, and an AEO surface. Also: scripts/screenshot-section.ts now passes --no-sandbox when running as root, so remote-container sessions can take screenshots.


July 2026 (2026-07-12) — First-principles search UX: /jobs + /companies get one-row filter chips (#381, #382, #383)

Both in-app search pages rebuilt around the same principle: search bar → one chip row → results. No duplicate filter surfaces, no page furniture.

/jobs (#381):

  • Job result rows lead with the job: full-weight wrapping title, decision line (company · stage chip · location · Remote badge · green salary), relative recency right-aligned, letter-tile logo fallback. Killed the per-row "View Job" button, company taglines, and the company pill wall (including the "$614174976 raised" string bug).
  • FilterChipBar replaces FacetStrip: Remote toggle, Level / Funding stage dropdowns with live facet counts, Salary ($100K+…$250K+), Posted (day/week/month), "All filters" opens the drawer.
  • Save-search affordance rides the filter row (right-aligned chip): "Save search" when unsaved; "Saved · {name}" opening the edit modal when the current query matches a saved search. In-page saved-search list removed — recall lives in the sidebar's pinned searches + /saved-job-searches.

/companies (#382, #383):

  • Same chip row: Hiring toggle, Funding stage + Industry dropdowns with live Typesense facet counts (sector added to facet_by; the search API now returns facets). "All filters" opens the drawer; drawer-applied filters (tech, investors, HQ, collections, keyword, map area) surface as dismissable indigo chips in the row. Header's duplicate Filters button and the ActiveFiltersPills row deleted. Chip primitives shared across both pages (components/filters/FilterChipPrimitives.tsx).
  • Bug fix: useCompaniesSearch never forwarded sector, founded_after, or founded_before to the API — the Phase 26 industry filter and part of Phase 23 NL-compiled searches were silently ignored (Fintech + Kubernetes + hiring returned 723 results; correct answer is 105). Verified on prod post-deploy: sector=Fintech + Hiring → 428 companies, Airwallex/Stripe/Okx up top.

July 2026 (2026-07-11) — Curated industry taxonomy replaces Diffbot classifiers (Phase 26)

Diffbot's DBpedia-style industry tags ("Software Companies" covered ~90% of the database) replaced with an operator-vocabulary taxonomy, end to end in one day:

  • 26a: industries (17 sectors → 58 subsectors, seed of record in lib/industries/industryTaxonomy.ts) + company_industries tables; /admin/industries.
  • 26b: LLM classification (Haiku batches; description + premium copy + Diffbot hints + derived tech stack + job titles → primary sector + ≤3 subsectors with confidence). Nightly classify-company-industries job for new companies; full backfill run at ship time. Spot-checks: Okta → Cybersecurity/Identity & Access, Sezzle → Fintech/Payments, Anduril → Aerospace & Defense/Defense Technology.
  • 26c: Typesense sector (facet) + subsector fields; /companies Industries filter now sector-grouped taxonomy names (sector param; legacy ind tag-id URLs still resolve); MCP find_companies sector param (server v0.8.2, /docs updated); new /industries + /industries/[slug] SEO pages (stats, subsectors, who's-here grid, adjacent subsectors); industry chips on company profiles.
  • 26d: populate-company-tags no longer writes industry or legacy company-tech-stack Spatie tags (investors still sync). Existing taggings stay read-only; /company-collections unaffected.

July 2026 (2026-07-11) — Homepage profile proof + dual pricing + taxonomy-backed tech filter (Phases 21a + 21e)

Homepage (21a):

  • "What's in a profile" blocks 02/03 finally have product shots: Waymo's job-derived tech-stack section and Sierra's founder cards (Bret Taylor career chips); block 01 re-captured as a tight crop of Waymo's funding section. Captions name the intelligence ("TECH STACK FROM OPEN ROLES · NOT SELF-REPORTED").
  • Company Premium card on the homepage — $99/mo · 10 seats alongside the candidate card (previously a footnote link). pricing_cta_clicked PostHog events on both CTAs with source: "homepage".

Search + MCP (21e):

  • /companies tech-stack filter sources the curated taxonomy via new /api/technologies (searchable by name or category); filter values are technology names matched against the Typesense union — ts=Kubernetes → 767 companies. Legacy tag-id URLs still work.
  • MCP find_companies advertises job-derived stack matching (server v0.8.1, /docs updated).

July 2026 (2026-07-10) — Tech-stack section rebuild + technology intelligence pages (Phase 21c)

The tech stack becomes a first-class surface — on company profiles and as its own page network.

  • Company profile tech-stack section rebuilt (features/companies/TechStackSection.tsx, replaces the flat mono chips in PremiumProfile): grouped by category (engineering leads), simple-icons logos, per-tech open-role counts, and provenance labels — "From open roles" (job-derived) vs "Company-reported" (declared) — the same transparency instinct as the profile firewall. Renders for every company with derived data (2,434 companies), not just the 11 published premium profiles. New "Tech Stack" TOC entry.
  • /tech-stacks/[slug] — 304 technology intelligence pages (editorial kit: Masthead + StatStrip): one-line definition, open-role/company/remote-share stats, who's-hiring grid (top 12 companies by listing count), co-occurring technologies, common role titles. getStaticProps + fallback: "blocking", no revalidate per the Vercel cost rules.
  • /tech-stacks index rewired from legacy company-collection tag links to the curated technology pages, grouped by category with live job counts.
  • skills.description backfilled with LLM one-liners (264 technologies) via scripts/backfill-technology-descriptions.ts.
  • Derived-stack JSON now carries logo_slug (aggregation re-run across all 2,434 companies).

July 2026 (2026-07-10) — Company tech stacks aggregated from job listings (Phase 21b-3)

job_skills now rolls up to a per-company derived stack.

  • companies.tech_stack_from_jobs — denormalized JSON ([{id, name, slug, category, listings}], top 20, confidence ≥ 0.5). Backfilled: 2,434 companies across 299 technologies. Waymo → C++/Python/PyTorch/TensorFlow/JAX; Vercel → Next.js/Vercel/TypeScript.
  • aggregate-company-tech-stack worker job (daily 8:15am UTC, after extraction), full-rebuild idempotent, clears stale stacks; admin Run Now + CLI script.
  • Typesense tech_stack filter now matches a union: job-derived stack ∪ Spatie tech tags ∪ published premium-profile declared stacks — unenriched companies become findable by technology for the first time. Applies on next companies sync.
  • Premium tech_stack column deliberately NOT renamed (live-prod breakage); two-table split keeps derived vs declared sources separate.
  • Taxonomy pruning continues: CMS, EDR, ACH, APT unreviewed (generic acronyms/standards, not products) — 297 technologies now.

July 2026 (2026-07-10) — Job-derived tech-stack extraction (Phase 21b-2)

The dead job_skills pipeline (last write March 2025, ~1% coverage, out-of-repo) is replaced by an in-repo, dictionary-first extractor. No LLM calls at extraction time — the curated taxonomy's names + aliases compile into one word-boundary matcher.

  • Backfill: 187,282 job_skills rows across 45,683 active listings (51%) covering 2,560 companies, confidence-scored by mention count.
  • extract-job-skills-scheduled worker job (pipeline queue, daily 7:15am UTC after the job-listings sync) keeps new listings extracted incrementally via the new job_listings.skills_extracted_at stamp; admin Run Now wired. CLI: scripts/extract-job-skills.ts (--force rescans after taxonomy changes).
  • False-positive control (lib/skills/technologyDictionary.ts): deny list for English/domain collisions ("GTM" is go-to-market, "Assembly" is General Assembly, "backbone of our platform") and job-post boilerplate (LinkedIn/Instagram/Zoom mentions aren't stack signal); ambiguous English-word technologies (Go, React, Swift, Excel, Segment…) match case-sensitively, never at sentence start, never in hyphen compounds ("Go-to-market"), and single mentions get a confidence discount. Verified by two sample-inspection passes against prod before the full run.

July 2026 (2026-07-10) — Canonical technology taxonomy (Phase 21b-1)

Foundation for StackShare-style tech-stack intelligence (roadmap.md § Phase 21). A "technology" is now a skills row with category IS NOT NULL AND reviewed = true — the never-finished category/aliases/reviewed columns are finally in use.

  • 311 curated technologies across 17 category buckets (lib/skills/technologyCategories.ts), seeded by scripts/curate-technology-taxonomy.ts: candidates gathered from job_skills frequency + the 1,804 drafted premium_company_profiles.tech_stack arrays + Spatie company-tech-stacks tags, LLM-classified (strict: no soft skills, methodologies, protocols, or generic categories), then upserted onto canonical skills rows. Classifications cache to .cache/ so re-runs are free; script is idempotent and skips already-reviewed technologies.
  • New skills.slug + skills.logo_slug columns (docs/sql/2026-07-10-skills-technology-taxonomy.sql). Slugs are unique (will power /tech-stacks/[slug] in Phase 21c); logos resolve deterministically against the simple-icons package (154/311 matched; server-side only — see lib/skills/simpleIconMatch.ts).
  • Alias clusters folded into canonical spellingsnormalized_name variants like "gcp"/"Google Cloud Platform", "Postgres"/"postgresql", and case-dupes now all group under one canonical name, with the variants preserved in aliases (these feed the Phase 21b-2 dictionary extractor).
  • /admin/technologies — admin review page grouped by category: logo, slug, aliases, job-listing usage counts across each technology's whole cluster.

July 2026 (2026-07-10) — Mercury-style app navigation: no top navbar for logged-in users

Logged-in chrome rebuilt around the sidebar (modeled on Mercury's app shell); public pages untouched.

  • Top navbar removed on desktop for authenticated users. AppLayout no longer renders LegacyNavbar. The new AppSidebar is the only desktop chrome: brand at top, grouped nav (Home + AI Chat, then EXPLORE: Search Jobs / Companies / Map / Learn, then YOUR SEARCH: Job Tracker / Saved Searches / Profile), user menu pinned to the bottom (avatar + name/email + Premium/Free plan, opening up to Your Profile / Billing-or-Upgrade / Sign out). Nav config lives in one place (components/layout/appNavLinks.ts) shared by desktop and mobile so they can't drift.
  • Saved Searches finally has a nav entry/saved-job-searches was previously unreachable from the sidebar.
  • Active states now match nested routes (/jobs/[slug] highlights Search Jobs, /profile/* highlights Profile) instead of exact-path only.
  • Collapse state persists in localStorage across pages and sessions; collapsed rail redesigned (no more dark-hover artifacts).
  • Mobile: slim app header (logo + hamburger) replaces the marketing navbar; the hamburger and bottom-nav "More" open a new app-specific slide-over (AppMobileMenu) that mirrors the sidebar instead of showing marketing links (About/Pricing/Register). Bottom-nav Home now points at /dashboard.
  • Job tracker layout collapsed into AppLayout fullHeight — tracker no longer renders a marketing Footer under the kanban board.
  • Viewport-height fixes for the navbar-less world: /chat and the explore map size to the full viewport on desktop (100dvh) and account for the 3.5rem mobile header.

Follow-up (same PR):

  • Pinned saved searches in the sidebar (Mercury's "Bookmarks"): up to 5 saved searches render under the Saved Searches link with a new-jobs-this-week badge. New /api/auth/saved-searches/new-counts endpoint (capped at 5 COUNT queries, SWR-deduped 5 min per session).
  • Trial urgency in the user area: "Trial — N days left" replaces the plan label in sidebar + mobile menu, upgrade card copy switches to keep-your-access framing, Billing menu item hidden during trial (trial users have no Stripe subscription to manage).
  • Accent unification: indigo → tangelo/ink across all authed surfaces (chat page + panel, dashboard sections, recommendations). Indigo is now retired from app chrome per the redesign brief.
  • Tracker cards fixed: cards showed only "Created over a year ago" because board syncs hard-delete stale job_listings rows. Now: title/company/URL snapshotted into external_* columns at save time, || fallbacks tolerate legacy empty-string values, and orphaned rows render "Listing no longer available" instead of blank cards with dead links.
  • AppPageHeader standardizes the slim title + primary-action row (dashboard, saved-searches pages).

Dashboard command center (same PR):

  • Setup-first dashboard. New SetupPanel owns the top slot until the user has BOTH a profile signal (jobs-for-you profileStrength ≥ 20) and saved preferences. Step 1: resume upload (existing parse flow) or paste LinkedIn/resume text into new /api/auth/profile/import-text (reuses the MCP upload_profile_from_text pipeline, refactored into a shared applyProfileText core — platform key, no credit metering). Step 2: preferences are AI-drafted from the profile via new /api/auth/preferences/suggest (Claude call constrained to the PreferenceCard option slugs, deterministic heuristic fallback), prefilled into the preference chips for one-tap save.
  • Recommendations gated on setup. Jobs For You and Recommended Companies render only when profile + preferences exist — no more matching on an empty profile. "Skip for now" collapses setup to a resume-setup banner.
  • Mercury-style command center once set up: "Welcome back, {name}", quick-action pills (Search jobs / Ask AI / Track application / Saved searches), and a stat row — active applications, new saved-search matches this week, profile strength, watchlist size. Stats ride two new COUNTs on /api/dashboard plus the existing new-counts and jobs-for-you endpoints (shared SWR keys, no extra requests).

July 2026 (2026-07-10) — /jobs fixes for signed-in users: salary on cards, working save button, honest copy, skills deep-link

  • Salary pill on job cards. minimum_salary/maximum_salary came back from the search API but were never rendered — now a green 💵 $140K – $180K pill.
  • Save-job button fixed. It gated on isPremium with a "You must be logged in" toast (shown to logged-in users) and its optimistic state never rolled back on API failure. Now gates on authentication (the endpoint only requires a session), rolls back on error, and has an aria-label.
  • Funding pill consolidation. Three separate yellow "Latest Round" pills (name / date / amount) → one Series B · $50M · Mar 2025 pill.
  • Honest copy + dead controls. Free-user pagination said "You must login to search for jobs" to logged-in users → now an upgrade message. The no-results "Reset search" button did nothing → now links back to a clean /jobs.
  • "Add skills" CTAs now land in the skills editor. /profile?add=skills deep-link opens AddEditSkillsModal directly (query preserved through both profile redirects); wired into the /jobs For You tab (both CTAs) and the dashboard For You section. Previously these dropped users on the profile page with no hint where skills live.

July 2026 (2026-07-10) — AI Chat fixed: markdown rendering, investor filtering, no more truncated answers

Connor's screenshot of /chat surfaced four defects, all fixed:

  • Raw markdown. Assistant messages rendered as plain text (###, **, bare [text](url)). Now rendered via react-markdown + remark-gfm through a shared ChatMarkdown component (typography-plugin styling; internal links go through next/link with prefetch={false}), used by both /chat and the slide-in ChatPanel.
  • "Investors isn't a filterable field" — it is. The chat's tool schemas in lib/chat/tools.ts had drifted from the MCP definitions: no investors param, so Claude told users the database couldn't do it and padded answers from training data. Added investors (with full-firm-name guidance: "Andreessen Horowitz", not "a16z" — verified against the tags table), min/max_employees to both search tools, and min_salary + posted_within_days to search_jobs. System prompt now states investor filtering exists and forbids padding results from training data.
  • Answers cut off mid-sentence. Per-round max_tokens raised 1024 → 2048 (session cap unchanged at 15K).
  • Glued paragraphs. Multi-round answers concatenated text across tool rounds without a separator ("…specifically:Good news…"). The client now inserts a paragraph break when a tool round starts.

July 2026 (2026-07-10) — Phase 10 begins: company-side monetization + candidate/company segmentation

First slice of Phase 10 (roadmap § Phase 10, unblocked by the funnel + churn signals — see now.md investigation #7):

  • Profile field firewall formalized (lib/companies/profileFirewall.ts). The descriptive-vs-objective boundary was previously implied by route field-omission; now one module sanitizes every company-authored write. Reps lost edit access to notable_investors + funding_narrative (funding intelligence — the rep editor now explains why instead of showing the inputs). All rep writes stamp profile_source=company_submitted + profile_updated_at; the admin review queue lists company submissions alongside AI-enriched drafts.
  • Company MCP tools (server v0.8, 24 tools). get_my_company (view counts, completeness, publish state) and update_company_profile (descriptive fields, partial updates) — gated by a new requireCompanyClaim guard: active users_companies claim required, no candidate subscription needed. Candidate tools keep their premium gate; everything stays authenticated + rate-limited + audit-logged. All four docs surfaces synced (22→24).
  • Self-serve $99/mo checkout. POST /api/company-rep/companies/[id]/checkout (rep-session-gated; the existing metadata.type="company_profile" webhook branch handles fulfillment) + an upgrade card on the rep dashboard with a "Premium active" state. Previously the only path was an admin-generated payment link. New isCompanyPremium() helper mirrors isPremiumUser() on the company side.
  • Rep segmentation signal. Claiming a company now tags the user user_types.type="employer" (candidate paths already wrote "candidate").
  • Public company funnel. New /for-companies marketing page (editorial layout: how-it-works, free-vs-$99 pricing, the trust-model explainer, FAQ); "For companies" section on /pricing; "For Companies" footer link; "Hiring?" one-liner under the homepage pricing card (kept quiet deliberately — homepage stays candidate-first per positioning.md).
  • Self-serve new-company creation. A rep signing in with an unindexed work domain (e.g. octavehq.com) now lands on /companies/new: enter the company name, website pinned to the verified Google Workspace domain (the domain IS the verification), company row + email_domain + claim + employer tag created in one POST, straight to the dashboard. Guards: consumer domains rejected, existing-match race check, ambiguous-domain 409 routed to support, unique-slug generation. The claim page and company-login dead ends ("email connor@") now route here instead. Slack ping on every creation.
  • $99/mo = full MCP for up to 10 teammates. New hasCompanyPremiumSeat(): an active claim on a company with an active subscription grants the full 24-tool MCP (everything candidates get for $25) to the company's first 10 active claimers — computed in the MCP verifyToken, accepted by the candidate-tool guard alongside candidate premium. No invite system: domain-match claiming IS the seat mechanism; cancelling revokes all seats at once; claimer #11 degrades gracefully to profile tools. Dashboard + get_my_company show seat usage; /docs explains both tiers.
  • Claim-outreach export (scripts/export-claim-outreach.ts). Instantly.ai-ready CSV of high-traffic unclaimed profiles with 30-day view counts + claim URLs — the "{N} people viewed {Company} last month" hook from gtm.md § Cold email outreach. Enriched-first by default.

Phase 10 hardening (same day, PRs #367–#368) — see company-signup-flow.md:

  • Duplicate route fix (#367). Deleted stale pages/for-companies/index.tsx ($299/mo intake form). Production had been serving the wrong page alongside pages/for-companies.tsx ($99 self-serve) — next build picked one unpredictably.
  • Inline Google sign-in on /for-companies. GoogleSignInButton — reps sign in without leaving the marketing page (callbackUrl=/companies/new).
  • Async LLM signup screening. qualify-company-signup BullMQ job (Claude Haiku) on every self-created company; creation is never blocked — flagged signups go to Slack + /admin/company-signups review queue with one-click deactivate.
  • Legacy intake retired. /api/intake/company-profile (old $299 intake) → 410, redirects reps to /for-companies.
  • Ops follow-up: Railway worker redeploy required so qualify-company-signup runs in production.

July 2026 (2026-07-10) — MCP server v0.7: company comparison + saved-search email alerts

Four more tools (22 total), building on v0.6:

  • compare_companies — 2-5 slugs → one markdown table: what they do, HQ, founded, size, total raised, latest round, open roles, work model, tech stack, notable investors. Pure read over companies + premium_company_profiles; preserves the caller's column order and reports unknown slugs.
  • save_search / list_saved_searches / delete_saved_search — "watch for staff-level AI infra roles in SF and email me weekly" is now one tool call. Writes the same saved_job_searches rows (and the same query-JSON shape) the website and the notification worker already use, so MCP-created alerts ride the existing daily/weekly send-job-notifications schedule with zero new infra. Default frequency weekly; capped at 25 saved searches per user; deletes are scoped to the caller's own rows.
  • All four docs surfaces synced (18→22): /docs ToolCards + new "Saved Searches & Alerts" section + v0.7.0 history entry, /mcp catalog, /features/mcp stats.

July 2026 (2026-07-09) — MCP server v0.6: geo search + profile-from-text, landing-page copy un-stales

Two new MCP tools (the first two items of the MCP-server campaign), bringing the server to 18 tools:

  • find_companies_near_me — free-text US location (address, "City, ST", or zip) → Mapbox geocode → nearest companies by HQ via PostGIS KNN (company_locations.geo <-> point, 29K+ geocoded HQs). Returns distance in miles, one-liner, open-role count, profile link; optional radius_miles and hiring_only filters. The campaign's marketing differentiator — no other job-search MCP does geo.
  • upload_profile_from_text — paste a resume / LinkedIn About / bio → one Claude extraction call (credit-metered like summarize_company, BYOK-aware) → applies via the existing update_profile / add_work_experience / add_skills handlers so validation stays in one place. Replies with what was written and what's still missing so the client model can follow up.
  • Docs surfaces synced per the CLAUDE.md rule: /docs ToolCards + count (16→18) + v0.6.0 version-history entry; /features/mcp stats. Also fixed drift found during the audit: /mcp and lib/mcp/tools.ts still said "Seven tools" from v0.3 — the tool catalog there now lists all 18.

July 2026 (2026-07-09) — Map pin popup: anchored at the company, full company info

The /map company card used to be a fixed bottom-right overlay showing only name + "View company". Now it's a real mapboxgl.Popup anchored at the clicked pin (React card portaled into the popup via createPortal; chrome reset scoped under .company-map-popup in globals.css), and it carries the full company snapshot: logo, tagline (premium tagline/one_liner, falling back to truncated description), employee range, latest round + year, open-role count, website, and the profile link. /api/maps/feature/[id] enriched accordingly (also returns HQ lng/lat so sidebar-driven opens anchor correctly). Card extracted to components/maps/CompanyPopupCard.tsx to stay under the 500-line frontend cap.


July 2026 (2026-07-09) — Homepage funding-desk strip: screenshots of recently funded, hiring-now companies

New "Where the money just landed" section on the homepage (PR #341): top-6 venture rounds (Series*/Seed/Angel) closed in the last 12 months where the company is hiring today and has a website screenshot from the capture-company-screenshot pipeline. Each card links to the company profile. Data snapshots at build time via getStaticProps (lib/home/fundedSpecimens.ts) — no ISR; images use per-image unoptimized (no Vercel image-transformation cost); links prefetch={false}. ~223 companies qualify, ranked by round size, so the strip refreshes naturally with each deploy.


July 2026 (2026-07-08) — Diffbot credit budget corrected: 250K/month confirmed active, funding rounds back on Diffbot

A 2026-07-05 session concluded the Diffbot account had lapsed to a "250 credits/month free tier" and kicked off a provider-replacement effort (People Data Labs evaluation, Crunchbase Pro CSV import for funding rounds). Verified live against Diffbot's account API on 2026-07-08 (curl "https://api.diffbot.com/v4/account?token=$DIFFBOT_API_KEY"): that was a misread. The personal250 plan is active with 250,000 credits/month, free — usage history confirms it (254,000 credits spent across 2026-07-04/05 on 10,160 founder Person Enhance calls, i.e. ~25 credits per Enhance, before 429s began; 359,350 credits on 2026-06-17).

  • PDL replacement dropped. Frontend PR #338 and go-cli PR #3 closed unmerged. Diffbot stays the primary source for company discovery, refresh, and founder enrichment.
  • Funding rounds back on Diffbot. The Crunchbase Pro CSV import path is dropped (importer PR closed unmerged; the planned Crunchbase Pro subscription was never actually purchased, so there is nothing to cancel). sync-funding-rounds-from-diffbot is primary again, with a dedup-key fix landing in employbl-go-cli (match on amount_raised + currency via IS NOT DISTINCT FROM instead of exact round_date — Diffbot shifts round dates ±1 day between pulls) plus a one-time dedupe-funding-rounds cleanup command.
  • Budget discipline documented (data-pipeline.md § Credit budget): ~25 credits per Enhance → ~10K enhances/month total; cycle resets ~the 4th, no rollover; currently exhausted until ~2026-08-03/04. Both recent cycles burned the full 250K in 1–2 unthrottled bulk days — new rule is ~2–2.5K enhances/week, and the enrich-all-founders job now caps each run at 2,000 founders by default (payload accepts a limit override) and reports the remaining backlog in its Slack summary.

July 2026 — Funnel-tracking regression found and fixed, testing + monitoring infra added

Digging into why Phase 13f's company-surface click-through looked terrible found the real story: the company page stopped rendering <BriefCard> on 2026-07-04 (a legitimate cleanup — see the Phase 17 entry below) and the replacement, GatedContent, shipped with zero analytics. The company page — the single highest-traffic surface — had no conversion-click visibility at all for 2 days; the 8,004 "impressions" in the original Phase 13f pass were dead-code telemetry, not a live rate.

  • GatedContent now fires gated_content_impression/gated_content_cta_click with surface/tracking_id properties, wired into both call sites (company-page funding gate, FoundersSection's founders gate).
  • Closed the attribution loop. The gate's from query param (e.g. company-funding-{slug}) was generated on every register link but never read anywhere. Now threaded into register_form_submitted and user_signed_up PostHog properties.
  • New checkFunnelHealth worker job (daily, 9am UTC): compares each brief_card_impression surface's volume against its own 14-day trailing baseline and alerts Slack if a surface that normally fires goes silent, plus cross-checks Postgres signups against user_signed_up events. Would have caught this regression the same day it shipped instead of via manual investigation two days later.
  • Added a test runner. No test framework existed in this repo before now — added vitest + @testing-library/react (npm run test, folded into npm run check), with unit tests for BriefCard and GatedContent's tracking behavior so a future refactor that silently drops analytics fails CI instead of going unnoticed.

See roadmap.md § Phase 13f for the full investigation and the corrected (still directional, not final) verdict.


July 2026 — Spam filter gap patch + admin moderation tooling

Follow-on to the June 2026 signup-quality overhaul. Investigation of live signups found a concrete post-cutover escape (moviesdownload021@gmail.com, "Movies clips", via Google OAuth on 2026-07-05) that the name/business-token filter didn't catch, plus two tooling gaps that made it hard to act on spam even when spotted.

  • lib/auth/spamFilter.ts gained a spam_keyword_name check. Catches content-farm / crypto-slang "names" (download, movies, chain, web3, nft, official, etc.) that don't contain a business-name token or unusual-TLD email suffix. Documented as reactive/whack-a-mole, not exhaustive.
  • /admin/signups provider column was silently broken. authProvider was derived from google_id, which the OAuth signup path (google-sync/route.ts) never populates — every Google signup showed as "email". Now derived from password === null.
  • Deactivation now actually works. users.deleted_at (previously unused for users) is now checked in handleLogin, resolveBearerToken (covers every bearer-token API route), and the Google OAuth re-auth path in google-sync/route.ts — a deactivated account can no longer mint a new session through any path. New POST /api/admin/users/[id]/deactivate route + a one-click "Deactivate" button on /admin/signups.
  • New cleanup-unverified-signups job. Daily (7am PST), soft-deletes credential-path signups that never verified email within 14 days. Hard-floored at the 2026-06-30 verification-gate cutover date — the 4,318 pre-cutover users who never verified are historical noise, not spam, and are explicitly excluded from this job.

July 2026 — Phase 17 remainder: close the last public funding-data leaks

Audited the roadmap's stated Phase 17 remainder ("BriefCard's LAST RAISED callout and the /funding-rounds pages") and found that framing itself was stale — no BriefCard instance renders that callout anywhere, and /funding-rounds is a static page with no live data. Found two real, unflagged gaps instead.

  • Company-page StatStrip "Last raised" figure gated. /companies/[slug]'s stat strip was showing the exact dollar amount above the fold, ungated, even after the funding-rounds section below it was gated the day before. buildCompanyCallouts() now takes a teaseFunding option that swaps the amount for "Sign in to view" while keeping the round name visible.
  • /api/companies/search and /api/funding-rounds/search now require premium server-side. Both returned raw funding amounts and investor names with zero auth check at the API level — the frontend only self-gated client-side on isPremium (SWR key was null otherwise), so the routes themselves were a live, unauthenticated scrape target. Both now call isPremiumUser() and return 401 otherwise, matching the frontend's own trust boundary. No legitimate caller was affected (verified no other code calls either route).
  • Phase 18 (dashboard redesign) confirmed shipped. Git archaeology found it had already landed — bundled into PR #332 under a misleading title — so the roadmap never reflected it until now. See below.

July 2026 — Phase 16: traffic-ranked profile drafting at scale

Scaling premium company profiles from 10 toward ~100, prioritized by PostHog traffic (roadmap.md § Phase 16).

  • Traffic-ranked target query. lib/reporting/trafficRankedTargets.ts ranks /companies/[slug] pages by PostHog page views, cross-referenced against publish/claim/Diffbot-cache status in Postgres. New CLI script (scripts/rank-companies-by-traffic.ts) plus an admin "Targets" page (/admin/premium-profiles) with a per-company "queue draft" action.
  • Drafting job now uses cached Diffbot data. draftPremiumProfile.ts previously used only Firecrawl + Claude; it now also reads the already-cached diffbot_companies.diffbot_response (customers, description, categories, investments — zero additional Diffbot cost) as corroborating context, and populates notable_customers for the first time (the field existed in the schema and on the public page, but nothing wrote to it before). Falls back to Diffbot-only drafting when a site fails to scrape but a cache exists, instead of failing the whole draft.
  • New batch job, human-review gate preserved. backfillPremiumProfiles.ts fans out drafts over the traffic-ranked list — it never auto-publishes (human review required). New admin "Review" page (/admin/premium-profiles/review) lists batch-drafted candidates (profile_source = enriched, unpublished) for a human skim-and-publish pass through the existing editor.
  • Fixed a real bug found along the way: company claims were never persisted. The Google OAuth claim flow (lib/auth/nextauth.ts) recomputed token.companySlug on every JWT tick but never wrote anything to the database — a stateless per-session grant, not a durable claim. Now upserts a users_companies row on a genuine sign-in match, so "is this profile claimed?" is finally a real, queryable fact. /companies/[slug] shows a tasteful unclaimed-profile banner (copy differs for thin vs. already-enriched profiles) instead of an always-on "Claim this profile" link that ignored actual claim status.
  • PR #333.

July 2026 — Company profile fixes: broken screenshots, redundant brief, access gating

A batch of fixes to /companies/[slug], mostly found via live QA on the OpenAI profile.

  • Website screenshot URLs fixed and backfilled. captureCompanyScreenshot.ts was building virtual-hosted-style S3 URLs (bucket.s3.region.amazonaws.com/...), which silently fail to load because the bucket name (s3-images.employbl.com) contains dots — the wrong URL style for that bucket. Switched to the path-style URL used everywhere else in the codebase and backfilled all 685 existing rows via a one-off SQL rewrite. Screenshot now renders on every company profile regardless of premium-publish status (previously gated behind published_at, which meant it never showed for any of the thousands of unpublished profiles).
  • Removed the redundant "Section I · The Brief" teaser. It duplicated the masthead (title/subtitle/stat callouts) verbatim on every company page and showed a generic "locked" premium-profile table of contents even for companies with no premium profile at all.
  • Founders and capital-raised sections now blurred for signed-out visitors. Same filter: blur(5px) convention as job listings elsewhere in the app, with a "Sign up to see..." CTA. Logged-in users see everything. (First slice of Phase 17 access-control gating.)
  • Removed the Waymo sample-profile section from the homepage. The static "lead brief" demo card and its unused content/component were cut.

July 2026 — Phase 15h/i/j: screenshot capture, competitive landscape, founder deep profiles + Phase 18 dashboard redesign

Three data-side follow-ons to the June profile refresh, plus a full dashboard redesign that got bundled into the same PR under a title that didn't mention it (see the Phase 17 entry above — the roadmap only caught up to this on 2026-07-05).

  • 15h — Website screenshot capture. New capture-company-screenshot BullMQ job (ScreenshotOne → S3 → premium_company_profiles.website_screenshot_url), an admin trigger button on the premium profile editor, throttled to stay under ScreenshotOne's 20 req/min account cap (a burst of jobs tripped the limit almost immediately on first rollout). --early-only flag added to the backfill script to prioritize Pre Seed/Seed/Series A companies per city.
  • 15i — Competitive landscape section. New competitive_context Json? field on premium_company_profiles, an admin CompetitiveLandscapeSection for authoring competitor rows (name/stage/funding/differentiator), rendered as a structured table in PremiumProfile Group E.
  • 15j — Founder deep profiles. /companies/[slug]'s Leadership section now sources from Prisma founders (bio, prior companies, education pills extracted from cached diffbot_response via lib/founders/extractDiffbotSummary.ts) instead of the Go API's name-and-headshot-only data. Falls back to ceo_name when a company has zero founder rows. New admin "Enrich founders" button fans out enrich-founder-diffbot jobs.
  • Phase 18 — dashboard declutter + AI Chat promotion. pages/dashboard/index.tsx's primary-action panel is now mutually exclusive (only one of preference card / onboarding / interests signal renders at a time). The old floating "Ask AI" button is gone, replaced by a prominent promo card with a tier-aware CTA; AI Chat moved from SideNavbar slot #7 to slot #2. Three stacked CompanyListCards collapsed into one tabbed discovery card; MCP card moved below the fold. Dashboard + SideNavbar chrome swapped from indigo-gradient to the same ink/paper/tangelo tokens the public redesign runs on.
  • PR #332.

July 2026 — Fixed stale company-rep sessions on Google account switch

The jwt callback in lib/auth/nextauth.ts only recomputed the signed-in company via findCompanyForEmail inside if (user?.email), which is only true on the very first sign-in. Switching Google accounts in the same browser (e.g. signing out of an employbl.com account and back in with a personal Gmail) left the old token.companySlug in the JWT, routing the new account to the previous company's dashboard. Now recomputes the match on every JWT tick and unconditionally resets the company fields when there's no match. PR #331.


July 2026 — Beehiiv env hardening

The June auto-subscribe-at-signup feature had been silently no-oping for two months: BEEHIIV_API_KEY/BEEHIIV_PUBLICATION_ID were never set in Vercel, and subscribeToNewsletter() short-circuits on missing keys without throwing. 617 of 856 opted-in users never landed in Beehiiv as a result. Both vars are now required in production (optional in dev, same defense as the earlier NEXTAUTH_SECRET fix), and the subscribe helper warns once per process to Sentry when short-circuiting in prod so the next regression doesn't hide for months. Backfill script pacing bumped 120ms → 300ms after the first run hit Beehiiv's write rate limit. PR #328.


July 2026 — Vercel cost reduction: block preview deploys on non-main branches

Follow-on to the June cost cuts. The prior git.deploymentEnabled: { "main": true, "*": false } block in vercel.json was a misleading no-op — Vercel's object form takes literal branch names, not glob patterns, so every non-docs commit on any PR branch still burned build minutes.

  • ignoreCommand now gates on VERCEL_GIT_COMMIT_REF. Any commit not on main short-circuits with exit 0 before the diff-filter, and Vercel records the deploy as CANCELED without spinning up build resources. main commits still respect the existing docs/scripts/worker/prisma/*.md/.github exclusion.
  • Dropped the dead deploymentEnabled block. The rule lives in the ignoreCommand where it can't drift from a dashboard toggle. PR #326.

July 2026 — MCP server v0.5: profile, preferences, recommendations, similar companies

The /api/mcp server picked up 9 new tools and a public documentation page. Server bumped 0.3.0 → 0.5.0 (16 tools total).

  • Profile management (4). get_profile, update_profile, add_work_experience, add_skills — read + update basic profile, work history, and skills over MCP. Selective-section fetch; Zod-validated input.
  • Preferences (2). get_preferences, update_preferences — read + partial-update users.user_interests (seniority, industries, stages, locations, comp floor).
  • Personalized recommendations (2). get_recommended_jobs, get_recommended_companies — Typesense-backed matching against the caller's preferences, optional reasoning mode, sort by recency / relevance / recent funding. Falls back to a helpful "set preferences first" message for users with an empty user_interests.
  • Similar companies (1). find_similar_companies — look up by name or domain, return the target company plus curated premium_company_profiles.similar_company_slugs. Useful for competitive discovery.
  • New /docs page. Full public catalog of all 16 tools with example queries, install steps for Claude Desktop, auth + rate-limit notes. Indexed for SEO and used as the onboarding target from the marketing surfaces.
  • Vercel build resilience. Landing this exposed a pre-existing OOM in next build's ESLint step (recommended-requiring-type-checking runs a full TS program per file → 7m12s to lint one file locally). Two fixes: a registerTool<TInput> helper in createServer.ts that erases ShapeOutput inference across the 16 tool calls, and --no-lint on the build command so ESLint runs only in dev / manual passes. Type-check still runs during next build. PR #325.

July 2026 — Data pipeline observability + Diffbot founders backfill

Two independent workstreams landed together, both targeting the Sunday Slack pages about SLA breaches and OpenAI failures.

  • Worker Sentry wiring. New worker/lib/sentry.ts (thin wrapper over @sentry/nextjs) called from worker/index.ts at boot. worker.on("failed") / worker.on("error") capture per-queue tags; fatal boot errors flush with a 2s deadline. getLocationFromOpenAI now Sentry.captureException(err) inside its catch — the ~40% openai_failed skips are no longer silent.
  • populate-job-listings no longer fails on one flaky board. Rewrote the parent rollup to only mark the run failed when either the fatality rate exceeds 25 % or the run produced zero real work. A single Ashby 429 or a deleted board now shows up as a partial-failure with the top 5 board errors surfaced in the Slack summary, rather than flipping the whole 24 h SLA red.
  • check-pipeline-health accepts productive-but-errored runs. SLA check now matches status='success' OR (status='error' AND inserted+updated>0). Ends the "SLA page every 2 h until the next daily cron" loop.
  • Diffbot founders backfill. Revives the founders pipeline (dead since May 2024). New backfill-founders-from-cache job reads diffbot_companies.diffbot_response.founders[] and upserts founders rows keyed by (company_id, diffbot_person_id) — zero credits, pure DB fan-out over cached data. Companion enrich-founder-diffbot calls Person Enhance (kg/v3/enhance?type=Person&refreshEntity=...) at 1 credit per founder, skipping rows fresher than 90 d. Wired into the admin scheduled-jobs page and /api/admin/trigger allowlist.
  • PR #324.

July 2026 — Profile-aware AI Chat + public-page conversion refinements

New standalone product: /chat — a profile-aware AI research assistant with access to Employbl's own data through 7 MCP tools. Built as a conversion + retention lever alongside further public-page polish.

  • /chat page. Full-page chat under AppLayout. 4-column starter-prompt grid, streaming SSE responses with a tool-status pulse indicator, upgrade CTA for non-premium users, resume-upload nudge for anyone without a parsed profile.
  • Profile-aware system prompt. lib/chat/candidateContext.ts assembles seniority, industries, target locations, skills, and comp floor from users + user_interests and injects it as a context block — the agent defaults to the candidate's preferences without asking.
  • All 7 MCP tools available in-session. findCompanies, searchJobs, getCompany, summarizeCompany, personalizeSearch, getJobListing, recentFundingRounds. Shared useChatSession hook powers both /chat and the in-app ChatPanel.
  • Cost model corrected. Credit metering now bills Sonnet ($3/MTok in, $15/MTok out) instead of Haiku pricing — Sonnet is the actual model in use.
  • AI Chat in the nav. New SideNavbar entry; replaced Application Tracker in BottomMobileNavigation. Copy added to pricing feature list, register VALUE_PROPS, and the homepage AI section.
  • /companies/[slug] locked-content refinement. The black-bar redacted section is now a clean numbered section-name list + "Locked" indicator. Anon visitors see only 5 jobs with blur on jobs 4-5 and a "View all N roles →" CTA to /register.

June 2026 — Signup quality overhaul (spam floor + verification gate + LinkedIn upfront + domain-match)

The register funnel had three problems on one form: too much spam getting in, a bare experience for the real candidates who did make it, and no fork for people signing up with their work email. Shipped as one PR because the phases are load-bearing on each other (verification gates the trial that the spam accounts were consuming; LinkedIn feeds the enrichment that runs after verification; domain-match branches both the redirect and the verification-email copy). Baseline before ship: ~6% real-candidate rate on 49 signups/30d.

Spam floor

  • New blocked_registrations table. Every rejection is logged with email, name, reason, and IP so we can tune the ruleset from data instead of anecdote.
  • lib/auth/spamFilter.ts runs on both signup paths. Rejects business-name-in-name-field (SFI Insurance, Genre Sports), digits/URLs in name, role-email addresses (marketing@…, admin@…) on business domains, and .seo/.co/.aus/.us/.ltd suffix locals like nconturbines.seo@gmail.com. Runs in both handleRegister (email/password) and google-sync/route.ts (Google OAuth) — the OAuth path was letting the same spam through.

Verification-gated trial

  • Verification email actually sends now. worker/processors/sendVerificationEmail.ts — SES-based BullMQ processor on the notifications queue, both candidate and domain-matched copy variants, logged via logSentEmail. Enqueued from register + from the auth-gated resend endpoint.
  • trial_ends_at moved from register to verify-email success. Spam accounts that never verify never consume trial state, and /admin "active trials" reflects real intent. Same 7d window still applies.
  • Full-page verify wall on /dashboard. New VerifyEmailWall component replaces the whole dashboard for unverified users with a resend button. Public /jobs search stays open.
  • Diagnosis note on Google OAuth. The plan claimed "0 Google signups in 30d." Actually 7 happened in June — the regression was already resolved before this PR started. Left the audit note so future sessions don't chase the same ghost.

LinkedIn upfront + PDL enrichment

  • Optional LinkedIn URL field on the register form. linkedin.com/in/… pattern-validated in both react-hook-form and the Zod register schema, persisted to users.linkedin_url, threaded through signUp()register()handleRegister.
  • enrich-new-user BullMQ processor. Runs on first successful verification. Prefers LinkedIn URL as the query key (email hit-rate is much lower on the PDL free tier), best-effort — misses and gated-field responses are silent. Writes results to user_interests + candidate_skills via the shared applyEnrichment helper in lib/pdl/applyEnrichment.ts.
  • PDL wrapper bug fix. lib/pdl/enrich.ts was calling data.location_locality?.trim() — but PDL returns literal true on gated-tier fields, which threw TypeError. New pickString() guard applied to every consumed string field. All response fields now typed unknown at the API boundary so the guard is the only way through.
  • Manual PDL "enrich my profile" card removed. With enrichment now running automatically post-verify, the dashboard's ProfileEnrichCard (and its POST /api/auth/enrich-profile backing route) were the manual escape hatch nobody needs anymore. Deleted both. PreferenceCard remains as the empty-state fallback for users whose enrichment missed or who signed up via Google OAuth.

Company-email fork

  • findCompanyForEmail extracted from lib/auth/nextauth.ts into lib/companies/emailDomainMatch.ts so both NextAuth (Google reps) and handleRegister (email/password) share one implementation of the domain→company match.
  • Domain-match at register. handleRegister calls findCompanyForEmail before user create, inserts a users_companies row on match, returns companyMatch: { slug, name } in the response. Frontend reads it and redirects to /companies/[slug]/claim instead of /dashboard.
  • Verification email copy branches. Candidates get "Verify your email to start your Employbl trial." Domain-matched users get "Verify to manage {CompanyName} on Employbl" with a link to their company dashboard.
  • /api/auth/detect-company blur-detect endpoint. Register-form email onBlur pings this; if it resolves, a nudge banner appears: "Looks like you work at {CompanyName}. Signing up will link you to their company page." Non-blocking — they can still proceed with password.

Migration docs/sql/2026-06-30-blocked-registrations.sql was applied to prod via psql before this PR opened; the Prisma schema is in sync.


June 2026 — Search/filter consolidation across /jobs, /companies, and global nav

Four UX issues from the live signed-in product, fixed together.

  • /companies skeleton no longer gets stuck. RecommendedCompaniesSection was rendering a 6-cell loading skeleton forever when the /auth/recommended-companies fetch errored — SWR returns data === undefined for both loading and error states, and the component only checked the former. Now treats error as an empty result so the section disappears cleanly instead of bleeding gray boxes across the top of the page.
  • /jobs keyword search consolidated into the Filters drawer. The standalone "Search Tech Jobs" form (big orange Search button at the top of every results page) was pulling eyes away from the structured filters. Candidates come to the job board to discover roles, not to look up a known title. The keyword search is now a "Keyword" text input at the top of the Filters drawer on both desktop (JobSearchFilters) and mobile (MobileFilters), binding to query.q via the same replaceInQuery pattern other filters use (commits on blur or Enter). The existing removable "Keyword(s): … ×" pill still works.
  • Job Locations filter now shows clean USA cities only. The autocomplete was sourcing from job_listings_locations.city which contains dirty rows like "33 New Montgomery, 1100, San Francisco, CA" and "Babcock Hills Veterinary Hospital - San Antonio, TX". Regex/length filters caught some but not all of the garbage ("Bay Area", "California", "Nyc" slip through any cleaning rule). Replaced the SQL query with a curated static list of ~120 top USA cities (lib/server/us-job-cities.ts) sourced from the actual job-count ranking. The canonical search "Series A + San Francisco + Software Engineer" composes correctly: /jobs?q=software+engineer&raiseround=Series+A&job_locations=san+francisco_CA_US.
  • Global header search bar removed. <GlobalSearchAutocomplete /> was rendering in every authenticated page header — redundant with page-level filters. Removed from LegacyNavbar and deleted the orphaned component file. Each page now shows just logo + premium badge + avatar in the header chrome.

June 2026 — Homepage production cleanup + MCP positioning + locations bug fix

Three production-quality fixes to the public site, shipped together.

  • Homepage placeholders removed. The hero "PRODUCT SCREENSHOT — TO BE ADDED" box and the "DOSSIER SCROLL MOCK-UP — TO BE ADDED" box were shipping to prod as bordered empty <div>s. Both are now commented out with TODO markers so the layout intent is preserved for whoever drops the real assets in.
  • "Dossier" → "company profile" everywhere. The word "dossier" was doing too much work as the central product noun (12 visible sites). All of them now read "profile" / "company profile" — WHAT'S IN A PROFILE, INSIDE A PROFILE, Browse all 25,000+ profiles →, etc. STAT_ITEMS and INCLUDED updated to match. JSDoc cleanup in Masthead.tsx and StatStrip.tsx.
  • Hero repositioned as data platform + newsletter. Masthead kicker is now EMPLOYBL · COMPANY INTELLIGENCE PLATFORM + WEEKLY NEWSLETTER; subtitle names the web, API, and MCP surfaces plus the Saturday newsletter explicitly.
  • MCP server now featured. New HairlineSection between Geographic Discovery and the stats strip — FOR AI AGENTS AND DEVELOPERS · Bring Employbl into Claude. with the install command and a link to a new /mcp landing page.
  • /mcp landing page. New Pages-Router static page with the install command (OAuth + bearer alternative), the full 7-tool list with descriptions sourced from lib/mcp/createServer.ts, auth/rate-limit/audit notes, and CTAs to /subscribe and /profile/mcp.
  • Shared install command. Extracted MCP_OAUTH_COMMAND, MCP_URL, mcpBearerCommand into lib/mcp/install.ts so the homepage section, /mcp page, and the authenticated McpServerCard.tsx stay in sync.
  • /company-locations/[slug] zero-results bug fixed. All city-state slugs on the homepage (miami-fl, denver-co, austin-tx, seattle-wa, etc.) were rendering empty tables because the slug parser in app/api/_lib/companyCollections.ts blindly replaced dashes with spaces, turning "miami-fl" into "miami fl" which matched no rows in the DB. New locationSlugToCity() helper strips a trailing 2-letter US state code (checked against a fixed set so cities like "Santa Fe" don't get truncated) before converting remaining dashes to spaces. Miami's page now lists its 357 HQ-in-Miami companies.
  • BottomCta simplified. The "Stop guessing. Start knowing." trial-CTA block was removed (redundant with the masthead and pricing-card CTAs). The Beehiiv newsletter signup is retained.

June 2026 — Company profile refresh (Contrary-inspired) + similar-companies section

Full editorial rewrite of /companies/[slug] and its premium profile, in the visual language of Contrary's portfolio pages — quieter, more editorial, more data-dense.

  • Sticky table of contents. Long premium profiles get a right-rail TOC that highlights the active section as you scroll.
  • New founders section. Names + headshots pulled from the Go API, rendered as a proper hero block instead of an inline text mention.
  • Editorial PremiumProfile. Grouped narrative sections (product, tech & business, culture, hiring) replace the old flat card stack. Reviews removed — the signal wasn't there.
  • 15g — Similar companies. New similar_company_slugs String[] field on premium_company_profiles (migration applied); horizontal-scroll cards of logo + name at the bottom of the profile. Populated manually during profile authoring for now; auto-population via industry-tag cosine similarity is future work.
  • Phase 15h/i/j (website screenshot, competitive landscape, founder deep profiles) shipped 2026-07-03 — see the July 2026 entry above.

June 2026 — Resume-parse onboarding (Phase 9)

Prefill the candidate profile from a resume upload instead of making everyone pick pills by hand.

  • Resume upload on /profile (also surfaced inside PreferenceModal on first run).
  • Anthropic parse. Extracts { skills, seniority, cities, compMin?, summary } from PDF / DOCX text.
  • Diff-and-confirm UI. Extracted fields shown side-by-side with current values; user accepts or edits before saving.
  • Writes flow into the scoring loop. Skills persist to candidate_skills; the rest into user_interests — drops straight into jobs-for-you scoring on the next request.
  • Out of scope, by design. No resume review, grading, or AI feedback. The point is to fill the profile, not judge it.

June 2026 — Admin conversion intelligence (Phase 14)

/admin had aggregate counts but no people. Every conversion question ("who churned?", "which signups never came back?") was a one-off SQL query. Five new admin sub-pages, each answering a specific funnel question at a glance.

  • /admin/signups (14a). Last 25 users with auth provider (Google vs email), trial-status chip, subscription check, saved-jobs count. Postgres-backed.
  • /admin/funnel (14b). Live PostHog funnel — brief impressions → register submit → signed up → subscription created. 5-minute in-memory cache on /api/admin/funnel.
  • /admin/cohorts (14c). Weekly cohort retention table (W+0/1/2/4/8). "Active" = saved a job or search in that window. Color-coded cells.
  • /admin/subscriptions (14d). MRR waterfall (new / churned / net) plus a list of churned subs in the last 30d with Stripe cancel reason.
  • /admin/subscribers (14e). Sortable table of every active subscriber: email, start date, days-to-paid, MRR, cumulative spend. Joins Stripe → Postgres.
  • Housekeeping. Extracted CommandRunsTable to a component and brought pages/admin/index.tsx back under the 500-line ESLint cap.

June 2026 — MCP OAuth 2.1 + Streamable HTTP transport

The /api/mcp server got two upgrades so third-party MCP clients (Claude Desktop, Cursor, etc.) can connect via standard OAuth instead of a hand-copied bearer.

  • OAuth 2.1 PKCE server. New /api/mcp/oauth/** routes implement authorization, token, and revocation with PKCE.
  • /.well-known/oauth-protected-resource route. Standard discovery endpoint so MCP clients can auto-configure. Required a firewall bypass for .well-known/*.
  • Streamable HTTP transport. /api/mcp supports the newer Streamable HTTP flow alongside the legacy SSE transport; also required a firewall bypass.
  • UX and security follow-ups. Simplified the /profile/mcp install flow (PR #317) and patched a set of OAuth 2.1 spec-compliance gaps in the same pass.
  • /mcp marketing landing page. Public-facing page with the install command (OAuth + bearer alternative), the full 7-tool catalog with descriptions sourced from lib/mcp/createServer.ts, auth/rate-limit/audit notes, and CTAs to /subscribe and /profile/mcp.

June 2026 — Public landing redesign: "The Dossier"

We rebuilt every high-traffic anonymous landing surface to convert on the first pageview. Sessions average 1.13 pageviews — the offer has to land immediately.

  • BriefCard component. A reusable editorial-brief module that appears above the fold on every public surface. Warm paper background, Fraunces serif headline, IBM Plex Mono data callouts, redacted "Section II — Restricted" reveals for bots and AI assistants to index (labels visible to search engines, bars visible to humans), tangelo CTA.
  • City pages (/company-locations/[slug]) — previously had no above-fold CTA. BriefCard now appears above the company table with callouts for company count, open roles, top investor, and hottest employer.
  • Company pages (/companies/[slug]) — CompanyUpsellHero replaced by BriefCard with founded, headcount, open roles, and last funding round callouts.
  • Job pages (/jobs/[slug]) — 200–500 anonymous views per top job, previously no above-fold offer. BriefCard now appears above JobHeader for unauthenticated visitors, with location, company size, posted date, and ATS source callouts. Six redacted sections (comp band, equity, interview rubric, hiring manager, team context, offer timeline).
  • Company collections (/company-collections/[slug]) — BriefCard added above the company grid.
  • New editorial nav. MarketingNav replaces LegacyNavbar on public pages — a SaaS-style top nav with Companies and Tools dropdowns, no search form for anonymous users (search is a premium feature).
  • Homepage rewrite. Dead search bars removed, new Hero component, trust stats bar, "Filter by what actually matters to you" section.
  • Feature landing pages. /job-application-tracker, /profile, /funding-rounds now have proper marketing landing pages with conversion CTAs.
  • Always sidebar when logged in. MarketingLayout proxies to AppLayout for authenticated users — all 36+ public pages automatically show the sidebar once you're signed in.
  • PostHog telemetry. brief_card_impression, brief_card_cta_click, brief_reveal_hover events fire on every BriefCard surface.

June 2026 — Company logo backfill

We now have logos for 30,498 of 30,709 companies (99.3% coverage).

  • Previous coverage was incomplete — Clearbit (acquired by HubSpot) shut down its logo API and Diffbot proxy URLs had expired.
  • Google faviconV2 API (t0.gstatic.com/faviconV2) used as the primary source, with a size filter (>800 bytes) to reject generic fallback icons.
  • Direct /favicon.ico fetch added as a first-pass strategy for newer companies that Google hasn't indexed yet — picked up an additional 17 logos that the Google API missed.
  • S3 pipeline. Logos uploaded to s3-images.employbl.com/company-images/{slug}-logo.png, image_url_s3 and image_url updated in the companies table.
  • 211 companies remain without logos — all are either very new startups or sites without accessible favicons. Script is idempotent and can be re-run as coverage improves.
  • Script lives at scripts/backfill-company-logos.ts.

June 2026 — Auto-subscribe to the newsletter at signup

We wired the "subscribe to the newsletter" checkbox on /register directly into Beehiiv. Before this, the checkbox just set a flag in our database; growing the list meant manually exporting CSVs and re-importing them — which is why ~850 opted-in candidates were sitting on the wrong side of the wall.

  • New signups go straight into the publication. Opted-in registrations POST to Beehiiv's v2 subscriptions API at signup time. Fire-and-forget — registration never blocks on Beehiiv being down.
  • Backfill script for the historical opt-ins (scripts/backfill-beehiiv-subscribers.ts). Dry-runs by default; refuses to apply past the Launch plan's 2,500 cap.
  • No change for Google OAuth signups — we never captured an explicit newsletter opt-in on that path, so they're not enrolled.

June 2026 — Maps: see where AI startups are hiring

We launched a full Mapbox-powered map of where the AI startup ecosystem is hiring, end to end.

  • New /map page. A single interactive map you can pan across the US. Toggle between companies and an open-jobs heatmap, filter by metro, and click any pin for the company or role. Search inside the map to jump to a company by name.
  • City landing pages. Dedicated pages for the nine biggest tech metros — NYC, SF, LA, Seattle, Chicago, Austin, Miami, Denver, San Diego — each with live company counts, top hiring companies, and the neighborhoods they cluster in. Great for browsing, great for search engines.
  • Mini-maps on every company and job page. Premium and trial members now see an embedded map on /companies/[slug] and /jobs/[slug] showing HQ, branch offices, and on-site role locations — no more squinting at an address.
  • Search & filters synced to the URL. Send a teammate a link to a filtered view of the map and they'll see exactly what you see.
  • Backend. 29K+ company locations and 45K+ job locations now have precise lat/lng coordinates, powered by PostGIS and a US cities lookup table.
  • Retired the old Leaflet map. /companies/map and the state-level maps now redirect to the new /map.

June 2026 — Reverse trial + marketing site overhaul

We rebuilt the path from "landed on Employbl" to "paying subscriber."

  • 7-day free trial, no credit card. New accounts get full premium access for seven days with no payment up front. A trial countdown banner lives on the dashboard, with an urgency nudge in the final 48 hours.
  • Welcome email drip. A 5-day onboarding email sequence walks new candidates through the features they're most likely to convert on.
  • New homepage hero. Sharper positioning ("Get the inside track on AI startup hiring"), a showcase of premium company profiles, and a single "Start free trial" CTA.
  • Rewritten pricing page. Accurate feature list, FAQ, and a real explanation of the $25/mo value prop.
  • Redesigned register page. Two-column layout with the value props and testimonials right next to the signup form.
  • Trial-end /subscribe page. When a trial ends, the new /subscribe page recaps what the user actually built — skills added, companies watchlisted, jobs saved, applications tracked, searches saved — alongside a 7-day refund guarantee, testimonial, and FAQ.
  • Sticky conversion bars on company, job, and blog pages prompt anon visitors to register.
  • Pricing link in the footer and an upgrade CTA in the sidebar for expired-trial users.

June 2026 — Smarter onboarding and personalization

A first-time experience that actually learns what you're looking for.

  • First-run preferences modal. New users pick seniority, target compensation, and locations on day one. Those preferences feed directly into the "For You" job feed.
  • Editable preferences on your profile. Update seniority, comp expectations, target cities, and skills any time.
  • Profile prefill from your resume context. New accounts get skills and seniority prefilled from People Data Labs where possible, so the profile isn't blank from minute one.
  • Better "For You" scoring. Seniority and salary now count toward how a job is ranked, not just skills and location.
  • Cleaner dashboard. Featured companies always show a logo. The onboarding cards collapse once you've completed them. The "personalize your job feed" prompt disappears once you have at least two skills.

June 2026 — Companies: claim your profile

Recruiters and founders can now own and edit their company's Employbl profile.

  • Sign in with Google. Companies log in with their work Google account; we use the email domain to verify they belong to the company.
  • Claim flow. /companies/[slug]/claim/companies/[slug]/dashboard/companies/[slug]/edit.
  • Company dashboard. See PostHog page views and unique visitors for your profile over the last 30 days, your open job count, and a profile completion bar.
  • Self-serve editor. Edit your company's description, tags, locations, and other profile fields directly — no more emailing us to fix typos.
  • Candidate Google login. Job seekers can now sign in with Google too.

June 2026 — Editorial blog redesign

We rebuilt the blog as a real publication, not a knowledge-base sidebar.

  • Editorial homepage. New /blog index with hero post, secondary features, and a clean grid of recent posts.
  • Redesigned post pages. Typography, layout, and inline-CTA system rebuilt from scratch.
  • AI-generated post images. Every published post now gets a unique header image, generated via gpt-image-1.
  • Better link rendering. Inline links inside post bodies now render properly.
  • Premium company profile upgrade. The ten flagship premium profiles (Waymo, Sierra, Hightouch, Vestwell, Decagon, ElevenLabs, ClickHouse, Vanta, Ramp, Verkada) got a content and layout refresh.

June 2026 — Faster, more reliable search

A multi-week migration to Typesense for instant search across our 25K+ companies and 61K+ jobs.

  • Typesense-powered search for companies, jobs, and funding rounds with sub-100ms results.
  • In-map search. Find a company by name from inside the /map page and the camera jumps to it.
  • Funding rounds search trimmed to the most recent 5 years, dramatically faster and no more out-of-memory crashes.

June 2026 — New background job system

We migrated all of our background jobs (job board sync, company aggregates, email sends, enrichment) off Trigger.dev and onto a self-hosted BullMQ + Redis stack on Railway. End users see this as: fewer stalled syncs, faster recovery from failures, and a much shorter feedback loop on data issues.

  • New /admin/pipeline page. A live operational view of every queue, every recent run, every worker — with one-click "run now" buttons.
  • Daily job sync now chains into company aggregates. Funding totals, headcount, and other aggregates refresh automatically the moment a sync completes.
  • Hardened the data pipeline. Better handling of missing Ashby boards, smarter fan-out, automatic zombie cleanup.

June 2026 — Vercel cost reductions

Several rounds of infrastructure cleanup to keep hosting costs sustainable.

  • Killed the /jobs/search/* route that was burning a disproportionate share of the bill.
  • Tightened ISR settings on programmatic SEO pages.
  • Disabled per-branch preview deploys to save on build minutes.
  • Stopped polling /api/auth/session on anonymous traffic.
  • Per-route function execution caps.
  • Server-side auth cookies (previously set client-side, which doubled some requests).

Not user-visible, but it's why we can keep the lights on.


Earlier in 2026

For older work — Phase 1–5 (admin tools, premium profiles, candidate auth, jobs-for-you scoring, content ops) — see git history.