Password Protection
Protect your docs with one shared passphrase, whether for the whole site or select pages, without moving any content out of your Git repository.
Password protection gates docs that stay in Git but shouldn't be public: runbooks, pre-release guides, partner-only docs, early-access features. One shared passphrase gates your whole site or a set of pages. Nothing moves out of your repo.
For per-user auth, use JWT authentication. It gates your docs behind your own login system, with per-user sessions and group-based page access.
Password protection requires a Jamdesk project connected to a Git repository. Configuration lives in docs.json and ships with your normal build.
Which mode should I pick?
Pick a mode based on what's public.
| Whole-site mode | Specific-pages mode | |
|---|---|---|
| Use when | Everything is private: internal engineering docs, a staging copy of your public site, an unreleased product. | Most docs are public. You hide a few pages (a runbook, a beta feature, an internal API reference). |
| How you turn it on | Set auth.password.enabled: true in docs.json. | Mark pages private with private: true in frontmatter, or list paths under auth.password.private[]. |
| Public exceptions | Yes: mark individual pages, nav groups, or glob patterns as public. | N/A. Every page is public unless you mark it private. |
The dashboard card, unlock screen, and rotation and revoke controls are the same in either mode. Switch modes by editing docs.json and pushing.
Protect your whole site
Declare whole-site protection in docs.json. The hint field is optional. It's the only on-screen clue readers get about how to obtain the password.
{
"$schema": "https://jamdesk.com/docs.json",
"name": "Acme Docs",
"theme": "jam",
"auth": {
"password": {
"enabled": true,
"hint": "Ask #docs-access on Slack"
}
}
}Hints are plain text, max 200 characters, no HTML.
Don't put the password in docs.json. Set it in the dashboard after the build. Your repo holds only the opt-in flag and an optional hint.
Push the change to your configured branch. The build activates password protection in whole-site mode.
git add docs.json
git commit -m "Turn on password protection"
git pushThe dashboard card changes from Off to Password not set. The site is locked. Every page returns 401 and shows the password screen. No password works until you set one. Pages you've marked public stay open. Project API keys still work for the Search API, the MCP server, .md URLs and the llms files. To make the whole site public again, set auth.password.enabled to false and push.

Open Project Settings in the dashboard and scroll to the Password Protection card. Type a passphrase (minimum 8 characters). Click Set password.
The card flips to On. Visitors without the password see the unlock screen.

Jamdesk never stores your plaintext password. It's hashed with scrypt in the dashboard database and never written to your repo or docs.json. Jamdesk can't email it to you if you forget it. Rotate instead.
Open your docs site in a private browser window (or curl it) and confirm you get the unlock screen. Try a wrong password to check the error state. Then enter the real one.
# Should respond with HTTP/1.1 401 and the unlock HTML
curl -I https://acme.jamdesk.app/
# Submit the password. On success, sets the jd_auth_<slug> cookie.
curl -i -X POST https://acme.jamdesk.app/jd/unlock \
-d "password=your-passphrase&from=/"A successful unlock returns a 303 redirect with a Set-Cookie: jd_auth_acme=...; HttpOnly; Secure; SameSite=Lax; Max-Age=2592000 header. Send that cookie with the next request.
Public exceptions
Keep specific pages public while the rest of the site is gated, for example a marketing landing page or a signup form.
Mark a page public in any of three ways. The build merges them into one allow-list.
Frontmatter is the most granular option. Add public: true to an .mdx file to make that page public:
---
title: Get started
public: true
---
Navigation groups cover a whole section. Set public: true on a group or tab in your docs.json navigation to make every page under it public. For example, a "Marketing" tab next to private engineering docs:
{
"navigation": {
"tabs": [
{
"tab": "Marketing",
"public": true,
"groups": [
{
"group": "Overview",
"pages": ["landing", "pricing", "changelog"]
}
]
},
{
"tab": "Internal",
"groups": [
{ "group": "Runbooks", "pages": ["deploys", "oncall"] }
]
}
]
}
}Explicit globs under auth.password.public[] cover what frontmatter and navigation can't: top-level landing pages, dynamically generated routes, or a subtree you'd rather not rewrite.
{
"auth": {
"password": {
"enabled": true,
"hint": "Ask #docs-access on Slack",
"public": [
"/landing",
"/pricing",
"/marketing/**",
"/blog/*"
]
}
}
}Globs support * (one path segment) and ** (any depth). Validation rejects a bare /, which would unlock your whole site. The dashboard card shows the allow-list resolved by the last build.
Protect just a few pages
In specific-pages mode, everything is public by default. Opt individual pages into the gate.
Add private: true to the page's frontmatter.
---
title: Incident Runbook
description: What to do when the deploys dashboard is on fire.
private: true
---To keep the list of gated paths in one file, add them under auth.password.private[] in docs.json. The two approaches are additive.
{
"auth": {
"password": {
"hint": "Ask the on-call engineer",
"private": ["/admin/runbook", "/internal/api-keys"]
}
}
}There's no enabled: true. Setting auth.password.private[] without enabled activates specific-pages mode.
Push your changes. The next build turns on protection in specific-pages mode. The dashboard prompts you to set a password.
git add content/runbook.mdx docs.json
git commit -m "Gate the incident runbook"
git pushOpen Project Settings, find the Password Protection card, and set a passphrase. The card header reads On with Specific pages instead of Whole site. It lists the private pages the build resolved.

