Build & Deployment
How τjs builds applications and deployment strategies.
Overview
Section titled “Overview”τjs uses Vite to build frontend applications and produces:
- Client assets - Browser JavaScript and CSS
- SSR bundles - Server-side rendering modules
- Manifests - Mapping of source files to built assets
The build process is designed for multi-app architectures with separate bundles per application.
Build Process
Section titled “Build Process”Build Steps
Section titled “Build Steps”A complete production build involves:
- Client assets (Vite)
- SSR bundles (Vite)
- Server bundle (esbuild/rollup)
These steps are intentionally separate for flexibility.
Client Build
Section titled “Client Build”npm run build:clientWhat happens:
- Cleans
dist/directory - Runs Vite for each app in config
- Outputs browser assets to
dist/client/{entryPoint}/ - Generates
manifest.jsonper app - Copies
index.htmlif present
Output structure:
dist/client/├── app/│ ├── assets/│ │ ├── entry-client-abc123.js│ │ ├── App-def456.js│ │ └── index-ghi789.css│ ├── manifest.json│ └── index.html└── admin/ ├── assets/ ├── manifest.json └── index.htmlSSR Build
Section titled “SSR Build”npm run build:ssr# orBUILD_MODE=ssr npm run buildWhat happens:
- Does not clean
dist/(preserves client assets) - Runs Vite in SSR mode for each app
- Outputs SSR bundles to
dist/ssr/{entryPoint}/ - Generates
ssr-manifest.jsonper app
Output structure:
dist/ssr/├── app/│ ├── ssr-manifest.json│ └── server.js└── admin/ ├── ssr-manifest.json └── server.jsServer Build
Section titled “Server Build”npm run build:serverBundles your Fastify server code (not part of τjs):
dist/server/└── index.jsThis is your own server code bundled for production.
Build Configuration
Section titled “Build Configuration”Vite Override
Section titled “Vite Override”Customise Vite configuration per app:
export default defineConfig({ apps: [ { appId: "web", entryPoint: "client", plugins: [ react(), visualizer(), // Bundle analyser ], }, ],});Alias Configuration
Section titled “Alias Configuration”τjs provides default aliases:
'@client' → app root (e.g., client/app/)'@server' → project/src/server'@shared' → project/src/sharedOverride or extend:
// taujs.config.ts with custom aliasesexport default defineConfig({ alias: { "@components": path.resolve(__dirname, "client/shared/components"), "@utils": path.resolve(__dirname, "client/shared/utils"), }, apps: [ /* ... */ ],});Public Assets
Section titled “Public Assets”publicDir Behavior
Section titled “publicDir Behavior”Client build:
- Uses
public/at project root - All apps share the same public directory
- Assets copied to
dist/client/
SSR build:
- Sets
publicDir: false - No public assets processed during SSR build
Output Structure
Section titled “Output Structure”After a complete build:
dist/├── client/│ ├── app/│ │ ├── assets/│ │ ├── manifest.json│ │ └── index.html│ └── admin/│ ├── assets/│ ├── manifest.json│ └── index.html│├── ssr/│ ├── app/│ │ ├── ssr-manifest.json│ │ └── server.js│ └── admin/│ ├── ssr-manifest.json│ └── server.js│└── server/ └── index.jsTargeted Builds
Section titled “Targeted Builds”τjs supports selective per-app builds, allowing you to build only the apps you care about instead of running a full multi-app build every time. This is useful for:
- Monorepos with many apps
- CI pipelines that detect changed apps
- Local workflows where you only want to build a single surface
There is no cross-app bundling or shared-chunk inference. Each app remains an isolated build unit, which keeps MFE boundaries clean.
Build all apps (default)
Section titled “Build all apps (default)”If you run your build script without any flags or env variables, τjs builds every app defined in your taujs.config.ts.
node scripts/build.mjsExample build script:
import path from "node:path";import { fileURLToPath } from "node:url";import { taujsBuild } from "@taujs/server";import config from "../taujs.config.js";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
await taujsBuild({ clientBaseDir: path.resolve(__dirname, "src/client"), projectRoot: __dirname, config,});Build a single app (CLI)
Section titled “Build a single app (CLI)”To build only one app, pass --app, --apps, or -a:
node scripts/build.mjs --app adminMultiple apps:
node scripts/build.mjs --apps admin,marketingThe filter matches both:
appIdfromtaujs.config.tsentryPoint(usually the client subfolder name)
So any of these work:
node scripts/build.mjs --app adminnode scripts/build.mjs --app @acme/adminBuild specific apps using environment variables (CI-friendly)
Section titled “Build specific apps using environment variables (CI-friendly)”CI pipelines often prefer environment variables instead of CLI args:
TAU_APP=admin node scripts/build.mjsMultiple:
TAU_APPS=admin,marketing node scripts/build.mjsPrecedence: CLI flags override environment variables if both are present.
Combining with SSR / Client build modes
Section titled “Combining with SSR / Client build modes”τjs uses BUILD_MODE to determine which bundle to produce:
| BUILD_MODE | Result |
|---|---|
client | Client bundle(s) |
ssr | SSR bundle(s) |
| (unset) | Falls back to default logic in your build script |
Examples:
BUILD_MODE=client node scripts/build.mjs --app adminBUILD_MODE=ssr TAU_APP=admin node scripts/build.mjsTypical package.json setup:
{ "scripts": { "build:client": "BUILD_MODE=client node scripts/build.mjs", "build:ssr": "BUILD_MODE=ssr node scripts/build.mjs", "build:client:admin": "BUILD_MODE=client TAU_APP=admin node scripts/build.mjs", "build:ssr:admin": "BUILD_MODE=ssr TAU_APP=admin node scripts/build.mjs" }}What “incremental” means in τjs
Section titled “What “incremental” means in τjs”Selective builds are per-app, not cached or partial builds.
- Client builds always wipe
dist/before building. - SSR builds do not delete
dist/by default. - Only the targeted apps are passed through Vite.
This avoids stale assets and keeps each app isolated.
Deployment Strategies
Section titled “Deployment Strategies”Option A: Single Server
Section titled “Option A: Single Server”Fastify serves both static assets and SSR:
import Fastify from "fastify";import fastifyStatic from "@fastify/static";import { createServer } from "@taujs/server";import path from "node:path";
const fastify = Fastify({ logger: false });
// Serve static assetsawait fastify.register(fastifyStatic, { root: path.join(process.cwd(), "dist", "client"), prefix: "/", wildcard: false,});
// τjs handles SSRawait createServer({ fastify, config, serviceRegistry, clientRoot: "dist/client",});
await fastify.listen({ port: 3000, host: "0.0.0.0" });Deployment:
npm run buildnpm startOption B: CDN for Static Assets
Section titled “Option B: CDN for Static Assets”Upload client assets to CDN:
Build and upload:
npm run build:client# Upload dist/client/ to CDNaws s3 sync dist/client/ s3://my-bucket/assets/
npm run build:ssrnpm run build:server# Deploy server with SSR bundlesCDN cache rules:
/assets/* → max-age=31536000, immutable/*.html → no-cacheOther files → no-cache unless hashedServer configuration:
// Don't serve static assets - CDN handles themawait createServer({ fastify, config, serviceRegistry, clientRoot: "dist/client", // Still needed for manifests registerStaticAssets: false, // Disable static middleware});Option C: Reverse Proxy (Nginx)
Section titled “Option C: Reverse Proxy (Nginx)”Nginx serves static assets, proxies SSR to Node:
# Nginx configurationupstream nodejs { server localhost:3000;}
server { listen 80; server_name example.com;
# Static assets - long cache location ~ ^/.+/assets/ { root /app/dist/client; add_header Cache-Control "public, max-age=31536000, immutable"; }
# Try static files first, then proxy to Node location / { root /app/dist/client; try_files $uri @nodejs; }
# Proxy to Node.js for SSR location @nodejs { proxy_pass http://nodejs; proxy_http_version 1.1; proxy_set_header Host $host; }}Option D: Container Deployment
Section titled “Option D: Container Deployment”FROM node:20-alpine AS buildWORKDIR /appCOPY package*.json ./RUN npm ciCOPY . .RUN npm run build
FROM node:20-alpine AS runtimeWORKDIR /appENV NODE_ENV=productionCOPY --from=build /app/dist ./distCOPY --from=build /app/node_modules ./node_modulesCOPY --from=build /app/package.json ./CMD ["node", "dist/server/index.js"]Cache Strategy
Section titled “Cache Strategy”Hashed Assets (Long Cache)
Section titled “Hashed Assets (Long Cache)”/assets/entry-client-abc123.js → max-age=31536000, immutable/assets/index-def456.css → max-age=31536000, immutableWhy: Filenames include content hash. New content = new filename.
Entry Points (No Cache)
Section titled “Entry Points (No Cache)”/app/index.html → no-cache/admin/index.html → no-cacheWhy: Entry points reference hashed assets. Must always be fresh.
Invalidation Strategy
Section titled “Invalidation Strategy”Never invalidate:
- Hashed assets in
/assets/
Always invalidate:
- HTML entry points
- Unhashed files (if any)
CDN purge example:
# Only purge HTML filesaws cloudfront create-invalidation \ --distribution-id DISTID \ --paths "/*.html"Troubleshooting
Section titled “Troubleshooting”Missing CSS/JS
Section titled “Missing CSS/JS”Problem: Assets not loading in production
Check:
- Did client build run first?
- Are assets in
dist/client/{app}/assets/? - Is static middleware configured?
SSR Failing to Import
Section titled “SSR Failing to Import”Problem: Server can’t find SSR bundle
Check:
- SSR bundles in
dist/ssr/{app}/? ssr-manifest.jsonpresent?clientRootpoints todist/client/?
Stale Frontend
Section titled “Stale Frontend”Problem: Users seeing old code
Solution: Invalidate HTML on deployment:
# After deployaws cloudfront create-invalidation \ --distribution-id DISTID \ --paths "/*.html"Performance Optimisation
Section titled “Performance Optimisation”Code Splitting
Section titled “Code Splitting”Vite automatically code splits:
// Lazy load heavy componentsconst Dashboard = lazy(() => import("./Dashboard"));
function App() { return ( <Suspense fallback={<Loading />}> <Dashboard /> </Suspense> );}Compression
Section titled “Compression”Enable compression in production:
import fastifyCompress from "@fastify/compress";
await fastify.register(fastifyCompress, { global: true, encodings: ["gzip", "deflate"],});