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.
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;
// 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 (url.startsWith('/API/') && 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 (url.startsWith('/API/')) return route.send(null, 401);
return route.redirect('/auth/login?returnUrl=' + encodeURIComponent(url));
};The sign-in itself — state cookie, code exchange, writeJwt and 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.Jwt for a signed-in user, or the Authorization header 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).