Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 154 additions & 0 deletions .agents/skills/fern-docs/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
---
name: fern-docs
description: >-
Author and edit Fern documentation: MDX pages, navigation, docs.yml config,
custom components, landing pages, and changelog entries. Use when working in a
Fern docs repo (a fern/ directory with docs.yml). Routes to a detailed
reference per task.
---

# Fern docs authoring

Write and edit documentation on a Fern site. `SKILL.md` is the index: follow the
principles below, then read the `references/` file that matches the
task before writing. Don't work from this page alone when a reference exists.

**Scope: this skill governs Fern docs content only** — MDX pages, navigation,
`docs.yml` and related config, snippets, changelog entries, custom components,
and landing pages. It does **not** govern repo governance or tooling files that
happen to live in the same repo, such as `CLAUDE.md`, `AGENTS.md`,
`STYLE-GUIDE.md`, and contributor guides. Never apply the principles below —
"one canonical home," "prefer editing over creating," consolidation — to those
files. Leave them alone unless the user explicitly asks you to edit them.

## Fern resources

Fern's configuration evolves. Look things up rather than guessing.

- **Docs:** https://buildwithfern.com/learn/docs/getting-started/overview.md
(append `.md` to any docs URL for clean markdown to fetch)
- **Page index:** https://buildwithfern.com/learn/llms.txt
- **MCP server** — connect your agent to Fern's live docs and query it for any
syntax or behavior you're unsure of. The endpoint works with any MCP client;
for Claude Code:

```bash
claude mcp add --transport http fern https://buildwithfern.com/learn/_mcp/server
```

## Routing

| Task | Read |
|------|------|
| Writing or editing a changelog entry | `references/changelog.md` |
| Setting up a product switcher, adding a product, or refactoring nav into products | `references/products.md` |
| Moving, renaming, or deleting a page, or any edit that changes a published URL | `references/redirects.md` |
| Restricting pages or content by role (RBAC), or wiring docs into an auth method (password, SSO, JWT, OAuth) | `references/authentication.md` |
| Editing the same content across multiple pages, or adding generic/boilerplate content (e.g. "contact support") | `references/snippets.md` |
| Translating pages into another language, adding a language, or updating existing translations (multi-language Fern repo) | `references/translations.md` |

## Core principles

