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 (
sectoradded tofacet_by; the search API now returnsfacets). "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:
useCompaniesSearchnever forwardedsector,founded_after, orfounded_beforeto 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 inlib/industries/industryTaxonomy.ts) +company_industriestables;/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-industriesjob 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) +subsectorfields;/companiesIndustries filter now sector-grouped taxonomy names (sectorparam; legacyindtag-id URLs still resolve); MCPfind_companiessectorparam (server v0.8.2,/docsupdated); new/industries+/industries/[slug]SEO pages (stats, subsectors, who's-here grid, adjacent subsectors); industry chips on company profiles. - 26d:
populate-company-tagsno longer writesindustryor legacycompany-tech-stackSpatie tags (investors still sync). Existing taggings stay read-only;/company-collectionsunaffected.
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_clickedPostHog events on both CTAs withsource: "homepage".
Search + MCP (21e):
/companiestech-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_companiesadvertises job-derived stack matching (server v0.8.1,/docsupdated).
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 inPremiumProfile): 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", norevalidateper the Vercel cost rules./tech-stacksindex rewired from legacy company-collection tag links to the curated technology pages, grouped by category with live job counts.skills.descriptionbackfilled with LLM one-liners (264 technologies) viascripts/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-stackworker job (daily 8:15am UTC, after extraction), full-rebuild idempotent, clears stale stacks; admin Run Now + CLI script.- Typesense
tech_stackfilter 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_stackcolumn 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_skillsrows across 45,683 active listings (51%) covering 2,560 companies, confidence-scored by mention count. extract-job-skills-scheduledworker job (pipeline queue, daily 7:15am UTC after the job-listings sync) keeps new listings extracted incrementally via the newjob_listings.skills_extracted_atstamp; admin Run Now wired. CLI:scripts/extract-job-skills.ts(--forcerescans 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 byscripts/curate-technology-taxonomy.ts: candidates gathered fromjob_skillsfrequency + the 1,804 draftedpremium_company_profiles.tech_stackarrays + Spatiecompany-tech-stackstags, LLM-classified (strict: no soft skills, methodologies, protocols, or generic categories), then upserted onto canonicalskillsrows. Classifications cache to.cache/so re-runs are free; script is idempotent and skips already-reviewed technologies. - New
skills.slug+skills.logo_slugcolumns (docs/sql/2026-07-10-skills-technology-taxonomy.sql). Slugs are unique (will power/tech-stacks/[slug]in Phase 21c); logos resolve deterministically against thesimple-iconspackage (154/311 matched; server-side only — seelib/skills/simpleIconMatch.ts). - Alias clusters folded into canonical spellings —
normalized_namevariants like "gcp"/"Google Cloud Platform", "Postgres"/"postgresql", and case-dupes now all group under one canonical name, with the variants preserved inaliases(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.
AppLayoutno longer rendersLegacyNavbar. The newAppSidebaris 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-searcheswas 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
localStorageacross 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:
/chatand 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-countsendpoint (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_listingsrows. Now: title/company/URL snapshotted intoexternal_*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. AppPageHeaderstandardizes the slim title + primary-action row (dashboard, saved-searches pages).
Dashboard command center (same PR):
- Setup-first dashboard. New
SetupPanelowns 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 MCPupload_profile_from_textpipeline, refactored into a sharedapplyProfileTextcore — 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/dashboardplus 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_salarycame back from the search API but were never rendered — now a green💵 $140K – $180Kpill. - Save-job button fixed. It gated on
isPremiumwith 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 2025pill. - 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=skillsdeep-link opensAddEditSkillsModaldirectly (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 viareact-markdown+remark-gfmthrough a sharedChatMarkdowncomponent (typography-plugin styling; internal links go throughnext/linkwithprefetch={false}), used by both/chatand the slide-inChatPanel. - "Investors isn't a filterable field" — it is. The chat's tool schemas in
lib/chat/tools.tshad drifted from the MCP definitions: noinvestorsparam, so Claude told users the database couldn't do it and padded answers from training data. Addedinvestors(with full-firm-name guidance: "Andreessen Horowitz", not "a16z" — verified against the tags table),min/max_employeesto both search tools, andmin_salary+posted_within_daystosearch_jobs. System prompt now states investor filtering exists and forbids padding results from training data. - Answers cut off mid-sentence. Per-round
max_tokensraised 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 tonotable_investors+funding_narrative(funding intelligence — the rep editor now explains why instead of showing the inputs). All rep writes stampprofile_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) andupdate_company_profile(descriptive fields, partial updates) — gated by a newrequireCompanyClaimguard: activeusers_companiesclaim 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 existingmetadata.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. NewisCompanyPremium()helper mirrorsisPremiumUser()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-companiesmarketing 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 MCPverifyToken, 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_companyshow seat usage;/docsexplains 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 alongsidepages/for-companies.tsx($99 self-serve) —next buildpicked 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-signupBullMQ job (Claude Haiku) on every self-created company; creation is never blocked — flagged signups go to Slack +/admin/company-signupsreview 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-signupruns 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 overcompanies+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 samesaved_job_searchesrows (and the same query-JSON shape) the website and the notification worker already use, so MCP-created alerts ride the existing daily/weeklysend-job-notificationsschedule 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):
/docsToolCards + new "Saved Searches & Alerts" section + v0.7.0 history entry,/mcpcatalog,/features/mcpstats.
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; optionalradius_milesandhiring_onlyfilters. 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 likesummarize_company, BYOK-aware) → applies via the existingupdate_profile/add_work_experience/add_skillshandlers 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:
/docsToolCards + count (16→18) + v0.6.0 version-history entry;/features/mcpstats. Also fixed drift found during the audit:/mcpandlib/mcp/tools.tsstill 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-diffbotis primary again, with a dedup-key fix landing inemploybl-go-cli(match onamount_raised+currencyviaIS NOT DISTINCT FROMinstead of exactround_date— Diffbot shifts round dates ±1 day between pulls) plus a one-timededupe-funding-roundscleanup 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 theenrich-all-foundersjob now caps each run at 2,000 founders by default (payload accepts alimitoverride) 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.
GatedContentnow firesgated_content_impression/gated_content_cta_clickwithsurface/tracking_idproperties, wired into both call sites (company-page funding gate,FoundersSection's founders gate).- Closed the attribution loop. The gate's
fromquery param (e.g.company-funding-{slug}) was generated on every register link but never read anywhere. Now threaded intoregister_form_submittedanduser_signed_upPostHog properties. - New
checkFunnelHealthworker job (daily, 9am UTC): compares eachbrief_card_impressionsurface'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 againstuser_signed_upevents. 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 intonpm run check), with unit tests forBriefCardandGatedContent'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.tsgained aspam_keyword_namecheck. 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/signupsprovider column was silently broken.authProviderwas derived fromgoogle_id, which the OAuth signup path (google-sync/route.ts) never populates — every Google signup showed as"email". Now derived frompassword === null.- Deactivation now actually works.
users.deleted_at(previously unused for users) is now checked inhandleLogin,resolveBearerToken(covers every bearer-token API route), and the Google OAuth re-auth path ingoogle-sync/route.ts— a deactivated account can no longer mint a new session through any path. NewPOST /api/admin/users/[id]/deactivateroute + a one-click "Deactivate" button on/admin/signups. - New
cleanup-unverified-signupsjob. 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 ateaseFundingoption that swaps the amount for "Sign in to view" while keeping the round name visible. /api/companies/searchand/api/funding-rounds/searchnow 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 onisPremium(SWR key wasnullotherwise), so the routes themselves were a live, unauthenticated scrape target. Both now callisPremiumUser()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.tsranks/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.tspreviously used only Firecrawl + Claude; it now also reads the already-cacheddiffbot_companies.diffbot_response(customers, description, categories, investments — zero additional Diffbot cost) as corroborating context, and populatesnotable_customersfor 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.tsfans 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) recomputedtoken.companySlugon every JWT tick but never wrote anything to the database — a stateless per-session grant, not a durable claim. Now upserts ausers_companiesrow 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.tswas 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 behindpublished_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-screenshotBullMQ 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-onlyflag added to the backfill script to prioritize Pre Seed/Seed/Series A companies per city. - 15i — Competitive landscape section. New
competitive_context Json?field onpremium_company_profiles, an adminCompetitiveLandscapeSectionfor authoring competitor rows (name/stage/funding/differentiator), rendered as a structured table inPremiumProfileGroup E. - 15j — Founder deep profiles.
/companies/[slug]'s Leadership section now sources from Prismafounders(bio, prior companies, education pills extracted from cacheddiffbot_responsevialib/founders/extractDiffbotSummary.ts) instead of the Go API's name-and-headshot-only data. Falls back toceo_namewhen a company has zero founder rows. New admin "Enrich founders" button fans outenrich-founder-diffbotjobs. - 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 fromSideNavbarslot #7 to slot #2. Three stackedCompanyListCards collapsed into one tabbed discovery card; MCP card moved below the fold. Dashboard +SideNavbarchrome 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.
ignoreCommandnow gates onVERCEL_GIT_COMMIT_REF. Any commit not onmainshort-circuits withexit 0before the diff-filter, and Vercel records the deploy as CANCELED without spinning up build resources.maincommits still respect the existing docs/scripts/worker/prisma/*.md/.github exclusion.- Dropped the dead
deploymentEnabledblock. 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-updateusers.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 emptyuser_interests. - Similar companies (1).
find_similar_companies— look up by name or domain, return the target company plus curatedpremium_company_profiles.similar_company_slugs. Useful for competitive discovery. - New
/docspage. 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-checkingruns a full TS program per file → 7m12s to lint one file locally). Two fixes: aregisterTool<TInput>helper increateServer.tsthat erasesShapeOutputinference across the 16 tool calls, and--no-linton the build command so ESLint runs only in dev / manual passes. Type-check still runs duringnext 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 fromworker/index.tsat boot.worker.on("failed")/worker.on("error")capture per-queue tags; fatal boot errors flush with a 2s deadline.getLocationFromOpenAInowSentry.captureException(err)inside its catch — the ~40% openai_failed skips are no longer silent. populate-job-listingsno 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-healthaccepts productive-but-errored runs. SLA check now matchesstatus='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-cachejob readsdiffbot_companies.diffbot_response.founders[]and upsertsfoundersrows keyed by(company_id, diffbot_person_id)— zero credits, pure DB fan-out over cached data. Companionenrich-founder-diffbotcalls 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/triggerallowlist. - 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.
/chatpage. 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.tsassembles seniority, industries, target locations, skills, and comp floor fromusers+user_interestsand 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. ShareduseChatSessionhook powers both/chatand the in-appChatPanel. - 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
SideNavbarentry; replaced Application Tracker inBottomMobileNavigation. Copy added to pricing feature list, registerVALUE_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_registrationstable. Every rejection is logged with email, name, reason, and IP so we can tune the ruleset from data instead of anecdote. lib/auth/spamFilter.tsruns 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/.ltdsuffix locals likenconturbines.seo@gmail.com. Runs in bothhandleRegister(email/password) andgoogle-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 thenotificationsqueue, both candidate and domain-matched copy variants, logged vialogSentEmail. Enqueued from register + from the auth-gated resend endpoint. trial_ends_atmoved 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. NewVerifyEmailWallcomponent replaces the whole dashboard for unverified users with a resend button. Public/jobssearch 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 tousers.linkedin_url, threaded throughsignUp()→register()→handleRegister. enrich-new-userBullMQ 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 touser_interests+candidate_skillsvia the sharedapplyEnrichmenthelper inlib/pdl/applyEnrichment.ts.- PDL wrapper bug fix.
lib/pdl/enrich.tswas callingdata.location_locality?.trim()— but PDL returns literaltrueon gated-tier fields, which threwTypeError. NewpickString()guard applied to every consumed string field. All response fields now typedunknownat 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 itsPOST /api/auth/enrich-profilebacking route) were the manual escape hatch nobody needs anymore. Deleted both.PreferenceCardremains as the empty-state fallback for users whose enrichment missed or who signed up via Google OAuth.
Company-email fork
findCompanyForEmailextracted fromlib/auth/nextauth.tsintolib/companies/emailDomainMatch.tsso both NextAuth (Google reps) andhandleRegister(email/password) share one implementation of the domain→company match.- Domain-match at register.
handleRegistercallsfindCompanyForEmailbefore user create, inserts ausers_companiesrow on match, returnscompanyMatch: { slug, name }in the response. Frontend reads it and redirects to/companies/[slug]/claiminstead 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-companyblur-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.
/companiesskeleton no longer gets stuck.RecommendedCompaniesSectionwas rendering a 6-cell loading skeleton forever when the/auth/recommended-companiesfetch errored — SWR returnsdata === undefinedfor both loading and error states, and the component only checked the former. Now treatserroras an empty result so the section disappears cleanly instead of bleeding gray boxes across the top of the page./jobskeyword 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 toquery.qvia the samereplaceInQuerypattern 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.citywhich 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 fromLegacyNavbarand 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 withTODOmarkers 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_ITEMSandINCLUDEDupdated to match. JSDoc cleanup inMasthead.tsxandStatStrip.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
HairlineSectionbetween 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/mcplanding page. /mcplanding page. New Pages-Router static page with the install command (OAuth + bearer alternative), the full 7-tool list with descriptions sourced fromlib/mcp/createServer.ts, auth/rate-limit/audit notes, and CTAs to/subscribeand/profile/mcp.- Shared install command. Extracted
MCP_OAUTH_COMMAND,MCP_URL,mcpBearerCommandintolib/mcp/install.tsso the homepage section,/mcppage, and the authenticatedMcpServerCard.tsxstay 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 inapp/api/_lib/companyCollections.tsblindly replaced dashes with spaces, turning"miami-fl"into"miami fl"which matched no rows in the DB. NewlocationSlugToCity()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.BottomCtasimplified. 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 onpremium_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 insidePreferenceModalon 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 intouser_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
CommandRunsTableto a component and broughtpages/admin/index.tsxback 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-resourceroute. Standard discovery endpoint so MCP clients can auto-configure. Required a firewall bypass for.well-known/*.- Streamable HTTP transport.
/api/mcpsupports the newer Streamable HTTP flow alongside the legacy SSE transport; also required a firewall bypass. - UX and security follow-ups. Simplified the
/profile/mcpinstall flow (PR #317) and patched a set of OAuth 2.1 spec-compliance gaps in the same pass. /mcpmarketing landing page. Public-facing page with the install command (OAuth + bearer alternative), the full 7-tool catalog with descriptions sourced fromlib/mcp/createServer.ts, auth/rate-limit/audit notes, and CTAs to/subscribeand/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.
BriefCardcomponent. 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]) —CompanyUpsellHeroreplaced 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.
MarketingNavreplacesLegacyNavbaron 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
Herocomponent, trust stats bar, "Filter by what actually matters to you" section. - Feature landing pages.
/job-application-tracker,/profile,/funding-roundsnow have proper marketing landing pages with conversion CTAs. - Always sidebar when logged in.
MarketingLayoutproxies toAppLayoutfor 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_hoverevents 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.icofetch 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_s3andimage_urlupdated in thecompaniestable. - 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
/mappage. 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/mapand 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
/subscribepage 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
/blogindex 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
/mappage 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/pipelinepage. 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/sessionon 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.