Browse your docs site. Public pages load as before. Private pages redirect to the unlock screen. The password signs you in for 30 days on that device.
What visitors see
A gated page shows a centered unlock card with the site name and an optional hint. There is no sidebar or navigation.

The card is branded with your site's logo and primary color from docs.json. The password field has a reveal toggle and autofocus.
A wrong password renders the same card with an error message, a fresh input, and a small delay between attempts. Nothing on the page distinguishes "password wrong" from "no password entered yet."

The correct password sets a signed cookie. It lasts until the session expires or you revoke it.
AI tools and API keys
Tools that can't enter the password, such as an MCP client or a script that fetches llms.txt, can use a project API key instead. The key works for the MCP server, .md page URLs and the llms files. It never opens HTML pages or the AI chat.
Rotating and revoking sessions
Open the Password Protection card, type a new passphrase in the Rotate password field, and click Save new password. The old password stops working on the next request. Rotation takes effect immediately and doesn't require a rebuild.
To sign out every active session without changing the passphrase (say, a laptop went missing), click Revoke all sessions. It bumps a server-side version counter. Every cookie issued before the bump stops working. Visitors re-enter the current password.
Disabling protection
Turn protection off by editing docs.json and pushing.
- Whole site: remove
auth.password.enabled(or set it tofalse). - Specific pages: remove every
private: truemarker and clearauth.password.private.
The next build deletes the stored password hash and flips the card back to Off. Re-enabling protection later requires a new password.
Your source repo isn't password-protected. Password protection gates the hosted docs site at *.jamdesk.app (or your custom domain). A public GitHub repo still exposes the MDX content. Make the repo private for full content protection.
Precedence rules
A page can match several signals. The resolution order, from most to least specific:
auth.password.enabled: truegates the whole site.private: trueon individual pages becomes redundant.- A page marked
public: trueandprivate: trueis public. Public wins. - Frontmatter
public: true, nav-grouppublic: true, andauth.password.public[]globs merge into one allow-list. If any signal says a page is public, it's public. auth.password.private[]withoutauth.password.enabledactivates specific-pages mode.
How sessions and rate limiting work
The session cookie. On a successful unlock, Jamdesk sets a cookie named jd_auth_<slug> (for example, jd_auth_acme). It's HttpOnly, Secure, SameSite=Lax, scoped to the host, and signed with HMAC-SHA256. The payload includes the project slug, the current version counter, and an expiration timestamp. Tampering fails validation. The default lifetime is 30 days, refreshed on each successful unlock.
Rate limiting. The unlock endpoint applies two counters per hour: 10 attempts per IP and 100 attempts per project. They run before the scrypt hash check. A brute-force attempt can't burn CPU or leak timing information. Hitting either limit returns 429 Too Many Requests with a Retry-After header.
Storage. Your password is hashed with scrypt and stored in the dashboard's Firestore. It never lands in your repo, your docs.json, or a build artifact. If you lose it, rotate. No recovery path exists.
Testing during local development
jamdesk dev runs your docs against the live R2 content and live config. Password protection is not enforced in the local dev server. You can preview gated pages without the password.
To test the gate, open the deployed site at <slug>.jamdesk.app (or your custom domain) in a browser window without the cookie.
Troubleshooting
The dashboard card is probably showing Password not set. Protection needs (1) the config pushed in docs.json and (2) a password set in Project Settings. Until you set the password, every request returns 401 with the unlock screen as the body.
Their browser has an old jd_auth_<slug> cookie from before you rotated. Wait 30 days for the cookie to expire, click Revoke all sessions in the dashboard, or have them clear cookies for the docs domain. Their next visit prompts for the current password.
Not directly. Jamdesk uses one shared password per site. For per-group access, split your docs into multiple projects (each with its own password) or use specific-pages mode with separate public/private boundaries per audience.
Yes. The unlock cookie is host-bound. The *.jamdesk.app subdomain and your custom domain authenticate independently. Unlocking one host doesn't unlock the other.
Subpath setups (docs at yoursite.com/docs behind your own proxy) work out of the box. The unlock form submits under the /_jd/ path prefix. Every documented proxy configuration already forwards it. Turning password protection on or off needs no proxy changes.
For a proxy configured before the setup guide added /_jd/ forwarding, add /_jd/* to its forwarded paths.
No. Protected sites set noindex, nofollow on the unlock screen and return 401 for every gated page. Crawlers can't index anything behind the gate. Public pages inside a protected site are still indexed.