- **Defer to the repo's own conventions.** When a docs repo has an established
pattern, voice, or structure — a `CLAUDE.md`, a style guide, or just the
consistent practice of surrounding pages — follow it over the guidance here.
This skill is the default, not an override.
- **Write what the reader needs to succeed — no more.** Every sentence earns its place.
- **Prefer editing over creating.** Search the repo for a page that already
covers the topic and update it instead of adding a duplicate.
- **Make minimal, precise edits.** Don't rewrite a page when a paragraph fix will do.
- **Push back when something seems wrong.** Explain why rather than complying silently.
- **Ask when unclear.** Don't fill gaps with assumptions.
- **Never fabricate.** If you don't know a config key or behavior, look it up
(MCP server or docs) or say so. Don't invent frontmatter or YAML fields.
- **Cross-reference.** When you mention a concept documented elsewhere, link to it
so readers can find their way.
- **Preserve URLs when they change.** Any edit that moves, renames, or deletes a
page — or changes a slug, section, or product — can change a published URL.
When it does, set up [redirects](https://buildwithfern.com/learn/docs/seo/redirects.md)
from the old path (see `references/redirects.md`) so inbound links and search
rankings survive.

## Links

Internal links are URL paths derived from **this repo's own `docs.yml` and page
frontmatter** — not file paths on disk or relative paths. Nearly every edit
touches a link, so get this right every time.

A page URL is `<base-path>/<product-slug>/<rest>`:

- **Base path** — the prefix in your docs URL (e.g. `/learn`, `/docs`; may be empty).
- **Product slug** — the product's `slug` in `fern/docs.yml`. Omitted for a
product with no `slug:` (such as a default single Home product).
- **`<rest>`** — found by step 1 or step 2 below.

To find `<rest>`:

1. **Open the target page and check its frontmatter for `slug:`.** A frontmatter
slug is **absolute within the product**: it *is* `<rest>` on its own, and it
replaces the entire section/folder hierarchy. The section's slug does not
appear. Do not walk the navigation.
2. **Otherwise, walk `docs.yml`** within the product, from section through any
folders to the page. Each level contributes one slug:
- Its explicit `slug:` if set.
- Otherwise auto-derived from the display name: lowercased, spaces to
hyphens, special characters stripped (so `v3 (Latest)` becomes
`v-3-latest`). Auto-derivation isn't always obvious — verify, don't assume.
- In folder-based navigation, a page's slug comes from its **filename**, not
a display name (`quick-start.mdx` to `quick-start`).
- **Omit any level with `skip-slug: true`** — it contributes no segment.

Example — page `./pages/ai/overview.mdx` under section "AI features", in product
`docs`, base path `/learn`:

- with frontmatter `slug: ai-ai` → `/learn/docs/ai-ai` (the `ai-features`
section slug is dropped)
- with no frontmatter slug → `/learn/docs/ai-features/overview` (section
auto-slug `ai-features` + page auto-slug `overview`)

Section slugs frequently differ from folder names, so never guess the URL from
the directory layout. If you can't resolve a path from `docs.yml` and the page's
frontmatter, say so rather than guessing.

**Anchors** append `#heading`, and only resolve for real `##`/`###` Markdown
headings — not for JSX title props like `<Step title>`, `<Tab title>`,
`<Accordion title>`, or `<Card title>`.

**Not internal links** (leave as-is): external `https://` URLs, image paths
(`./images/...`), snippet includes (`<Markdown src="/snippets/..." />`), and
same-page anchors (`#section`).

## Cross-referencing

When you document new functionality — a new page, or new behavior on an existing
one — related pages usually need a pointer to it, or the new content is hard to
find from where readers actually start. This applies to small edits too, not
just new pages.

- **One canonical home.** The full explanation lives on one page; every other
page that touches the topic links to it instead of restating it. If you're
copying more than a sentence or two onto another page, it belongs on the
canonical page.
- **Find the targets.** Grep the docs for the feature name and one or two related
keywords, then read each hit and decide whether a pointer is warranted:

```bash
grep -rln "<feature>\|<keyword>" fern --include="*.mdx"
```

Common targets: pages for behavior the feature affects, adjacent feature pages,
and overview or landing pages.
- **Use the lightest form that works:** an inline link inside an existing
sentence, then a `<Note>` for a secondary aside, then a new `##` section, then
a new page. Inline links and Notes add no maintenance surface; new pages and
sections do.
- **Phrase inline links naturally — never "See [page]."** Put the link on a noun
phrase already in the sentence, not a tacked-on pointer sentence. Don't wrap a
lone link in a `<Tip>` whose only job is to host it.
- Good: "Each request counts against your [rate limit](...), which resets hourly."
- Bad: "Requests are rate limited. See the [rate limits reference](...) for details."
- Bad: a standalone `<Tip>` containing only "For more, see [rate limits](...)."
- **Frame by function, not plumbing.** Contrast what each feature does ("X
decides A; Y decides B"), not that they share a config key or sit in the same
directory. If the only thing linking two features is the same YAML key, it's
not worth a cross-reference.
180 changes: 180 additions & 0 deletions .agents/skills/fern-docs/references/authentication.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
# Authentication & RBAC

Gate a docs site so readers only see content meant for them. Read this when a
task involves restricting pages or content by role (RBAC), or wiring docs into an
auth method (password, SSO, JWT, OAuth).

This page is decision context: what each piece is, which surface it lives on, and
**which canonical doc to fetch before you edit**. Don't work the syntax from
memory — fetch the matching page (append `.md` for clean markdown) or query the
Fern MCP server, since the config evolves.

## First, locate the surface

Most of an auth setup is *not* in the docs repo. Before touching anything,
work out which surface the task actually lives on — it changes whether you edit a
file at all:

- **In the repo (`docs.yml` + MDX)** — declaring roles and gating content. This is
RBAC, and it's the bulk of what you'll edit here.
- **In the Fern Dashboard** — password protection (passwords mapped to roles).
- **With Fern support** — SSO, OAuth, and JWT signing secrets. You send Fern your
identity-provider details; Fern provisions the connection. **The repo never
holds client secrets or signing keys** — if a task asks you to put one in
`docs.yml`, push back.

When a request is really "turn on SSO" or "set the docs password," the answer is a
pointer to the Dashboard or support, not a `docs.yml` edit. Say so rather than
inventing config.

## RBAC (the in-repo part)

RBAC has three moving parts, all in the repo. You'll usually touch more than one
in a single task, so know what each is for:

- **Declare roles** — a top-level `roles:` list in `docs.yml`. Every role
referenced anywhere must be declared here or `fern check` fails. `everyone` is
auto-assigned to all visitors, including unauthenticated ones — but it is *not*
exempt from the declaration rule: if you reference it in `viewers:` or
`<If roles>`, you must still list `everyone` under `roles:` or `fern check`
fails with "Role 'everyone' is used but not declared."
- **Gate navigation** — a `viewers:` key on a navigation item. Only listed roles
see it. Fern allows `viewers:` on **products, versions, tabs, sections, pages,
api references, and changelogs** — every level of the tree.
- **Gate inline content** — the `<If roles={[...]}>` component, to hide part of a
page rather than the whole page.

Three behaviors that are easy to get backwards — get these right or the gating is
silently wrong:

- **Omitting `viewers:` is authenticated-only, NOT public.** A navigation item
with no `viewers:` key is visible to *any signed-in user* but hidden from
logged-out visitors. To make something truly public you must set it explicitly:
`viewers: [everyone]`. "No key" and `[everyone]` are different states — don't
treat a bare item as public.
- **Viewership is inherited.** If a section is restricted to `admins`, all its
pages and nested sections are admins-only too — you don't repeat `viewers:` on
each child. So to test or reason about one level in isolation, keep its parents
permissive; otherwise an inherited restriction, not the key you're looking at,
is what's gating.
- **Gated items are hidden, not locked.** By default an unauthorized reader sees
no trace of the item in nav (not a 404 or lock screen), and unauthenticated
users are redirected to login. A "visible but locked" presentation exists but
must be requested from Fern, so don't assume it's on.

**`viewers:` placement depends on the item.** On most items it sits directly on
the navigation entry, but **on a tab it goes in the `tabs:` map, not on the
`- tab:` entry** in `navigation:`. Putting it on the `- tab:` entry fails
`fern check`.

**Fetch before editing:**
https://buildwithfern.com/learn/docs/authentication/features/rbac.md — for exact
`roles:`/`viewers:`/`<If>` syntax.

### Gating a level means that level must exist in the nav

To gate, say, the tab or version level, the nav must actually contain a tab or a
version — and Fern's nav structure rules vary by file type in ways that bite when
you build a fixture or restructure to add a gate:

- A **versioned product**'s version files use flat navigation (sections/pages);
**tabs are not supported there** — put tab-level structure in a non-versioned
product, whose product file does support `tabs:`.
- A **versioned product entry** in `docs.yml` needs both a top-level `path:` (the
default version) **and** a `versions:` list.
- An `- api:` reference node validates in a product's flat navigation but is
rejected inside a version file's tab layout.

These are navigation-structure rules, not RBAC rules, but they surface the moment
you add a gate to a level that wasn't there before. Confirm structure against the
[products](https://buildwithfern.com/learn/docs/configuration/products.md) and
[versions](https://buildwithfern.com/learn/docs/configuration/versions.md) docs
(or `references/products.md`) rather than guessing — getting the wrong shape costs
far more time than looking it up.

### You can't verify RBAC in local preview

`fern docs dev` renders the site but does **not** enforce `viewers:` — every
reader is effectively unauthenticated, so restricted content just won't appear and
you can't confirm a role sees what it should. RBAC only takes effect on the
**published** site once an auth method is configured. Plan to verify on the
deployed site (or a preview instance) with real credentials, and tell the user
that local preview can't prove the gating works.

## Auth methods — which one, and where it's configured

Four ways to establish *who* a reader is; RBAC then decides what they see. JWT,
OAuth, and SSO all work through a `fern_token` browser cookie.

| Method | Configured | RBAC | API key injection | Fetch when working on it |
|--------|-----------|------|-------------------|--------------------------|
| Password | Dashboard | ✅ up to 3 roles | ❌ | [`setup/password-protection.md`](https://buildwithfern.com/learn/docs/authentication/setup/password-protection.md) |
| SSO | Fern support | ❌ | ❌ | [`setup/sso.md`](https://buildwithfern.com/learn/docs/authentication/setup/sso.md) |
| OAuth | Fern support | ✅ | ✅ | [`setup/oauth.md`](https://buildwithfern.com/learn/docs/authentication/setup/oauth.md) |
| JWT | You (mint the token) | ✅ | ✅ | [`setup/jwt.md`](https://buildwithfern.com/learn/docs/authentication/setup/jwt.md) |

Pick by the constraints, not preference:

- **Password** — quick gate for a small set of audiences. **Capped at three
password-mapped roles**, no API key injection. Passwords (and their role
mapping) are set in the Fern Dashboard → Settings → Password card, not in the
repo. Each Dashboard role name must match a string in `docs.yml`'s `roles:` list
**exactly**, or the password won't unlock the content it's meant to. You don't
map a password to `everyone` — that's the built-in unauthenticated role.
- **SSO** — internal docs behind corporate login (SAML 2.0 / OIDC). No RBAC, no
API key injection — it's all-or-nothing access.
- **OAuth** — Fern runs the flow against your provider; supports RBAC and API key
injection. Choose over JWT when you'd rather not mint tokens yourself.
- **JWT** — you sign the `fern_token` yourself, so you control the payload. The
only method with meaningful repo/code involvement: roles travel in the token's
`fern` claim, and you can inject an API key into the API Explorer through it.
Fetch `setup/jwt.md` for the claim shape and a token-minting example, and
[`features/api-key-injection.md`](https://buildwithfern.com/learn/docs/authentication/features/api-key-injection.md)
for the injection payload schema.

## Footguns

- **Declare before you use.** Any role in `viewers:` or `<If roles>` must be in the
top-level `roles:` list, or `fern check` fails.
- **Bare ≠ public.** An item with no `viewers:` is authenticated-only, not public.
Set `viewers: [everyone]` for anything that should reach logged-out visitors.
- **Inherited restrictions hide more than you think.** Because viewership cascades
down, a permissive-looking child can still be gated by an ancestor. Check the
whole path from product down, not just the item itself.
- **Gating without auth exposes everything.** `viewers:` and `<If roles>` are
inert until an auth method is configured. On a published site with *no* auth
method, all gated content is publicly visible — *not* hidden, and not locked.
`fern check` passing does not mean content is protected. Never treat `viewers:`
as protection until auth is wired up and verified live.
- **Secrets never live in the repo.** Signing keys, client secrets, and passwords
belong in the Dashboard or with Fern support.
- **Gating ≠ moving.** Hiding a page with `viewers:` doesn't change its URL, so
it's not a redirect situation. Renaming or moving it still is — see
`references/redirects.md`.

## Verify

Run `fern check` after editing roles or `viewers:` — it catches undeclared roles
and malformed config. But neither `fern check` nor `fern docs dev` proves the
gating works: visibility is only enforced on the **published** site with an auth
method configured, so a passing check is necessary, not sufficient.

Validating RBAC end-to-end happens mostly outside the repo. Walk the user through
the full loop and present it as concrete next steps:

1. **Publish** — `fern generate --docs` (needs `fern login` or a `FERN_TOKEN`; add
`--no-prompt` for non-interactive runs).
2. **Configure an auth method** — declaring and gating in the repo enforces
nothing on its own. For a quick RBAC test, password protection is the fastest
path: in the Dashboard, add one password per role, with role names matching
`docs.yml` exactly. Fetch
https://buildwithfern.com/learn/dashboard/configuration/password-protection.md
for the current Dashboard steps and walk the user through them — don't recite
menus from memory, since the UI changes.
3. **Verify per role** — load the published site and enter each role's credential
in turn, confirming each role sees exactly its slice and *not* another's. Give
the user a role → expected-visible-content table so they know what correct
looks like.

Always tell the user that local checks can't validate RBAC and that auth must be
live first.
Loading
Loading