Serving Static Files
Serve files from a local directory using app.serveStatic. This is typically the very next thing you'll register after addDomain — it powers your frontend, assets, and any other files the browser needs. Registers both GET and HEAD handlers automatically — HEAD answers with the headers and the file's Content-Length and no body, and an empty file is answered immediately with an empty body:
// Serve everything in ../public at the root
nukebase.app.serveStatic("/*", path.join(__dirname, "../public"));
// Serve a private directory (no auth callback = open access)
nukebase.app.serveStatic("/assets/*", path.join(__dirname, "assets"));
serveStatic auth callback receives (res, req) — res first, then req. This matches the underlying uWebSockets convention and is the opposite of Express.
Signature
app.serveStatic(routePattern, rootDir, auth?)
routePattern- URL pattern with trailing/*(e.g.,"/*","/admin/*"). The matched portion before/*is treated as the mount point and stripped before resolving againstrootDir.rootDir- Absolute path to the directory to serve from. Usepath.join(__dirname, "...").auth(optional) - Async callback returning a boolean. If provided, runs before any file is served. Returntrueto allow,falseto respond with 401.
Auth Callback
The auth callback receives (res, req) — same convention as postWithBody:
// Only logged-in users can access /private/*
nukebase.app.serveStatic("/private/*", path.join(__dirname, "../private"),
async (res, req) => {
return Boolean(req.uid);
}
);
// Admins only
nukebase.app.serveStatic("/admin/*", path.join(__dirname, "../admin"),
async (res, req) => {
return req.claims?.role === "admin";
}
);
The req object passed to the auth callback contains the auth fields populated by checkAuth (uid, username, email, claims, cookies, urlParams, referer, userAgent, ip, url) plus host and method. Identity fields are present only when the user is authenticated. Returning a falsy value sends a 401 response automatically.
The callback is a hook, not just a gate. It runs on every request before a byte is served and receives res, so it is the natural place for cross-cutting work — request logging, page-view analytics, setting a cookie. Do the work, then return true to serve the file normally. Keep it cheap and never let it throw: it sits on the path of every asset your site serves.
Behavior Details
- Index files: Requests ending in
/automatically serveindex.htmlfrom that directory. - Path traversal protection: Any URL that resolves outside
rootDir(e.g., via../) returns 403 Forbidden. - Directories: A URL resolving to a directory without a trailing slash gets a
301 Moved Permanentlyto the same URL with one added, so relative links inside the served page resolve correctly. - Missing files: Return 404 Not Found.
- Streaming: Files are streamed in 16KB chunks rather than buffered into memory — safe for large files.
- Content-Type: Set automatically from the file extension via the
mimepackage; falls back toapplication/octet-streamfor unknown types. - Validators: Every file carries a weak
ETag(from size and mtime) and aLast-Modifiedheader. A request with a matchingIf-None-Match(or, without one,If-Modified-Since) is answered304 Not Modifiedwith no body, so checking whether a file changed costs a round trip and no bytes. - Cache-Control:
no-cachefor most files, which means "always revalidate" — browsers never serve a stale copy on their own, and a push shows up on the next load. Files withimage/*content types keeppublic, max-age=31536000(1 year), which is why an icon is renamed rather than replaced. Files served by a mount whose auth callback read who the caller is (see below) getprivate, no-store.
Files gated on a person are never cached. If the auth callback reads any identity field — req.uid, req.username, req.email, req.token, req.claims or req.cookies — the response is marked private, no-store, so neither the browser's HTTP cache nor the service worker's page cache keeps it, and it can never be shown to the next person on a shared device. The template's () => true reads nothing, so an ordinary site stays cacheable. Reading req.ip or req.url (for logging, say) does not make a file private.