Hash files are cached
What you'll see
A developer — or more often an AI code-generation tool — writes a hash file (.hash) that embeds per-request data inline using #...#-expressions. Common shapes:
<img src="#docly.getProfilePictureUrl(request.Jwt.username)#" alt="Profile">
<p>Welcome, #request.Jwt.username#</p>
#if (!request.Jwt) docly.denyAccess();#The first request renders correctly. From that point on, every visitor sees the first user’s cached HTML — including their username, profile picture, or (worst) a denyAccess() call that fired for a logged-out user and now blocks logged-in users.
What's actually happening
Docly hash files are display templates that the HashJS engine renders to HTML and caches. The cache is keyed by the file path, not by request — #...#-expressions are evaluated once when the cache is built, then the cached output is served to all subsequent visitors without re-evaluation.
This is intentional. Hash rendering is what makes Docly-served pages fast: a hash file with no per-request expressions can be served straight from cache with no JS evaluation at all. The tradeoff is that anything depending on the current request — request.Jwt, request.IP, request.UserAgent, query parameters, the current time, the currently logged-in user — must not appear inside #...# in a hash file.
The vulnerability class this creates is data leakage: the first user to hit a page after cache invalidation populates the cache with their own session data, and every subsequent user receives that data until the cache is rebuilt. For a profile-picture URL this is a privacy bug; for an admin-only block (docly.denyAccess()) it can become a security incident.
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:
<img src="#docly.getProfilePictureUrl(request.Jwt.username)#" alt="Profile">
<p>Welcome, #request.Jwt.username#</p>Access control follows the same rule: never put docly.denyAccess() or request.Jwt-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.