COSMOS RSC uses Express as its web server. This guide shows you how to customize the server for authentication, middleware, API routes, and more.
Server architecture
The server implementation is in core/server/index.js. It handles:
- Static file serving from the build directory
- RSC payload generation and streaming
- Server action execution
- Server-side rendering via worker threads
- Cookie management
Basic server setup
The default server configuration:
const express = require('express');
const { BUILD_DIR } = require('./lib/constants');
const PORT = 8000;
const app = express();
// Serve webpack build artifacts
app.use(express.static(BUILD_DIR));
// Handle all routes
app.get('*splat', requestHandler);
app.post('*splat', requestHandler);
app.listen(PORT, () => {
logger.info(`Server is running on http://localhost:${PORT}`);
});
The *splat pattern matches all routes, allowing React to handle routing.
Understanding the request handler
The core request handler manages the entire rendering pipeline (core/server/index.js:42):
async function requestHandler(req, res) {
try {
// Parse incoming cookies
const incomingCookies = new Map(
req.headers.cookie?.split(';').map((cookie) => {
const [key, ...valueParts] = cookie.split('=');
return [key.trim(), { value: valueParts.join('=').trim() }];
}) ?? []
);
// Initialize app store
const appStore = {
metadata: { renderPhase: 'START' },
cookies: {
incoming: incomingCookies,
outgoing: new Map(),
},
};
runWithAppStore(appStore, async () => {
// Handle server actions (POST requests)
if (req.method === 'POST') {
// ... server action execution
}
// Set response cookies
if (cookies.outgoing.size > 0) {
res.setHeader('Set-Cookie', getCookieString([...cookies]));
}
// Load and render page
const pagePath = `../../app/pages${req.path}`;
const Page = require(pagePath).default;
const tree = createElement(Page, { searchParams: { ...req.query } });
// Generate RSC payload
const webpackMap = await getReactClientManifest();
const rscStream = renderToPipeableStream(payload, webpackMap);
// Return RSC payload or full HTML
if (req.headers.accept === 'text/x-component') {
res.setHeader('Content-Type', 'text/x-component');
rscStream.pipe(res);
} else {
// ... SSR streaming setup
}
});
} catch (error) {
logger.error(error);
res.status(500).send('Internal Server Error');
}
}
Adding custom middleware
You can add Express middleware before the route handlers:
Request logging
const morgan = require('morgan');
app.use(morgan('combined'));
app.use(express.static(BUILD_DIR));
CORS configuration
const cors = require('cors');
app.use(cors({
origin: 'https://your-domain.com',
credentials: true,
}));
Request body parsing
Do NOT add body parsing middleware globally. Server actions handle their own request parsing using busboy.
For custom API routes only:
app.use('/api/*', express.json());
app.use('/api/*', express.urlencoded({ extended: true }));
Session management
const session = require('express-session');
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === 'production',
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000, // 24 hours
},
}));
Adding API routes
Define custom API routes before the catch-all handler:
app.use(express.static(BUILD_DIR));
// Custom API routes
app.get('/api/health', (req, res) => {
res.json({ status: 'healthy', timestamp: Date.now() });
});
app.post('/api/webhook', express.json(), async (req, res) => {
const { event, data } = req.body;
// Process webhook
res.json({ received: true });
});
// RSC routes (must come after API routes)
app.get('*splat', requestHandler);
app.post('*splat', requestHandler);
Authentication
Token-based authentication
Add middleware to verify JWT tokens:
const jwt = require('jsonwebtoken');
function authenticateToken(req, res, next) {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'No token provided' });
}
try {
const user = jwt.verify(token, process.env.JWT_SECRET);
req.user = user;
next();
} catch (error) {
return res.status(403).json({ error: 'Invalid token' });
}
}
// Apply to specific routes
app.get('/api/protected/*', authenticateToken, requestHandler);
Session-based authentication
function requireAuth(req, res, next) {
if (!req.session.userId) {
return res.redirect('/login');
}
next();
}
// Apply to protected routes
app.get('/dashboard*', requireAuth, requestHandler);
app.get('/settings*', requireAuth, requestHandler);
Accessing auth in components
Pass authentication data through the request:
async function requestHandler(req, res) {
// ... existing code
// Pass user data to page component
const tree = createElement(Page, {
searchParams: { ...req.query },
user: req.user, // From authentication middleware
});
// ... rest of request handler
}
const helmet = require('helmet');
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"], // Required for RSC
styleSrc: ["'self'", "'unsafe-inline'"],
},
},
}));
app.use((req, res, next) => {
res.setHeader('X-Powered-By', 'COSMOS RSC');
res.setHeader('X-Content-Type-Options', 'nosniff');
next();
});
Error handling
Custom error pages
Add error handling middleware:
// 404 handler (before error handler)
app.use((req, res, next) => {
if (!req.route) {
res.status(404).send('Page not found');
} else {
next();
}
});
// Global error handler (last middleware)
app.use((err, req, res, next) => {
logger.error(err);
if (process.env.NODE_ENV === 'production') {
res.status(500).send('Internal Server Error');
} else {
res.status(500).send(`<pre>${err.stack}</pre>`);
}
});
Environment configuration
Port configuration
Use environment variables:
const PORT = process.env.PORT || 8000;
const HOST = process.env.HOST || 'localhost';
app.listen(PORT, HOST, () => {
logger.info(`Server is running on http://${HOST}:${PORT}`);
});
Environment-specific middleware
if (process.env.NODE_ENV === 'development') {
app.use(morgan('dev'));
app.use((req, res, next) => {
logger.info(`${req.method} ${req.path}`);
next();
});
}
if (process.env.NODE_ENV === 'production') {
app.use(compression());
app.set('trust proxy', 1);
}
Advanced customization
Modifying the Fizz worker
The Fizz worker handles SSR in a separate thread (core/server/lib/fizz-worker.js). You can modify it to:
- Add custom SSR context
- Inject additional data into the HTML
- Customize the bootstrap scripts
const htmlStream = renderToPipeableStream(
createElement(SSRApp, {
initialState: { tree },
rootLayout,
customContext: { /* your data */ },
}),
{
formState,
bootstrapScripts: [
'/client.js',
'/custom-init.js', // Your custom script
],
bootstrapModules: ['/hydrate.js'],
onShellReady: () => {
htmlStream
.pipe(injectRSCPayload(payloadConsumerRSCStream))
.pipe(writableStream);
},
}
);
Custom static file serving
Serve files from multiple directories:
app.use(express.static(BUILD_DIR));
app.use('/uploads', express.static(path.join(__dirname, 'uploads')));
app.use('/assets', express.static(path.join(__dirname, 'public')));
With caching:
app.use(express.static(BUILD_DIR, {
maxAge: process.env.NODE_ENV === 'production' ? '1y' : 0,
immutable: true,
}));
Favicon handling
The server has special handling for favicons (core/server/index.js:190):
if (req.path === '/favicon.ico') {
if (fs.existsSync(FAVICON_PATH)) {
res.sendFile(FAVICON_PATH);
} else {
res.status(404).end();
}
return;
}
Customize the favicon path in core/server/lib/constants.js.
Deployment considerations
Graceful shutdown
const server = app.listen(PORT, HOST, () => {
logger.info(`Server is running on http://${HOST}:${PORT}`);
});
process.on('SIGTERM', () => {
logger.info('SIGTERM signal received: closing HTTP server');
server.close(() => {
logger.info('HTTP server closed');
fizzWorker.terminate();
process.exit(0);
});
});
Health checks
app.get('/health', (req, res) => {
res.json({
status: 'healthy',
uptime: process.uptime(),
timestamp: Date.now(),
});
});
app.get('/ready', async (req, res) => {
try {
// Check if manifests are loaded
await getReactClientManifest();
await getReactSSRManifest();
res.json({ ready: true });
} catch (error) {
res.status(503).json({ ready: false, error: error.message });
}
});
Proxy configuration
Behind a reverse proxy:
app.set('trust proxy', true);
app.use((req, res, next) => {
// Access original IP
const clientIP = req.ip;
// Access original protocol
const protocol = req.protocol;
next();
});
Limitations and considerations
Do not modify the core request handler’s RSC rendering logic unless you understand the implications. Changes can break server actions, streaming, or hydration.
What you can safely customize:
- Add middleware before the request handler
- Add custom API routes
- Modify static file serving
- Customize error handling
- Add authentication and authorization
What to avoid:
- Parsing request bodies globally (breaks server actions)
- Modifying RSC stream handling
- Changing worker thread communication
- Altering manifest loading