Deployment
Deploy hadars apps to any Node.js host, Bun server, or AWS Lambda.
Production server
# Build client + SSR bundles
hadars build
# Start the production server
hadars runFiles in static/ at your project root are served as-is (images, fonts, robots.txt, etc.). Build output — JS bundles, CSS, the HTML template — lands in .hadars/static/ and is served the same way. No extra config needed for either.
For multi-core usage, set workers: os.cpus().length in your config. hadars forks one process per worker via node:cluster. Each worker is an independent HTTP server sharing the same port.
import os from 'os';
import type { HadarsOptions } from 'hadars';
const config: HadarsOptions = {
entry: 'src/App.tsx',
port: 3000,
workers: os.cpus().length,
};
export default config;AWS Lambda
hadars apps run on AWS Lambda backed by API Gateway (HTTP API v2 or REST API v1).
File-based deployment
Run hadars build, then create a Lambda entry file that imports createLambdaHandler:
// lambda-entry.ts
import { createLambdaHandler } from 'hadars/lambda';
import config from './hadars.config';
export const handler = createLambdaHandler(config);Deploy the entire project directory (including the .hadars/ output folder) as your Lambda package. For production, front the function with CloudFront and route static paths from .hadars/static/ to an S3 origin.
Single-file bundle
hadars export lambda produces a completely self-contained .mjs file that requires no .hadars/ directory on disk. The SSR module and HTML template are inlined at build time.
# Outputs lambda.mjs in the current directory
hadars export lambda
# Custom output path
hadars export lambda dist/lambda.mjsThe command:
- Runs
hadars build - Generates a temporary entry shim with static imports of the SSR module and
out.html - Bundles everything into a single ESM
.mjswith esbuild
Deploy steps:
- Upload the output
.mjsas your Lambda function code - Set the handler to
index.handler - Upload
.hadars/static/assets to S3 and serve via CloudFront
Bundled API
import { createLambdaHandler, type LambdaBundled } from 'hadars/lambda';
// File-based (reads .hadars/ at runtime)
export const handler = createLambdaHandler(config);
// Bundled — zero I/O, for use with 'hadars export lambda' output
import * as ssrModule from './.hadars/index.ssr.js';
import outHtml from './.hadars/static/out.html';
export const handler = createLambdaHandler(config, { ssrModule, outHtml });The handler accepts both API Gateway HTTP API (v2) and REST API (v1) event formats. Binary responses (images, fonts, pre-compressed assets) are base64-encoded automatically.
Static assets on S3 + CloudFront
By default hadars serves JS bundles, CSS, and your static/ files from the same process as SSR. For production you'll want to offload them — upload to S3 and route asset requests through CloudFront so Lambda only handles HTML rendering.
1 — Upload to S3
Build first, then sync both output directories. Build artifacts are content-hashed so they can be cached indefinitely. Skip out.html — that's the SSR template used by Lambda at runtime, not a file browsers should fetch directly.
# Build client + SSR bundles
hadars build
# Upload build output — safe for long cache (content-hashed filenames)
aws s3 sync .hadars/static/ s3://your-bucket/ \
--exclude "out.html" \
--cache-control "public, max-age=31536000, immutable"
# Upload your own static files — shorter cache for files that may change
aws s3 sync static/ s3://your-bucket/ \
--cache-control "public, max-age=3600"2 — CloudFront distribution
Set up two origins and use path-pattern behaviours to split traffic:
S3 bucketS3 (OAC)JS, CSS, fonts, images, robots.txtAPI Gateway / Lambda URLHTTPSSR — all other requestsAdd a behaviour for each static file extension pointing to the S3 origin, and leave the default behaviour (*) pointing to Lambda:
*.jsS31 year (content-hashed)*.cssS31 year (content-hashed)*.woff2, *.woff, *.ttfS31 year*.png, *.jpg, *.svg, *.ico, *.webpS3as needed* (default)Lambda / API Gatewayno cache (or use hadars cache config)3 — CORS for fonts
If your app loads fonts from the S3/CloudFront origin, browsers will send a CORS preflight. Add a CORS response policy to your S3 bucket and forward the Origin header in the CloudFront behaviour for font extensions:
# S3 bucket CORS configuration (aws s3api put-bucket-cors)
{
"CORSRules": [{
"AllowedOrigins": ["https://your-domain.com"],
"AllowedMethods": ["GET", "HEAD"],
"AllowedHeaders": ["*"],
"MaxAgeSeconds": 86400
}]
}Environment variables
process.env is available in all server-side code (getInitProps, fetch, cache, etc.) and resolved at runtime per request.
Client-side code is an exception: process.env.* references are substituted at build time by rspack's DefinePlugin. They will not reflect env vars set after the build. Use the define option for values known at build time, or return runtime values from getInitProps to expose them to the client.
// hadars.config.ts — expose a build-time constant
const config: HadarsOptions = {
entry: 'src/App.tsx',
define: {
'__API_URL__': JSON.stringify(process.env.API_URL),
},
};
// In your component (replaced at build time, not runtime)
declare const __API_URL__: string;
const apiUrl = __API_URL__;
// For runtime env vars (e.g. Lambda env vars set after build)
export const getInitProps = async () => ({
apiUrl: process.env.API_URL, // resolved at request time on the server
});SSR cache
Cache SSR responses in-process with a TTL. Useful for pages with data that changes infrequently — skips the render entirely and serves the cached HTML string.
const config: HadarsOptions = {
entry: 'src/App.tsx',
// Cache every page by pathname for 60 seconds.
// Skip caching for authenticated users (cookie present).
cache: (req) => req.cookies.session
? null
: { key: req.pathname, ttl: 60_000 },
};