@rip-lang/server
v1.4.28
Published
Bun-native content server: static sites, apps, HTTP proxy, and TCP/TLS passthrough
Maintainers
Readme
Rip Server - @rip-lang/server
Rip Server serves content: static sites, Rip apps, proxied HTTP services, and TCP/TLS services -- all from one Bun-native runtime.
Here, content means anything you want to make reachable over the network. That
may be a static site, a small Rip app, an HTTP service behind a proxy, a raw
TCP/TLS service, or a containerized tool you want to publish.
Rip Server replaces the usual app framework + process manager + reverse proxy
stack with one runtime. Start with rip server for a single app. Add
serve.rip when you want multi-host routing, shared apps, named proxy
backends, reusable certs, or TCP/TLS passthrough. The job stays the same:
take something you have and make it reachable safely, cleanly, and coherently.
Written entirely in Rip. Runs on Bun.
Serving Modes
- Static file and website serving
- Small app serving with Sinatra-style routing and
read()validators - HTTP / WebSocket reverse proxy
- Layer 4 TCP / TLS passthrough
Serving Guarantees
- Managed worker pools with rolling restarts
- HTTPS, ACME, certificate reuse, and SNI routing
- Proxy health checks, retry behavior, and upstream timeouts
- Shared-port HTTPS multiplexer
- Atomic config reload with verification and rollback
- Drain semantics, diagnostics, and control APIs
- Composable
serve.ripconfig
Quick Start
Install
bun add @rip-lang/serverSingle app
import { get, read, start } from '@rip-lang/server'
get '/' ->
'Hello from Rip Server!'
get '/users/:id' ->
id = read 'id', 'id!'
{ user: { id, name: "User #{id}" } }
start()Schema-validated routes + OpenAPI
A route registered with an input: schema validates the JSON body
before the handler runs — a 400 with structured {field, error,
message} issues goes out on failure, and the parsed (defaulted,
coerced) value is @input. Every such route contributes to an
auto-generated GET /openapi.json:
import { post, openapi } from '@rip-lang/server'
SignupInput = schema
email! email
password! string, 8..100
page? ~integer
post '/signup', input: SignupInput, ->
{ welcome: @input.email }
openapi title: 'My API', version: '1.0.0' # optional info blockThe read() validator vocabulary doubles as schema coercers: importing
@rip-lang/server registers every validator as a ~:name field type,
so chart! ~:id, ssn? ~:ssn, amount? ~:money, dob? ~:date all
normalize exactly like read 'x', 'id' does. registerValidator
registers both surfaces.
Run it:
rip serverAdd serve.rip
Add serve.rip next to your entry file when you want host routing, proxy
backends, automatic TLS, or TCP/TLS passthrough.
serve.rip
serve.rip is the one config file. Top-level keys: ssl, hsts, acme,
sites, apps, version, server.
export default
ssl: '/path/to/ssl'
sites:
dev: 'local.example.com'
prod: 'example.com'
apps:
web: 'dev prod'ssl
Path to a directory of .crt/.key file pairs. Rip scans the directory,
parses X509 SANs, and automatically matches certificates to hostnames.
sites
Named aliases mapping to hostnames:
sites:
dev: 'local.medlabs.health'
prod: 'medlabs.health'
incus: 'incus.trusthealth.com'apps
String-based app specs binding targets to sites:
apps:
medlabs: 'dev prod'
patient: '../patient dev prod'
zion: '/home/shreeve/www zion browse'
incus: 'https://10.0.0.50:8443 incus'
mysql: 'tcp://10.0.0.50:3306 db'Target kind is inferred from prefix:
| Prefix | Kind | Behavior |
|--------|------|----------|
| ./, ../, / | local | Rip app (if index.rip exists) or static files |
| (none) | local | Rip app in current directory or static files |
| http://, https:// | HTTP proxy | Reverse proxy to URL |
| tcp:// | TCP proxy | Layer 4 SNI passthrough |
Optional flags: browse (directory listing), spa (SPA fallback).
Path-scoped mounts
A site token can carry a mount path with site@/path, letting multiple apps
share one hostname at different path prefixes:
sites:
relay: 'relay.trusthealth.com'
apps:
mqtt: 'http://127.0.0.1:9001 relay@/mqtt'
repl: '/var/www/relay-repl relay@/repl browse'wss://relay.trusthealth.com/mqtt hits the MQTT proxy; https://relay.trusthealth.com/repl
serves the static directory. Omitting @/path keeps the old root-mount
behavior. TCP proxies cannot be path-mounted.
Rules:
- Each
(site, mountPath)pair may be bound by exactly one app;/plus subpath mounts on the same site are allowed - TCP targets must include a port
- HTTP proxy routes automatically enable WebSocket upgrade
- If a TCP stream route shares the HTTPS port, Rip switches into multiplexer mode: matching SNI is passed through at Layer 4, everything else falls through to Rip's internal HTTPS server
server
Optional global settings:
server:
hsts: true
acme: ['example.com']
timeouts:
connectMs: 2000
readMs: 30000Clean Example
export default
ssl: '/ssl'
hsts: true
acme: ['medlabs.health', 'trusthealth.com']
sites:
dev: 'local.medlabs.health'
prod: 'medlabs.health'
zion: 'dev.zionlabshare.com'
incus: 'incus.trusthealth.com'
db: 'db.trusthealth.com'
apps:
medlabs: 'dev prod'
patient: '../patient dev prod'
zion: '/home/shreeve/www zion browse'
incus: 'https://10.0.0.50:8443 incus'
mysql: 'tcp://10.0.0.50:3306 db'Operator Runbook
Start with an explicit config file
rip server --file=./serve.ripValidate config without serving
rip server --check-config
rip server --check-config --file=./serve.ripGenerate nginx.conf
rip server --nginx > /etc/nginx/nginx.conf && nginx -s reloadGenerates a complete, production-hardened nginx config from your serve.rip.
Generate a Caddyfile
rip server --caddy > Caddyfile && caddy reloadGenerates a Caddyfile from the same serve.rip. TCP/TLS passthrough routes
emit a caddy-l4 layer4 block, which
requires a custom build (xcaddy build --with github.com/mholt/caddy-l4).
Reload config safely
kill -HUP "$(cat /tmp/rip_myapp.pid)"or:
curl --unix-socket /tmp/rip_myapp.ctl.sock -X POST http://localhost/reloadBinding to ports 80 and 443
Ports below 1024 require elevated privileges. If you see a permission error on startup, grant Bun the capability once:
sudo setcap cap_net_bind_service=+ep $(which bun)This survives reboots but not Bun upgrades — re-run it after
bun upgrade.
Run as a systemd service
A minimal unit for running rip-server under systemd. Save as
/etc/systemd/system/rip-myapp.service:
[Unit]
Description=Rip Server (myapp)
After=network.target
[Service]
Type=simple
User=rip
WorkingDirectory=/srv/myapp
Environment=PATH=/home/rip/.bun/bin:/usr/local/bin:/usr/bin:/bin
ExecStart=/home/rip/.bun/bin/rip server -f /srv/myapp/serve.rip
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.targetThen:
sudo systemctl daemon-reload
sudo systemctl enable --now rip-myapp.service
journalctl -u rip-myapp -fNotes:
WorkingDirectoryshould be the directory holdingserve.rip— relativeapps:targets resolve against it.User=ripshould be a dedicated unprivileged account. To bind ports 80/443 without running as root, grant Bun the capability once (see "Binding to ports 80 and 443" above).rip server -randrip server --restartapply config changes and rebuild workers without restarting the unit, so most edits don't needsystemctl restart.
Diagnostics
curl http://localhost/diagnosticsThe config section reports:
- active
serve.rippath - version
- counts for apps, proxies, hosts, routes, and streams
- active route descriptions
- reload history and rollback details
Realtime
Rip Server includes built-in WebSocket pub/sub while your backend stays HTTP-oriented.
Security
Built-in protections include:
- conflicting
Content-Length+Transfer-Encodingrejection - multiple
Hostheader rejection - oversized URL rejection
- null-byte URL rejection
- path traversal protection for static serving
- CORS preflight policy unified with
cors()middleware (no reflect-any-origin during OPTIONS short-circuit) - per-IP token-bucket rate limiter with LRU eviction, O(1) per check
Middleware to opt into:
csrf()— double-submit cookie + HMAC-bound token. Recommended for any form/cookie-authenticated app:use sessions secret: process.env.SESSION_SECRET, encrypt: true use csrf secret: process.env.CSRF_SECRETHandlers receive the token as
c.csrfTokenfor embedding in HTML forms or fetch headers (X-CSRF-Token).sessions({encrypt: true})— AES-256-GCM (AEAD) sealed cookie. Required when storing tokens, internal IDs, or any non-public data in session. The default signed mode is tamper-proof but the payload is client-readable.
Roadmap
- Prometheus / OpenTelemetry metrics export
- Inline edge handlers
- HTTP response caching at the edge
License
MIT
