Skip to content

Static Assets

How τjs handles static file serving in development and production.

τjs can mount static asset middleware for you, but it does not bundle any static file plugin by default.

Instead, you provide one or more Fastify plugins (for example @fastify/static) via the staticAssets option on createServer. If you don’t provide staticAssets, τjs does no static file serving - it only reads manifests from your client build for SSR.

This keeps the core clean and lets you decide whether assets are served by:

  • Fastify (via @fastify/static or a custom plugin)
  • a CDN (S3/CloudFront, etc.)
  • a reverse proxy (Nginx, Apache, etc.)

If you don’t configure static assets:

import { createServer } from "@taujs/server";
await createServer({
fastify,
config,
serviceRegistry,
clientRoot: "./dist/client",
});

What happens:

  1. τjs initialises its SSR server
  2. It loads manifests and templates from clientRoot
  3. It does not register any static file middleware

Result: HTML rendering works, manifest lookups work, but Fastify will not serve JS/CSS/images for you. You must either:

  • mount your own static middleware, or
  • serve assets via CDN / reverse proxy.

This is intentional - τjs doesn’t hide a static dependency inside the core package.

Registering Static Assets with @fastify/static

Section titled “Registering Static Assets with @fastify/static”

To let τjs mount static file serving for you, pass a staticAssets configuration. For a single mount using @fastify/static:

import fastifyStatic from "@fastify/static";
import { createServer } from "@taujs/server";
import path from "node:path";
const clientRoot = path.join(process.cwd(), "dist", "client");
await createServer({
fastify,
config,
serviceRegistry,
clientRoot,
staticAssets: {
plugin: fastifyStatic,
options: {
// τjs will default root to your client build, but you can override:
root: clientRoot,
prefix: "/", // serve assets under /
index: false,
wildcard: false,
// any other @fastify/static options...
},
},
});

Under the hood τjs normalises this into a list of mount entries and registers them on the Fastify instance.

You can mount multiple static plugins or prefixes:

await createServer({
fastify,
config,
clientRoot,
staticAssets: [
{
// App assets
plugin: fastifyStatic,
options: {
root: clientRoot,
prefix: "/app/",
},
},
{
// Admin assets
plugin: fastifyStatic,
options: {
root: clientRoot,
prefix: "/admin/",
},
},
],
});

τjs sorts entries by prefix depth, so more specific prefixes are registered first.

If you’re serving assets from elsewhere (CDN, Nginx, etc.), you can explicitly disable τjs static mounting:

await createServer({
fastify,
config,
serviceRegistry,
clientRoot: "./dist/client",
staticAssets: false, // τjs will not register any static middleware
});

Use this when:

  • Assets are served from a CDN
  • A reverse proxy (Nginx/Apache) serves static files directly
  • You mount all static plugins yourself outside of τjs

τjs still reads manifests from clientRoot for SSR, but never serves the files.

τjs expects a project-level public/ directory for non-bundled assets:

project/
├── public/
│ ├── favicon.ico
│ ├── robots.txt
│ └── app/
│ └── logo.svg
└── client/
├── app/
└── admin/

During build:

  • Client build: copies public/ contents into dist/client/
  • SSR build: uses publicDir: false (no extra copying)

Result after build:

dist/client/
├── favicon.ico
├── robots.txt
├── app/
│ ├── logo.svg
│ └── assets/
└── admin/
└── assets/

These files are then served by whatever static setup you’ve chosen (Fastify, CDN, proxy).

You can namespace assets per app:

public/
├── app/
│ ├── logo.svg
│ └── favicon.ico
└── admin/
├── logo.svg
└── favicon.ico

References in HTML:

<!-- Customer app -->
<img src="/app/logo.svg" />
<!-- Admin app -->
<img src="/admin/logo.svg" />

As long as your static middleware (or CDN/proxy) serves dist/client/ at /, these URLs resolve correctly.

After building the client:

Terminal window
npm run build:client
# Upload to S3 (example)
aws s3 sync dist/client/ s3://my-bucket/assets/ \
--cache-control "max-age=31536000,immutable" \
--exclude "*.html"
# HTML with no-cache
aws s3 sync dist/client/ s3://my-bucket/assets/ \
--cache-control "no-cache" \
--include "*.html"
await createServer({
fastify,
config,
serviceRegistry,
clientRoot: "./dist/client", // still needed for manifests
staticAssets: false, // CDN serves all assets
});

τjs uses the manifests for SSR, but all files are actually served by the CDN.

  • Hashed assets: max-age=31536000, immutable
  • HTML files: no-cache
  • Other files: no-cache (unless they’re hashed and safe to cache long-term)
upstream nodejs {
server localhost:3000;
}
server {
listen 80;
# Static assets - Nginx serves directly
location ~ ^/.+/assets/ {
root /app/dist/client;
add_header Cache-Control "public, max-age=31536000, immutable";
}
# Try static first, then proxy to Node
location / {
root /app/dist/client;
try_files $uri @nodejs;
}
location @nodejs {
proxy_pass http://nodejs;
proxy_set_header Host $host;
}
}

Configure τjs:

// Nginx handles static files; τjs only does SSR
await createServer({
fastify,
config,
serviceRegistry,
clientRoot: "./dist/client",
staticAssets: false,
});

Check one of the following is true:

  • You configured staticAssets with a valid plugin (e.g. @fastify/static), or
  • You have a CDN / proxy correctly pointing at your built dist/client/ directory.

Verify the files actually exist:

Terminal window
ls dist/client/app/assets/
# Should show built JS/CSS chunks

If your static plugin needs MIME overrides, configure them in the plugin options:

import fastifyStatic from "@fastify/static";
await createServer({
fastify,
config,
clientRoot,
staticAssets: {
plugin: fastifyStatic,
options: {
root: "./dist/client",
setHeaders: (res, filePath) => {
if (filePath.endsWith(".js")) {
res.setHeader("Content-Type", "application/javascript");
}
},
},
},
});