| name | roster-server |
| description | Virtual hosting for multiple HTTPS sites with Let's Encrypt SSL automation. Each domain gets isolated VirtualServer instance, supports Express/Socket.IO/custom handlers, static sites (index.html only, no Node), local HTTP dev mode with CRC32-based ports, automatic www redirects, and SNI certificate management. Static site logic is modular (lib/static-site-handler.js, lib/resolve-site-app.js). |
Quick Setup
Production
const Roster = require('roster-server');
const roster = new Roster({
email: 'admin@example.com',
wwwPath: '/srv/www',
greenlockStorePath: '/srv/greenlock.d',
local: true
});
roster.start();
Local Development
const roster = new Roster({
local: true,
wwwPath: './www'
});
roster.start().then(() => {
console.log('example.com:', roster.getUrl('example.com'));
});
Directory Structure
project/
├── greenlock.d/ # SSL certificates (auto-generated)
├── www/
│ ├── example.com/
│ │ └── index.js # Handler for example.com
│ ├── api.example.com/
│ │ └── index.js # Handler for subdomain
│ ├── static-site.com/ # Static site (no index.js)
│ │ ├── index.html
│ │ ├── css/
│ │ └── images/
│ └── *.example.com/
│ └── index.js # Wildcard: one handler for all subdomains
└── server.js # Your setup
Site resolution: For each domain folder, RosterServer looks for index.js / index.mjs / index.cjs first. If none exist but index.html exists, it serves the folder as a static site (modular handler in lib/static-site-handler.js). Node app takes precedence when both exist.
Handler Patterns
Node app: Each www/{domain}/index.js (or .mjs/.cjs) must export a function that receives httpsServer and returns a request handler.
Static site: If the domain folder has no index script but has index.html, RosterServer serves the folder as static files (GET / → index.html, other paths → file or 404, path-traversal protected). No code required.
Pattern 1: Basic HTTP Handler
module.exports = (httpsServer) => {
return (req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Hello World');
};
};
Pattern 2: Express App
const express = require('express');
module.exports = (httpsServer) => {
const app = express();
app.get('/', (req, res) => res.send('Hello'));
app.post('/api/data', (req, res) => res.json({ ok: true }));
return app;
};
Pattern 3: Socket.IO
const { Server } = require('socket.io');
module.exports = (httpsServer) => {
const io = new Server(httpsServer);
io.on('connection', (socket) => {
socket.on('message', (data) => io.emit('message', data));
});
return (req, res) => {
if (req.url && req.url.startsWith(io.opts.path)) return;
res.writeHead(200);
res.end('Socket.IO running');
};
};
Pattern 4: Manual Registration
roster.register('example.com', (httpsServer) => {
return (req, res) => {
res.writeHead(200);
res.end('Manual handler');
};
});
roster.register('api.example.com:8443', handler);
roster.register('*.example.com', handler);
roster.register('*.example.com:8080', handler);
Pattern 5: Static Site (no code)
Place only index.html (and assets) in www/example.com/. No index.js needed. RosterServer serves files with path-traversal protection; / → index.html, other paths → file or 404. Implemented in lib/static-site-handler.js and lib/resolve-site-app.js.
Pattern 6: Cluster-Friendly (external server)
const https = require('https');
const Roster = require('roster-server');
const roster = new Roster({
email: 'admin@example.com',
wwwPath: '/srv/www',
greenlockStorePath: '/srv/greenlock.d'
});
await roster.init();
const server = https.createServer({ SNICallback: roster.sniCallback() });
roster.attach(server);
process.on('message', (msg, connection) => {
if (msg === 'sticky-session:connection') {
server.emit('connection', connection);
}
});
Pattern 7: Cluster Production (single cert manager + workers)
const manager = new Roster({
email: 'admin@example.com',
greenlockStorePath: '/srv/greenlock.d',
wwwPath: '/srv/www'
});
manager.register('example.com', () => (req, res) => res.end('manager'));
await manager.start();
await manager.ensureCertificate('example.com');
const worker = new Roster({
email: 'admin@example.com',
greenlockStorePath: '/srv/greenlock.d',
wwwPath: '/srv/www',
autoCertificates: false
});
worker.register('example.com', () => (req, res) => res.end('worker'));
await worker.init();
const httpsServer = await worker.createServingHttpsServer({ servername: 'example.com' });
httpsServer.listen(4336);
Key Configuration Options
new Roster({
email: 'admin@example.com',
wwwPath: '/srv/www',
greenlockStorePath: '/srv/greenlock.d',
dnsChallenge: { ... },
local: false,
staging: false,
hostname: '::',
port: 443,
minLocalPort: 4000,
maxLocalPort: 9999,
filename: 'index',
basePath: '/srv'
})
Core API
roster.start()
Loads sites, generates SSL config, starts servers. Returns Promise<void>. Calls init() internally.
roster.init()
Loads sites, creates VirtualServers, prepares dispatchers — but creates no servers and calls no .listen(). Returns Promise<Roster>. Idempotent. Use this for cluster-friendly integration where an external manager owns the socket.
roster.requestHandler(port?)
Returns (req, res) => void dispatcher for a port (defaults to defaultPort). Requires init() first. Handles Host-header routing, www→non-www redirects, wildcard matching.
roster.upgradeHandler(port?)
Returns (req, socket, head) => void for WebSocket upgrade routing. Requires init() first.
roster.sniCallback()
Returns (servername, callback) => void TLS SNI callback that resolves certs from greenlockStorePath. With autoCertificates enabled (default), it can issue missing certs automatically. Production mode only. Requires init() first.
roster.ensureCertificate(servername)
Forces certificate availability for a domain and returns { key, cert }. With autoCertificates enabled (default), it issues certs automatically when missing.
roster.loadCertificate(servername)
Loads existing { key, cert } from greenlockStorePath without issuing new certificates.
roster.createManagedHttpsServer(options)
Creates a pre-wired https.Server with default cert, SNI callback, and attached request/upgrade handlers.
roster.createServingHttpsServer(options)
Serving-only helper for worker processes. Same as createManagedHttpsServer(..., ensureCertificate: false).
roster.attach(server, { port }?)
Convenience: wires requestHandler + upgradeHandler onto an external http.Server or https.Server. Returns this. Requires init() first.
roster.register(domain, handler)
Manually register a domain handler. Domain can include port: 'api.com:8443'. For wildcards use '*.example.com' or '*.example.com:8080'.
roster.getUrl(domain)
Get environment-aware URL:
- Local mode:
http://localhost:{port}
- Production:
https://{domain} or https://{domain}:{port}
- Returns
null if domain not registered. Supports wildcard-matched hosts (e.g. getUrl('api.example.com') when *.example.com is registered).
How It Works
Request Flow
- Request arrives → Dispatcher extracts
Host header
- Strips
www. prefix (301 redirect if present)
- Looks up domain → Gets
VirtualServer instance
- Routes to handler via
virtualServer.processRequest(req, res)
VirtualServer Architecture
Each domain gets isolated server instance that simulates http.Server:
- Captures
request and upgrade event listeners
- Complete separation between domains
- No configuration conflicts between apps
Port Assignment
Production: Default 443, custom via domain:port syntax
Local: CRC32 hash of domain → deterministic port in range 4000-9999
Reserved: Port 80 for ACME challenges only
SSL Management
- Automatic Let's Encrypt certificate generation
- Auto-renewal 45 days before expiration
- SNI support for multiple domains
- Custom ports reuse certificates via SNI callback
- Wildcard (
*.example.com): use folder www/*.example.com/ or roster.register('*.example.com', handler). Default DNS-01 plugin is local/manual acme-dns-01-cli; set dnsChallenge only when overriding provider integration.
Common Issues & Solutions
Port 443 in use: Use different port { port: 8443 }
Certificate failed: Check firewall (ports 80, 443), verify DNS, try staging: true
Site not found: Verify directory name matches domain. For Node: check index.js exports function. For static: ensure index.html exists (no index script).
Local port conflict: Adjust minLocalPort/maxLocalPort range
Socket.IO not working: Ensure handler checks io.opts.path and returns properly
Best Practices
- Test with staging first:
staging: true to avoid Let's Encrypt rate limits
- Use local mode for dev:
local: true for faster iteration
- Environment variables: Configure via
process.env for portability
- Error handling: Wrap handlers with try/catch, don't expose internals
- Socket.IO paths: Always check
req.url.startsWith(io.opts.path) in returned handler
- Port 80: Never use as HTTPS port (reserved for ACME)
Quick Examples
Full Production Setup
const Roster = require('roster-server');
const roster = new Roster({
email: process.env.ADMIN_EMAIL,
wwwPath: '/srv/www',
greenlockStorePath: '/srv/greenlock.d',
staging: process.env.NODE_ENV !== 'production'
});
roster.start().then(() => {
console.log('RosterServer running');
}).catch(err => {
console.error('Startup failed:', err);
process.exit(1);
});
Local Dev with Manual Registration
const roster = new Roster({ local: true, wwwPath: './www' });
roster.register('test.local', (server) => {
return (req, res) => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ status: 'ok', url: roster.getUrl('test.local') }));
};
});
roster.start();
Environment-Aware Configuration
const isProduction = process.env.NODE_ENV === 'production';
const roster = new Roster({
email: process.env.ADMIN_EMAIL || 'admin@example.com',
wwwPath: process.env.WWW_PATH || './www',
greenlockStorePath: process.env.SSL_PATH || './greenlock.d',
local: !isProduction,
staging: !isProduction,
minLocalPort: parseInt(process.env.MIN_PORT) || 4000,
maxLocalPort: parseInt(process.env.MAX_PORT) || 9999
});
roster.start();
Implementation Checklist
When implementing RosterServer: