Docly Developers
Knowledge base Pitfall By design

Hash files are cached

In shortA hash page is rendered once per URL and the HTML is served to every visitor after that - on a publication with login, to every logged-in user. A page therefore never sees who is signed in: request.user and request.Jwt are null in pages, and User is not set in a .docly display template. Other per-visitor expressions, like the current time, freeze the first visitor's value. Dynamic per-user data must come from #/API/ endpoints fetched by browser JS at runtime.
Applies to Hash templatesCaching
  1. What you'll see

    A developer — or more often an AI code-generation tool — writes a hash file (.hash) that embeds per-visitor data inline using #...#-expressions. Common shapes:

    <img src="#docly.getProfilePictureUrl(request.user)#" alt="Profile">
    <p>Welcome, #request.user#</p>
    <p>Generated #new Date().toISOString()#</p>

    Neither the name nor the profile picture shows the visitor: request.user is null in a page, for everyone. The time is rendered once, and from then on every visitor gets the first visitor’s cached HTML with that time in it.

  2. What's actually happening

    Docly hash files are display templates that the HashJS engine renders to HTML. The rendered output is cached and served to later visitors without running the template again. Dynamic content belongs in #/API/, which is never cached.

    The cache key is the page's path — not the visitor, and not the query string: /page?id=1 and /page?id=2 share one cached copy (a template cannot read the query string anyway — query exists only in API scripts). Everyone who opens the page gets the same HTML. Anything that depends on the visitor — the IP, the user agent, the current time — must therefore not appear inside #...# in a hash file.

    Publications with login are cached too. On a publication that requires login, the platform checks access first and then serves the cached page — and the cache is shared by all logged-in users. One copy serves them all, so the page cannot know who is viewing it.

    A page never sees the signed-in user. request.user and request.Jwt are null in hash pages, and in a .docly display template User is not set — also when the document is pulled in with #include(…)#. They are set only in API functions in #/API/, which are never cached. A check like #if (!request.user) …# in a page is therefore always true.

    The vulnerability class this prevents is data leakage: if a page could read the visitor, the first visitor after cache invalidation would populate the cache with their own data, and every later visitor would receive it until the cache was rebuilt.

  3. What to do

    Put per-request logic in #/API/-functions. From the hash file, use static HTML plus browser JS that calls the API at runtime via fetch(). The hash itself stays static and cacheable; the dynamic data flows through the API on every request.

    Do — split static hash from dynamic API:

    // #/API/profile.js
    export default () => {
        if (!request.Jwt) return docly.denyAccess();
        return {
            username: request.Jwt.username,
            pictureUrl: docly.getProfilePictureUrl(request.Jwt.username)
        };
    }
    <!-- profile.hash — static HTML, dynamic data fetched at runtime -->
    <img id="avatar" alt="Profile">
    <p>Welcome, <span id="name"></span></p>
    <script>
        fetch('~/API/profile')
            .then(r => r.json())
            .then(d => {
                document.getElementById('avatar').src = d.pictureUrl;
                document.getElementById('name').innerText = d.username;
            });
    </script>

    Don’t — per-request expression in hash (neither shows the visitor, since request.user is null in a page):

    <img src="#docly.getProfilePictureUrl(request.user)#" alt="Profile">
    <p>Welcome, #request.user#</p>

    Access control follows the same rule: docly.denyAccess() only exists in API functions, so never put access checks or per-user checks inside #...# in a hash file. Protect the endpoint behind the data instead — let the API call denyAccess(), and let the hash page show a “loading…” state until the fetch resolves or fails.

Last reviewed 25 Oct 2026