Use the site router for your own sign-in and API tokens

Best practice Recommended
To let other systems call a site with a token, or to sign people in with your own provider instead of a Docly account, publish the folder without sign-in and let the site router protect it. The router runs before every request — pages, files, images and API — and lets it through, sends it to sign-in or denies it. The identity is the JWT the router writes, and failures never let anyone in. For Docly accounts, publish with sign-in instead: Docly's protection then comes first and the router runs after it.
Applies to: Sites with their own sign-inand sites whose API is called by other systems

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, #/Root pages, 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.writeJwt when the sign-in completes and reads request.Jwt on 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 give 500 — never the page — and the cookies the router set are discarded.
  • Pages stay shared. request.Jwt is 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 only docly.sha256 of 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.js

2. 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.script names a file in #/Services/.
  • Everything not named as public ends in a refusal (an answer with 401 or 403) 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).