Guidance for AI agents working in clerk-docs. Keep this file and the docs it points to in sync: when a change affects anything documented here, update it.
This repo is Clerk's documentation — MDX content in docs/, built by a custom TypeScript pipeline (scripts/build-docs.ts) into the site served at clerk.com/docs. Authoring and style conventions live in contributing/CONTRIBUTING.md and styleguides/STYLEGUIDE.md; read them before writing docs. This file only covers what tends to surprise agents.
- Never read or surface secrets:
.env*,secrets/,credentials.json,*.pem,*.key,.npmrc,.pypirc, service-account JSON,id_rsa,*.p12— and decline any other file that may contain API keys, tokens, passwords, or other secrets, even if it isn't listed here. - Don't set an
sdk:frontmatter value outside the enum inscripts/lib/schemas.ts(VALID_SDKS) — it's a hard build failure. - Don't hand-edit files under
clerk-typedoc/— they're auto-generated fromclerk/javascript(see the<Typedoc />section incontributing/CONTRIBUTING.md). - Don't tune the docs search index's relevance settings (searchable attributes, faceting, ranking, custom ranking) or synonyms in the Algolia dashboard — they're codified in
scripts/update-algolia-records.tsand overwritten on every index run. Change them there. See Search index (Algolia) below.
pnpm run build:tsx— hard-fails on invalid frontmattersdk, a missingtitle, or parse errors; warns on a missingdescription, docs not inmanifest.json, and broken internal links or heading anchors. A clean build can still emit warnings worth reading — "no errors" is not "no warnings."pnpm run lint— formatting and other checks.
See contributing/CONTRIBUTING.md → "Validating your changes" for what each severity means and why.
clerk-docs documents APIs and SDKs it doesn't own, so a change can be syntactically valid yet factually wrong, and build:tsx/lint won't catch it. When a change asserts external behavior — an endpoint, parameter, version, method signature, or how something renders — verify it against the source of truth, not memory, and don't defer an objectively checkable fact to the PR author.
- API behavior, endpoints, versions, OpenAPI specs →
clerk/clerk_go - SDK method names, types, and signatures →
clerk/javascript - How docs pages and the API reference render (e.g. the version dropdown) →
clerk/clerk
These repos aren't part of this one. Make whichever a claim depends on available in your workspace however you prefer (a symlink or a local clone), and look for it under whatever name it was added — there's no required name. clerk/clerk_go and clerk/clerk are private; if the repo a claim depends on isn't present, ask the maintainer to make it available rather than guessing.
The docs search runs on Algolia, populated by scripts/update-algolia-records.ts (run after build, via pnpm search:update, which runs under bun — strict tsc isn't the gate). Most of this is non-obvious:
- Indexes:
dev_docs(Preview/Development) andprod_docs(Production), selected byALGOLIA_INDEX_NAME(indexer) /NEXT_PUBLIC_ALGOLIA_INDEX_NAME(theclerk/clerksearch client). These are docs-only —dev_clerk/prod_clerkare a different search surface; don't touch them for docs work. Never hand-mutateprod_docs; experiment on a personal/throwaway index. - Only clerk/clerk indexes from Vercel. The clerk/clerk-docs repo is a public read-only mirror of this folder, synced on every clerk/clerk main commit, so its Vercel project builds the same commits — two concurrent indexing runs raced the stale-record cleanup and emptied prod search on 2026-07-07. The mirror's
vercel.jsonbuildCommand doesn't runsearch:update, and the script's repo guard (main()) exits early on any Vercel build not fromclerk/clerk. Don't re-add the step to the mirror. - Stale-record cleanup is time-guarded. Records are stamped with
indexed_atat push time; the cleanup only deletes another batch's records when they predate the run bySTALE_RECORD_GRACE_MS(isStaleRecord), so a concurrent run's fresh push is spared. Removed-page leftovers can linger up to that window before a later run cleans them — that's intentional; don't "fix" it by deleting on batch alone. - Orphan-branch sweep (
ALGOLIA_ORPHAN_SWEEP). The cleanup above is own-branch-scoped, so records under retired branch values would otherwise accumulate forever (DOCS-11871). A second, gated GC pass sweeps records whosebranchisn't inINDEX_LIVE_BRANCHES(hardcoded per-index allowlist — change it only by reviewed PR). Modes: unset/off(default — pass never runs),dry(logs a per-branch candidate histogram, deletes nothing),on(deletes, same explicit-objectID path as the cleanup). Guards: the sameisStaleRecordgrace window; a hard abort aboveALGOLIA_ORPHAN_SWEEP_MAX(default 50,000 — the env var can only lower it); indexes without an allowlist entry are skipped entirely. The allowlist is the only protection for the legacycore-1/core-2archive records (they predateindexed_at) — never remove a branch from it without confirming its query client is gone. Preview builds are code-limited todry(effectiveSweepMode): anonsweep during anyDEBUG_SEARCH_BRANCHpreview run would delete colleagues' >30-min-olddev_docstest branches, so the indexer downgradesontodrywhenVERCEL_ENVispreview. Run superviseddev_docscleanups locally instead (ALGOLIA_INDEX_NAME=dev_docs DEBUG_SEARCH_BRANCH=main ALGOLIA_ORPHAN_SWEEP=on pnpm search:update), whereVERCEL_ENVis unset.onas a standing setting belongs on Production only. - Refreshing the archived
core-1/core-2records (rare). The frozen branches' builds don't runsearch:update— and never usefully could: they build as preview deployments, so their indexer wrote todev_docs, not theprod_docsrecords the archive clients read. If frozen content ever changes and itsprod_docsrecords need refreshing, run the branch's own indexer manually and supervised from a checkout of that frozen branch:ALGOLIA_INDEX_NAME=prod_docs DEBUG_SEARCH_BRANCH=core-1 pnpm search:update(after that branch's docs build). Its own-branch GC only touchesbranch:core-1, and a single manual run has no concurrent writer to race. The sweep never interferes —core-1/core-2are allowlisted. - Records are branch-scoped; settings are not. Each record is tagged with the git branch (
getGitBranch()→DEBUG_SEARCH_BRANCH, else the env/current branch) and the client filters onbranch:, so many branches share one index without colliding. ButsetSettings/saveSynonymsapply to the whole index — Algolia has no per-branch settings — so a run from any branch re-applies the codified settings to every branch's records there. Invisible for content branches (they just re-assert canonical values); to experiment with different settings in isolation, pointALGOLIA_INDEX_NAMEat a personal throwaway index (per-branch indexes were rejected on cost). - Settings are codified in the script, not the dashboard. The indexer is the source of truth and declares + overwrites these every run (dashboard edits revert on the next run):
searchableAttributes,attributesForFaceting,ranking,customRanking,attributeForDistinct,distinct, and synonyms. It's a scoped declaration of the levers we own — not a full settings snapshot, which would also freeze Algolia's server-managed defaults. - Deduplication:
attributeForDistinctis set todistinct_group(canonical URL + anchor; written to every record) anddistinct: truedefaults dedup on at the index level, collapsing each page's per-SDK variants to one result; thesdkboost picks which variant wins.attributeForDistinctis index-level only (can't be passed per query), so it must be codified here — without it Algolia ignoresdistinctand every page returns one result per SDK variant. Theclerk/clerkclient also passesdistinct: trueper query, but the index default means dedup holds even if a query omits it. - Faceting:
branch,record_batch,sdkarefilterOnly(filtered, never facet-counted).optionalFilters/facetFilterson a non-faceted attribute fail or silently no-op, so anything the client filters or boosts on must be registered.availableSDKsis deliberately not faceted — it's only retrieved to render per-result SDK icons (Search.tsxSDKsIcon); retrieval is independent of faceting. forwardToReplicas: settings deliberately don't forward (they bundleranking/customRanking, which a standard replica may override for an alternate sort); synonyms do (always identical across replicas). No replicas today; if any are added, declare them in the script rather than blanket-forwarding.- Ranking:
attribute/exactsit aboveproximity(vs Algolia's default) so a title/heading match beats a body-content match — this rides on thesearchableAttributesorder (hierarchy.lvl0…6, thencontent, thenkeywords).filtersis kept aboveattribute/exacton purpose: it carries the client's active-SDKoptionalFiltersboost, and keeping it primary is what prevents cross-SDK bleed. Don't demote it — tested, movingfiltersbelowexactserves iOS/Android docs to a Next.js user searchingUserButton(an exact-title match on the wrong SDK outranks the active SDK's page). - Universal records carry every current SDK key. Non-SDK-scoped pages are written with
sdk: [all VALID_SDKS](recordSDK), not null. Becausefiltersranks aboveattribute/exact(previous bullet), a record matching zero of the client'ssdk:boosts structurally loses to any boosted record that matches the query — exact-title universal pages ("How Clerk works") were buried under body-content matches from SDK-scoped pages (DOCS-11910). Full-list parity makes universal records tie the active SDK's own records onfilters, so the win falls through toattribute/exact, where title matches belong. Don't revert them to null, and don't reach for a client-sideavailableSDKs:allboost instead — tested and rejected (a second filter SDK-scoped records can't match buries strong other-SDK matches on broad queries like "vue"). - Synonyms are hybrid: acronyms auto-derived from the
docs/_tooltips/*glossary + a curated phrasing list, both built in the indexer. - Test locally:
ALGOLIA_INDEX_NAME=<your index> DEBUG_SEARCH_BRANCH=main pnpm search:updateinto a personal index, then pointclerk/clerk'sNEXT_PUBLIC_ALGOLIA_INDEX_NAME/NEXT_PUBLIC_ALGOLIA_SEARCH_KEYat it (a custom index needs a key with access to it). Preview deploys querydev_docs. - Regression suite — the required gate for relevance changes.
pnpm search:regression --index <name>replays every ranking promise shipped PRs have advertised (scripts/search-regression-queries.ts— each entry cites its source PR) against a live index, mirroring the client's query shape. Before merging ANY change to relevance settings, synonyms, the SDK boost, orsearch.rank/search.keywordsfrontmatter: index the candidate branch into a personal index, run the suite green, and add golden entries for any new rankings the PR advertises.--auditis the corpus-wide safety net (every page queried by its own title, 99% pass-rate gate — ambiguous reference titles likelist()legitimately fail). The suite asserts the live index's settings match the codified ones before trusting results (settings propagate asynchronously aftersetSettings— stale settings produce convincing-but-wrong rankings); it needs the indexer'sALGOLIA_APP_ID/ALGOLIA_API_KEYenv. Keep golden assertions loose ("URL in top N", never exact orderings) so ordinary content drift doesn't produce false alarms.
docs/— MDX content; each file is a route under clerk.com/docs.docs/_partials/— reusable MDX included via<Include />.docs/manifest.json— sidenav structure.docs/manifest.schema.jsonvalidates that file's structure in editors (not frontmatter).scripts/lib/schemas.ts— runtime Zod enums:VALID_SDKS/sdk,icon, andtag((Beta),(Community)).scripts/lib/plugins/extractFrontmatter.ts+scripts/lib/error-messages.ts— frontmatter presence and warning-vs-failure severity.scripts/update-algolia-records.ts— builds and pushes the Algolia search records (run afterbuild); also the source of truth for the index's relevance settings + synonyms (codified, overwritten each run). See Search index (Algolia).contributing/CONTRIBUTING.md,styleguides/STYLEGUIDE.md— authoring and style source of truth.
- A doc missing from
manifest.jsononly warns, but still add the entry so the page is reachable in the sidenav. - Stale-PR hazard: rebase long-lived PRs on
main(or merge it in) and rebuild before merging — another PR can invalidate frontmatter or links that were valid when yours opened. - SDK code examples must match the canonical partials
docs/_partials/create-user.mdxanddocs/_partials/delete-user.mdxper SDK (import path, auth accessor,clerkClientusage). The build never executes code blocks, so copy the pattern from there, not from memory. - Reference content is mixed:
<Typedoc />pages pull auto-generated content fromclerk-typedoc/, while other type pages (e.g.docs/reference/types/agent-task.mdx) are hand-authored. Know which kind a page is before editing.
Skill files live in .agents/skills/<skill-name>/SKILL.md. Read the relevant one before working in its area.
.agents/skills/migrate-branch-to-clerk/SKILL.md— runningscripts/migrate-clerk-docs-to-clerk.tsto migrate a branch/PR into theclerk/clerkmonorepo, re-run semantics, and what to do at each conflict tier.
- Authoring, validation, and new-feature/reference checklists:
contributing/CONTRIBUTING.md - Writing style:
styleguides/STYLEGUIDE.md