roster-server
v2.4.6
Published
👾 RosterServer - A domain host router to host multiple HTTPS.
Downloads
5,250
Maintainers
Readme
👾 RosterServer
Because hosting multiple HTTPS sites has never been easier!
Welcome to RosterServer, the ultimate domain host router with automatic HTTPS and virtual hosting. Why juggle multiple servers when you can have one server to rule them all? 😉
✨ Features
- Automatic HTTPS with Let's Encrypt via Greenlock.
- Dynamic Site Loading: Just drop your Node.js apps in the
wwwfolder. - Static Sites: No code? No problem. Drop a folder with
index.html(and assets) and RosterServer serves it automatically—modular static handler with path-traversal protection and strict 404s. - Virtual Hosting: Serve multiple domains from a single server.
- Automatic Redirects: Redirect
wwwsubdomains to the root domain. - Zero Configuration: Well, almost zero. Just a tiny bit of setup.
- Bun compatible: Works with both Node.js and Bun.
📦 Installation
npm install roster-serverOr with Bun:
bun add roster-server🤖 AI Skill
You can also add RosterServer as a skill for AI agentic development:
npx skills add https://github.com/clasen/RosterServer --skill roster-server🛠️ Usage
Directory Structure
Your project should look something like this:
/srv/
├── greenlock.d/
├── roster/server.js
└── www/
├── example.com/
│ └── index.js
├── subdomain.example.com/
│ └── index.js
├── static-site.com/ # Static site: no index.js needed
│ ├── index.html
│ ├── css/
│ └── images/
├── other-domain.com/
│ └── index.js
└── *.example.com/ # Wildcard: one handler for all subdomains (api.example.com, app.example.com, etc.)
└── index.jsEach domain folder can have either:
- Node app:
index.js,index.mjs, orindex.cjs(exporting a request handler). - Static site:
index.html(and any assets). If no JS entry exists, RosterServer serves the folder as static files. Node takes precedence when both exist.
Wildcard DNS (*.example.com)
You can serve all subdomains of a domain with a single handler in three ways:
- Folder: Create a directory named literally
*.example.comunderwww(e.g.www/*.example.com/index.js). Any request toapi.example.com,app.example.com, etc. will use that handler. - Register (default port):
roster.register('*.example.com', handler)for the default HTTPS port. - Register (custom port):
roster.register('*.example.com:8080', handler)for a specific port.
Wildcard SSL certificates require DNS-01 validation (Let's Encrypt does not support HTTP-01 for wildcards). By default Roster uses acme-dns-01-cli through an internal wrapper (adds propagationDelay and modern plugin signatures).
For fully automatic TXT records with Linode DNS, set:
export ROSTER_DNS_PROVIDER=linode
export LINODE_API_KEY=...Then Roster creates/removes _acme-challenge TXT records automatically via api.linode.com.
If LINODE_API_KEY is present, this mode auto-enables by default for wildcard DNS-01.
Override with a custom plugin:
import Roster from 'roster-server';
const roster = new Roster({
email: '[email protected]',
wwwPath: '/srv/www',
greenlockStorePath: '/srv/greenlock.d',
dnsChallenge: { module: 'acme-dns-01-route53', /* provider options */ } // optional override
});Set dnsChallenge: false to disable. For other DNS providers install the plugin in your app and pass it. See Greenlock DNS plugins.
Setting Up Your Server
// /srv/roster/server.js
import Roster from 'roster-server';
const options = {
email: '[email protected]',
greenlockStorePath: '/srv/greenlock.d', // Path to your Greenlock configuration directory
wwwPath: '/srv/www' // Path to your 'www' directory (default: '../www')
};
const server = new Roster(options);
server.start();Your Site Handlers
Each domain has its own folder under www. You can use:
- Node app: Put
index.js(orindex.mjs/index.cjs) that exports a request handler function. - Static site: Put
index.htmland your assets (CSS, JS, images). RosterServer will serve files from that folder.GET /servesindex.html; other paths serve the file if it exists, or 404. Path traversal is blocked. If both an index script andindex.htmlexist, the script is used.
Examples
I'll help analyze the example files shown. You have 3 different implementations demonstrating various ways to handle requests in RosterServer:
- Basic HTTP Handler:
export default (httpsServer) => {
return (req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end('"Loco de pensar, queriendo entrar en razón, y el corazón tiene razones que la propia razón nunca entenderá."');
};
};- Express App:
import express from 'express';
export default (httpsServer) => {
const app = express();
app.get('/', (req, res) => {
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
res.send('"Loco de pensar, queriendo entrar en razón, y el corazón tiene razones que la propia razón nunca entenderá."');
});
return app;
}- Socket.IO Server:
import { Server } from 'socket.io';
export default (httpsServer) => {
const io = new Server(httpsServer);
io.on('connection', (socket) => {
console.log('A user connected');
socket.on('chat:message', (msg) => {
console.log('Message received:', msg);
io.emit('chat:message', msg);
});
socket.on('disconnect', () => {
console.log('User disconnected');
});
});
return (req, res) => {
if (req.url && req.url.startsWith(io.opts.path)) return;
res.writeHead(200);
res.end('Socket.IO server running');
};
};- Manual:
roster.register('example.com', (httpsServer) => {
return (req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end('"Loco de pensar, queriendo entrar en razón, y el corazón tiene razones que la propia razón nunca entenderá."');
};
});- Manual: Custom port:
roster.register('example.com:8080', (httpsServer) => {
return (req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end('"Mad with thought, striving to embrace reason, yet the heart holds reasons that reason itself shall never comprehend."');
};
});Running the Server
# With Node.js
node server.jsOr with Bun:
bun server.jsAnd that's it! Your server is now hosting multiple HTTPS-enabled sites. 🎉
🤯 But Wait, There's More!
Static Sites (index.html)
Domains under www that have no index.js/index.mjs/index.cjs but do have index.html are served as static sites. The logic lives in lib/static-site-handler.js and lib/resolve-site-app.js:
GET /andGET /index.htmlserveindex.html.- Any other path serves the file under the domain folder if it exists; otherwise 404 (strict, no SPA fallback).
- Path traversal (e.g.
/../) is rejected with 403. - Content-Type is set from extension (html, css, js, images, fonts, etc.).
No Express or extra dependencies—plain Node. At startup you’ll see (✔) Loaded site: https://example.com (static) for these domains.
Automatic SSL Certificate Management
RosterServer uses greenlock-express to automatically obtain and renew SSL certificates from Let's Encrypt. No need to manually manage certificates ever again. Unless you enjoy that sort of thing. 🧐
Redirects from www
All requests to www.yourdomain.com are automatically redirected to yourdomain.com. Because who needs the extra three characters? 😏
Dynamic Site Loading
Add a new site? Drop it into the www folder: either an index.js (or .mjs/.cjs) for a Node app, or an index.html (plus assets) for a static site. RosterServer picks the right handler automatically. Restart the server to load new sites—nodemon has your back. 😅
⚙️ Configuration Options
When creating a new RosterServer instance, you can pass the following options:
email(string): Your email for Let's Encrypt notifications.wwwPath(string): Path to yourwwwdirectory containing your sites.greenlockStorePath(string): Directory for Greenlock configuration.dnsChallenge(object|false): Optional override for wildcard DNS-01 challenge config. Default isacme-dns-01-cliwrapper withpropagationDelay: 120000,autoContinue: false, anddryRunDelay: 120000. Manual mode still works, but you can enable automatic Linode DNS API mode by settingROSTER_DNS_PROVIDER=linodeandLINODE_API_KEY. In automatic mode, Roster creates/removes TXT records itself and still polls public resolvers every 15s before continuing. Setfalseto disable DNS challenge. You can pass{ module: '...', propagationDelay: 180000 }to tune DNS wait time (ms). For Greenlock dry-runs (_greenlock-dryrun-*), delay defaults todryRunDelay(same aspropagationDelayunless overridden withdnsChallenge.dryRunDelayor envROSTER_DNS_DRYRUN_DELAY_MS). When wildcard sites are present, Roster creates a separate wildcard certificate (*.example.com) that usesdns-01, while apex/www stay on the regular certificate flow (typicallyhttp-01), reducing manual TXT records.staging(boolean): Set totrueto use Let's Encrypt's staging environment (for testing).autoCertificates(boolean): Enables automatic certificate issuance and renewal in production lifecycle. Default:true. Set tofalseonly if certificates are managed externally.certificateRenewIntervalMs(number): Renewal check interval whenautoCertificatesis enabled (minimum 60s, default 12h).local(boolean): Set totrueto run in local development mode.minLocalPort(number): Minimum port for local mode (default: 4000).maxLocalPort(number): Maximum port for local mode (default: 9999).
🏠 Local Development Mode
For local development and testing, you can run RosterServer in local mode by setting local: true. This mode is perfect for development environments where you don't need SSL certificates or production features.
When { local: true } is enabled, RosterServer Skips SSL/HTTPS: Runs pure HTTP servers instead of HTTPS.
Setting Up Local Mode
import Roster from 'roster-server';
const server = new Roster({
wwwPath: '/srv/www',
local: true, // Enable local development mode
minLocalPort: 4000, // Optional: minimum port (default: 4000)
maxLocalPort: 9999 // Optional: maximum port (default: 9999)
});
server.start();Port Assignment
In local mode, domains are automatically assigned ports based on a CRC32 hash of the domain name (default range 4000-9999, configurable via minLocalPort and maxLocalPort):
example.com→http://localhost:9465api.example.com→http://localhost:9388- And so on...
You can customize the port range:
import Roster from 'roster-server';
const roster = new Roster({
local: true,
minLocalPort: 5000, // Start from port 5000
maxLocalPort: 6000 // Up to port 6000
});Getting URLs
RosterServer provides a method to get the URL for a domain that adapts automatically to your environment:
Instance Method: roster.getUrl(domain)
import Roster from 'roster-server';
const roster = new Roster({ local: true });
roster.register('example.com', handler);
await roster.start();
// Get the URL - automatically adapts to environment
const url = roster.getUrl('example.com');
console.log(url);
// Local mode: http://localhost:9465
// Local subdomain: http://api.localhost:9465
// Production mode: https://example.comThis method:
- Returns the correct URL based on your environment (
local: true/false) - In local mode: Returns
http://localhost:{port}for apex domains andhttp://{subdomain}.localhost:{port}for subdomains - In production mode: Returns
https://{domain}(or with custom port if configured) - Handles
www.prefix automatically (returns same URL) - Returns
nullfor domains that aren't registered
Example Usage:
import Roster from 'roster-server';
// Local development
const localRoster = new Roster({ local: true });
localRoster.register('example.com', handler);
localRoster.register('api.example.com', handler);
await localRoster.start();
console.log(localRoster.getUrl('example.com'));
// → http://localhost:9465
console.log(localRoster.getUrl('api.example.com'));
// → http://api.localhost:7342
// Production
const prodRoster = new Roster({ local: false });
prodRoster.register('example.com', handler);
await prodRoster.start();
console.log(prodRoster.getUrl('example.com'));
// → https://example.com
// Production with custom port
const customRoster = new Roster({ local: false, port: 8443 });
customRoster.register('api.example.com', handler);
await customRoster.start();
console.log(customRoster.getUrl('api.example.com'));
// → https://api.example.com:8443🔌 Cluster-Friendly API (init / attach)
RosterServer can coexist with external cluster managers (sticky-session libraries, PM2 cluster, custom master/worker architectures) that already own the TCP socket and distribute connections. Instead of letting Roster create and bind servers, you initialize routing separately and wire it into your own server.
How It Works
roster.init() loads sites, creates VirtualServers, and prepares dispatchers — but creates no servers and calls no .listen(). You then get handler functions to wire into any http.Server or https.Server.
Quick Example: Sticky-Session Worker
import https from 'https';
import Roster from 'roster-server';
const roster = new Roster({
email: '[email protected]',
wwwPath: '/srv/www',
greenlockStorePath: '/srv/greenlock.d'
});
await roster.init();
// Create your own HTTPS server with Roster's SNI + routing
const server = https.createServer({ SNICallback: roster.sniCallback() });
roster.attach(server);
// Master passes connections via IPC — worker never calls listen()
process.on('message', (msg, connection) => {
if (msg === 'sticky-session:connection') {
server.emit('connection', connection);
}
});Production Pattern: Single Certificate Manager + Workers
For robust ACME behavior with cluster runtimes, run a single certificate manager process (primary) and keep workers in serving-only mode. This avoids challenge race conditions while keeping certificate lifecycle automatic.
// primary
const certManager = new Roster({
email: '[email protected]',
greenlockStorePath: '/srv/greenlock.d',
wwwPath: '/srv/www'
});
certManager.register('example.com', () => (req, res) => res.end('manager'));
await certManager.start(); // enables ACME challenge lifecycle
await certManager.ensureCertificate('example.com');
// worker
const workerRoster = new Roster({
email: '[email protected]',
greenlockStorePath: '/srv/greenlock.d',
wwwPath: '/srv/www',
autoCertificates: false
});
workerRoster.register('example.com', () => (req, res) => res.end('worker'));
await workerRoster.init();
const server = await workerRoster.createServingHttpsServer({ servername: 'example.com' });
server.listen(4336);Reference implementation: demo/https-cluster-configurable.js.
API Reference
roster.init() → Promise<Roster>
Loads sites, generates SSL config (production), creates VirtualServers and initializes handlers. Idempotent — calling it twice is safe. Returns this for chaining.
roster.requestHandler(port?) → (req, res) => void
Returns the Host-header dispatch function for a given port (defaults to defaultPort). Handles www→non-www redirects, wildcard matching, and VirtualServer dispatch.
roster.upgradeHandler(port?) → (req, socket, head) => void
Returns the WebSocket upgrade dispatcher for a given port. Routes upgrades to the correct VirtualServer.
roster.sniCallback() → (servername, callback) => void
Returns a TLS SNI callback. It resolves certificates from greenlockStorePath and, when autoCertificates is enabled (default), can issue missing certificates automatically. Not available in local mode.
roster.ensureCertificate(servername) → Promise<{ key, cert }>
Ensures a certificate exists for servername. With autoCertificates enabled (default), it issues missing certificates automatically and returns PEMs.
roster.loadCertificate(servername) → { key, cert }
Loads an existing certificate from greenlockStorePath without issuing new certificates. Useful for serving-only workers.
roster.createManagedHttpsServer({ servername, port?, ensureCertificate?, tlsOptions? }) → Promise<https.Server>
Creates an HTTPS server prewired with default cert, SNI callback, and request/upgrade routing. By default it ensures certificate issuance before returning.
roster.createServingHttpsServer({ servername, port?, tlsOptions? }) → Promise<https.Server>
Convenience alias for serving-only workers. Equivalent to createManagedHttpsServer(..., ensureCertificate: false).
roster.attach(server, { port }?) → Roster
Convenience method. Wires requestHandler and upgradeHandler onto server.on('request', ...) and server.on('upgrade', ...). Returns this for chaining.
Standalone Mode (unchanged)
roster.start() still works exactly as before — it calls init() internally, then creates and binds servers:
const roster = new Roster({ ... });
await roster.start(); // full standalone mode, no changes needed🧂 A Touch of Magic
You might be thinking, "But setting up HTTPS and virtual hosts is supposed to be complicated and time-consuming!" Well, not anymore. With RosterServer, you can get back to writing code that matters, like defending Earth from alien invaders! 👾👾👾
🤝 Contributing
Feel free to submit issues or pull requests. Or don't. I'm not your boss. 😜
If you find any issues or have suggestions for improvement, please open an issue or submit a pull request on the GitHub repository.
🙏 Acknowledgments
📄 License
The MIT License (MIT)
Copyright (c) Martin Clasen
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Happy hosting! 🎈
