Use the site router for your own sign-in and API tokens
-
What you'll see
Another system needs to call a site's
#/API/scripts with a token, or the site's users sign in with your own identity provider rather than a Docly account. On a publication that requires Docly sign-in, a Bearer token is stopped by the sign-in before your code runs (a 302 to the login page).The tempting fix is a second, public publication with only API scripts and a token check in each script. Do not do that: a public publication serves every uploaded file by its direct path and the data folder's
#/Root, whatever app it runs — and every script that forgets the check is open. -
What's actually happening
One router in front of the whole site decides every request before anything is served, so the protection does not depend on each script remembering it.
- Publish without sign-in and set
"router": { "script": "Router.js" }. With sign-in on the publication, Docly's sign-in answers first and the router only ever sees signed-in Docly users. - The router sees every address — pages, uploaded files, scaled images,
#/Rootpages, API calls and addresses that do not exist. A router that only checks/API/leaves the rest public. Start from deny: name what is public, and require a session or a token for the rest. - The identity is the JWT. The router writes it with
docly.writeJwtwhen the sign-in completes and readsrequest.Jwton later requests; API scripts read the same value. A token call has no session: the router checks the token, and an API script reads the header if it needs to know the caller. - Failures never let anyone in. An exception, a timeout, a missing
return, an invalid script name or a missing file give500— never the page — and the cookies the router set are discarded. - Pages stay shared.
request.Jwtis not set in pages; per-user data comes from an API script (see Hash files are cached). The router runs before the page cache, so access is still decided on every request. - Keep secrets and hashes on the site, never tokens. The provider secret in a file under
#/; for API clients onlydocly.sha256of each token.
- Publish without sign-in and set
-
What to do
1. Publish and configure
Publish the folder without sign-in, and add to
site.json:"router": { "script": "Router.js" } // = #/Services/Router.js2. The router — deny by default
// #/Services/Router.js const PUBLIC = ['/assets/', '/about']; export default (route) => { const url = request.url; const isApi = /^\/(#\/|%23\/)?api\//i.test(url); // API addresses are case-insensitive // Your own sign-in: /auth/login and /auth/callback (shown in full on "The site router"). if (url.startsWith('/auth/')) return signIn(route, url); // Machine calls carry a token, checked against stored hashes. const h = docly.getHeader('Authorization') || ''; if (isApi && h.startsWith('Bearer ')) { const hash = docly.sha256(h.substring(7).trim()); const clients = docly.getJson('#/Services/api-clients.json') || []; if (clients.some(c => c.Sha256 === hash && c.Active !== false)) return route.next(); docly.setHeader('WWW-Authenticate', 'Bearer error="invalid_token"'); return route.json({ error: 'invalid_token' }, 401); } if (PUBLIC.some(p => url.startsWith(p))) return route.next(); if (request.Jwt) return route.next(); if (isApi) return route.send(null, 401); return route.redirect('/auth/login?returnUrl=' + encodeURIComponent(url)); };The sign-in itself — state cookie, code exchange,
writeJwtand a safe return address — is shown in full on The site router.3. API scripts
An API script runs only when the router let the call through. When the answer depends on who asks, read
request.Jwtfor a signed-in user, or theAuthorizationheader for a token call.Checklist
- The publication has no sign-in, and
router.scriptnames a file in#/Services/. - Everything not named as public ends in a refusal (an answer with
401or403) or a redirect to sign-in. - Token hashes, not tokens, are stored on the site; the provider secret lives under
#/. - A return address is only followed when it starts with a single
/. - Test: no token, a wrong token and the right token on
/API/(401, 401, 200); a page, an uploaded file and an image without a session (sent to sign-in); a router that throws (500, and no cookie set).
- The publication has no sign-in, and
Last reviewed 20 Oct 2026