@rip-lang/server 1.3.115 → 1.3.117

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +435 -622
  2. package/api.rip +4 -4
  3. package/control/cli.rip +221 -1
  4. package/control/control.rip +9 -0
  5. package/control/lifecycle.rip +6 -1
  6. package/control/watchers.rip +10 -0
  7. package/control/workers.rip +9 -5
  8. package/default.rip +3 -1
  9. package/docs/READ_VALIDATORS.md +656 -0
  10. package/docs/edge/CONFIG_LIFECYCLE.md +70 -32
  11. package/docs/edge/CONTRACTS.md +60 -69
  12. package/docs/edge/EDGEFILE_CONTRACT.md +258 -29
  13. package/docs/edge/M0B_REVIEW_NOTES.md +5 -5
  14. package/edge/config.rip +584 -52
  15. package/edge/forwarding.rip +6 -2
  16. package/edge/metrics.rip +19 -1
  17. package/edge/registry.rip +29 -3
  18. package/edge/router.rip +138 -0
  19. package/edge/runtime.rip +98 -0
  20. package/edge/static.rip +69 -0
  21. package/edge/tls.rip +23 -0
  22. package/edge/upstream.rip +272 -0
  23. package/edge/verify.rip +73 -0
  24. package/middleware.rip +3 -3
  25. package/package.json +2 -2
  26. package/server.rip +775 -393
  27. package/tests/control.rip +18 -0
  28. package/tests/edgefile.rip +165 -0
  29. package/tests/metrics.rip +16 -0
  30. package/tests/proxy.rip +22 -1
  31. package/tests/registry.rip +27 -0
  32. package/tests/router.rip +101 -0
  33. package/tests/runtime_entrypoints.rip +16 -0
  34. package/tests/servers.rip +262 -0
  35. package/tests/static.rip +64 -0
  36. package/tests/streams_clienthello.rip +108 -0
  37. package/tests/streams_index.rip +53 -0
  38. package/tests/streams_pipe.rip +70 -0
  39. package/tests/streams_router.rip +39 -0
  40. package/tests/streams_runtime.rip +38 -0
  41. package/tests/streams_upstream.rip +34 -0
  42. package/tests/upstream.rip +191 -0
  43. package/tests/verify.rip +148 -0
  44. package/tests/watchers.rip +15 -0
package/README.md CHANGED
@@ -2,42 +2,116 @@
2
2
 
3
3
  # Rip Server - @rip-lang/server
4
4
 
5
- > **A full-stack web framework and production edge server routing, middleware, multi-worker processes, hot reload, ACME auto-TLS, realtime WebSocket, observability, and mDNS written entirely in Rip with zero dependencies**
5
+ > **One runtime for your app and your edge — web framework, reverse proxy, TLS passthrough, and managed workers in a single zero-dependency Bun-native server**
6
6
 
7
- Rip Server is a unified web framework and application server. It provides
8
- Sinatra-style routing, built-in validators, file serving, and middleware
9
- composition for defining your API, plus multi-worker process management,
10
- rolling restarts, automatic TLS via Let's Encrypt, Bam-style realtime
11
- WebSocket support, runtime diagnostics, mDNS service discovery, and
12
- request load balancing for running it in production — all with zero external
13
- dependencies.
7
+ Rip Server replaces the combination of a web framework (Express, Hono), a
8
+ process manager (PM2), and a reverse proxy (Nginx, Caddy, Traefik) with one
9
+ unified runtime. Define your API with Sinatra-style routes, serve it with
10
+ managed multi-worker processes, proxy external services, and pass through raw
11
+ TLS for things like the Incus Web UI — all from a single `rip server` command
12
+ with zero external dependencies.
13
+
14
+ Written entirely in Rip. Runs on Bun.
14
15
 
15
16
  ## Features
16
17
 
18
+ ### App Framework
19
+ - **Sinatra-style routing** — `get`, `post`, `put`, `del` with path parameters and wildcards
20
+ - **Request validation** — 40+ built-in validators via [`read()`](docs/READ_VALIDATORS.md) eliminate API boilerplate
21
+ - **Middleware** — cors, sessions, compression, security headers, rate limiting, body limits
22
+ - **File serving** — Auto-detected MIME types via `@send`, SPA fallback support
23
+ - **Realtime WebSocket** — Pub/sub hub where your backend stays HTTP-only
24
+
25
+ ### Server Runtime
17
26
  - **Multi-worker architecture** — Automatic worker spawning based on CPU cores
18
27
  - **Hot module reloading** — Watches `*.rip` files by default, rolling restarts on change
19
- - **Rolling restarts** — Zero-downtime deployments
28
+ - **Rolling restarts** — Zero-downtime deployments with per-app worker pools
20
29
  - **Automatic HTTPS** — Shipped `*.ripdev.io` wildcard cert, or auto-TLS via Let's Encrypt ACME
21
- - **Realtime WebSocket** — Bam-style pub/sub hub where your backend stays HTTP-only
22
- - **Observability** — `/diagnostics` endpoint with request rates, latency percentiles, queue pressure
23
- - **Appliance-grade reliability** — Graceful shutdown, restart resilience, forced-exit safety net
30
+ - **Observability** — `/diagnostics` with request rates, latency percentiles, queue pressure
24
31
  - **mDNS discovery** — `.local` hostname advertisement
25
- - **Per-app worker pools** — Multi-app registry with host-to-app routing
26
- - **Request queue** — Built-in request buffering and load balancing
27
- - **Built-in dashboard** — Server status UI at `rip.local`
28
- - **Unified package** — Web framework + production server in one
32
+
33
+ ### Edge Proxy
34
+ - **HTTP/WS reverse proxy** — Route to external upstreams with health checks, circuit breakers, and retry
35
+ - **Layer 4 TLS passthrough** — SNI-based raw TCP routing for services that keep their own TLS
36
+ - **Unified port multiplexer** — Stream passthrough and normal HTTPS on the same `:443`
37
+ - **Atomic config reload** — Staged activation with post-verify and automatic rollback
38
+ - **Declarative routing** — Host/path/method matching with wildcard hosts and per-site overrides
39
+
40
+ ## What Can You Build?
41
+
42
+ - **A single API or web app** — `rip server` with zero config, one command
43
+ - **A multi-app platform** — Per-app worker pools, host routing, and rolling restarts via `config.rip`
44
+ - **A reverse proxy** — Route traffic to external HTTP/WS upstreams with health checks
45
+ - **A TLS passthrough edge** — Expose services like the Incus Web UI without terminating their TLS
46
+ - **A full edge runtime** — Replace Nginx, Caddy, or Traefik for single-host deployments via `Edgefile.rip`
29
47
 
30
48
  | Directory | Role |
31
49
  |-----------|------|
32
50
  | `api.rip` | Core framework: routing, validation, `read()`, `session`, `@send` |
33
51
  | `middleware.rip` | Built-in middleware: cors, logger, sessions, compression, security, serve |
34
52
  | `server.rip` | Edge orchestrator: CLI, workers, load balancing, TLS, mDNS |
35
- | `edge/` | Request path: forwarding, scheduling, metrics, registry, helpers, TLS, realtime |
53
+ | `edge/` | Request path and edge runtime: config, forwarding, metrics, registry, router, runtime, TLS, upstreams, verification |
36
54
  | `control/` | Management: CLI, lifecycle, workers, watchers, mDNS, events |
55
+ | `streams/` | Layer 4 stream routing: ClientHello parsing, SNI routing, stream runtimes, raw TCP upstreams |
37
56
  | `acme/` | Auto-TLS: ACME client, crypto, cert store, challenge handler |
38
57
 
39
58
  > **See Also**: For the DuckDB server, see [@rip-lang/db](../db/README.md).
40
59
 
60
+ ## Runtime Tiers
61
+
62
+ `rip server` is a graduated runtime. Start simple and add capabilities as you need them:
63
+
64
+ 1. **Single-app mode** — one app, zero config, one command
65
+ 2. **Managed multi-app mode** — many Rip apps with per-app worker pools via `config.rip`
66
+ 3. **Edge mode** — upstream proxying, host/path routing, TLS passthrough, staged reload, verification, and rollback via `Edgefile.rip`
67
+
68
+ ```mermaid
69
+ flowchart TD
70
+ SingleApp["rip server"] --> ManagedApps["config.rip"]
71
+ ManagedApps --> EdgeMode["Edgefile.rip"]
72
+ EdgeMode --> Upstreams["HTTP and WS upstream routes"]
73
+ EdgeMode --> Streams["Layer 4 TLS passthrough"]
74
+ EdgeMode --> ManagedRoutes["Managed app routes"]
75
+ ```
76
+
77
+ ## Architecture
78
+
79
+ ```mermaid
80
+ flowchart LR
81
+ Client[Client] --> Port[":443"]
82
+ Port -->|"HTTP/WS"| TLS["TLS termination"]
83
+ TLS --> Router["Route matching"]
84
+ Router -->|upstream| Proxy["Reverse proxy"]
85
+ Router -->|app| Workers["Worker pool"]
86
+ Port -->|"TLS passthrough"| SNI["SNI routing"]
87
+ SNI --> Upstream["Raw TCP upstream"]
88
+ ```
89
+
90
+ When a stream route shares the HTTPS port, Rip uses a multiplexer: matching SNI
91
+ traffic passes through at Layer 4, and everything else falls through to the
92
+ normal HTTP/WebSocket edge runtime. When no stream routes share the HTTPS port,
93
+ there is no multiplexer and no extra hop.
94
+
95
+ ## How Does Rip Server Compare?
96
+
97
+ | Capability | Rip Server | Caddy | Nginx | Traefik |
98
+ |---|---|---|---|---|
99
+ | HTTP reverse proxy | Yes | Yes | Yes | Yes |
100
+ | WebSocket proxy | Yes | Yes | Yes | Yes |
101
+ | Auto-TLS (ACME) | Yes | Yes | Plugin | Yes |
102
+ | Layer 4 TLS passthrough | Yes | Yes | Stream module | Yes |
103
+ | Managed app workers | Yes | No | No | No |
104
+ | Built-in app framework | Yes | No | No | No |
105
+ | Hot reload | Yes | Yes | Reload | Yes |
106
+ | Atomic rollback | Yes | No | No | No |
107
+ | Per-SNI multi-cert TLS | Yes | Yes | Yes | Yes |
108
+ | Config validation with hints | Yes | Partial | No | Partial |
109
+ | Zero dependencies | Yes | Go binary | C binary | Go binary |
110
+
111
+ Use Rip Server when you want one Bun-native runtime for both the app and the
112
+ edge. Use Caddy, Nginx, or Traefik when you specifically need mature HTTP
113
+ caching, multi-node service discovery, or their broader ecosystem integrations.
114
+
41
115
  ## Quick Start
42
116
 
43
117
  ### Installation
@@ -73,13 +147,13 @@ Create `index.rip`:
73
147
  ```coffee
74
148
  import { get, read, start } from '@rip-lang/server'
75
149
 
76
- get '/', ->
150
+ get '/' ->
77
151
  'Hello from Rip Server!'
78
152
 
79
- get '/json', ->
153
+ get '/json' ->
80
154
  { message: 'It works!', timestamp: Date.now() }
81
155
 
82
- get '/users/:id', ->
156
+ get '/users/:id' ->
83
157
  id = read 'id', 'id!'
84
158
  { user: { id, name: "User #{id}" } }
85
159
 
@@ -108,660 +182,371 @@ curl http://localhost/status
108
182
  # {"status":"healthy","app":"myapp","workers":5,"ports":{"https":443}}
109
183
  ```
110
184
 
111
- ## The `read()` Function
112
-
113
- A validation and parsing powerhouse that eliminates 90% of API boilerplate.
114
-
115
- ### Basic Patterns
116
-
117
- ```coffee
118
- # Required field (throws if missing)
119
- email = read 'email', 'email!'
120
-
121
- # Optional field (returns null if missing)
122
- phone = read 'phone', 'phone'
123
-
124
- # With default value
125
- role = read 'role', ['admin', 'user'], 'user'
126
-
127
- # Get entire payload
128
- data = read()
129
- ```
130
-
131
- ### Range Validation
132
-
133
- The `[min, max]` syntax works for both numbers and string lengths:
134
-
135
- ```coffee
136
- # Numbers: value range
137
- age = read 'age', 'int', [18, 120] # Between 18 and 120
138
- priority = read 'priority', 'int', [1, 10] # 1-10 range
139
-
140
- # Strings: length range
141
- username = read 'username', 'string', [3, 20] # 3-20 characters
142
- bio = read 'bio', 'string', [0, 500] # Up to 500 chars
143
-
144
- # Named parameters
145
- views = read 'views', 'int', min: 0 # Non-negative integer
146
- discount = read 'discount', 'float', max: 100 # Up to 100
147
- ```
148
-
149
- ### Enumeration Validation
150
-
151
- ```coffee
152
- # Must be one of these values
153
- role = read 'role', ['admin', 'user', 'guest']
154
- status = read 'status', ['pending', 'active', 'closed']
155
- ```
156
-
157
- ### Regex Validation
158
-
159
- ```coffee
160
- # Custom pattern matching
161
- code = read 'code', /^[A-Z]{3,6}$/
162
- ```
163
-
164
- ## Built-in Validators
165
-
166
- `@rip-lang/server` includes 37 validators for every common API need:
167
-
168
- ### Numbers & Money
169
- ```coffee
170
- id = read 'user_id', 'id!' # Positive integer (1+)
171
- count = read 'count', 'whole' # Non-negative integer (0+)
172
- price = read 'price', 'float' # Decimal number
173
- cost = read 'cost', 'money' # Banker's rounding to cents
174
- ```
175
-
176
- ### Text Processing
177
- ```coffee
178
- title = read 'title', 'string' # Collapses whitespace
179
- bio = read 'bio', 'text' # Light cleanup
180
- name = read 'name', 'name' # Trims and normalizes
181
- ```
182
-
183
- ### Contact Information
184
- ```coffee
185
- email = read 'email', 'email' # Valid email format
186
- phone = read 'phone', 'phone' # US phone → (555) 123-4567
187
- address = read 'address', 'address' # Trimmed address
188
- ```
189
-
190
- ### Geographic Data
191
- ```coffee
192
- state = read 'state', 'state' # Two-letter → uppercase
193
- zip = read 'zip', 'zip' # 5-digit zip
194
- zipplus4 = read 'zip', 'zipplus4' # 12345-6789 format
195
- ```
196
-
197
- ### Identity & Security
198
- ```coffee
199
- ssn = read 'ssn', 'ssn' # SSN → digits only
200
- sex = read 'gender', 'sex' # m/f/o
201
- username = read 'username', 'username' # 3-20 chars, lowercase
202
- ```
203
-
204
- ### Web & Technical
205
- ```coffee
206
- url = read 'website', 'url' # Valid URL
207
- ip = read 'ip_address', 'ip' # IPv4 address
208
- mac = read 'mac', 'mac' # MAC address
209
- color = read 'color', 'color' # Hex color → #abc123
210
- uuid = read 'user_id', 'uuid' # UUID format
211
- semver = read 'version', 'semver' # Semantic version
212
- ```
213
-
214
- ### Time & Date
215
- ```coffee
216
- date = read 'date', 'date' # YYYY-MM-DD
217
- time = read 'time', 'time' # HH:MM or HH:MM:SS (24-hour)
218
- time12 = read 'time', 'time12' # 12-hour with am/pm
219
- ```
220
-
221
- ### Boolean & Collections
222
- ```coffee
223
- active = read 'active', 'truthy' # true/t/1/yes/y/on → true
224
- inactive = read 'off', 'falsy' # false/f/0/no/n/off → true
225
- flag = read 'flag', 'bool' # Either → boolean
226
- tags = read 'tags', 'array' # Must be array
227
- config = read 'config', 'hash' # Must be object
228
- settings = read 'data', 'json' # Parse JSON string
229
- ids = read 'ids', 'ids' # "1,2,3" → [1, 2, 3]
230
- slug = read 'slug', 'slug' # URL-safe slug
231
- ```
232
-
233
- ### Custom Validators
234
-
235
- ```coffee
236
- import { registerValidator, read } from '@rip-lang/server'
237
-
238
- registerValidator 'postalCode', (v) ->
239
- if v =~ /^[A-Z]\d[A-Z] \d[A-Z]\d$/i
240
- _[0].toUpperCase()
241
- else
242
- null
243
-
244
- # Now use it
245
- code = read 'postal', 'postalCode!'
246
- ```
185
+ ## Edgefile Quick Start
247
186
 
248
- ## Routing
187
+ `Edgefile.rip` is the declarative edge config for the Bun-native edge runtime.
188
+ It gives you:
249
189
 
250
- ### HTTP Methods
190
+ - **HTTP/WS reverse proxy** — route traffic to external upstreams with health checks, retries, and circuit breakers
191
+ - **Layer 4 TLS passthrough** — route raw TLS connections by SNI to services that keep their own certificates and client-cert auth
192
+ - **Shared-port multiplexer** — stream passthrough and normal HTTPS on the same `:443`, automatically
193
+ - **Managed app routes** — host Rip apps with per-app worker pools alongside proxy routes
194
+ - **Atomic reload** — staged activation with post-verify and automatic rollback via `SIGHUP` or control API
195
+ - **Strict validation** — field-path errors and remediation hints, with `--check-config` for dry-run validation
196
+ - **Diagnostics** — config metadata, counts, reload history, and route descriptions in `/diagnostics`
251
197
 
252
- ```coffee
253
- import { get, post, put, patch, del, all } from '@rip-lang/server'
254
-
255
- get '/users' -> listUsers!
256
- post '/users' -> createUser!
257
- get '/users/:id' -> getUser!
258
- put '/users/:id' -> updateUser!
259
- patch '/users/:id' -> patchUser!
260
- del '/users/:id' -> deleteUser!
261
- all '/health' -> 'ok' # All methods
262
- ```
263
-
264
- ### Path Parameters
198
+ ### Canonical Shape
265
199
 
266
200
  ```coffee
267
- # Basic parameters
268
- get '/users/:id' ->
269
- id = read 'id', 'id!'
270
- { id }
271
-
272
- # Multiple parameters
273
- get '/users/:userId/posts/:postId' ->
274
- userId = read 'userId', 'id!'
275
- postId = read 'postId', 'id!'
276
- { userId, postId }
277
-
278
- # Custom patterns
279
- get '/files/:name{[a-z]+\\.txt}' ->
280
- name = read 'name'
281
- { file: name }
282
-
283
- # Wildcards
284
- get '/static/*', (env) ->
285
- { path: env.req.path }
201
+ export default
202
+ version: 1
203
+ edge:
204
+ hsts: true
205
+ timeouts:
206
+ connectMs: 2000
207
+ readMs: 30000
208
+ upstreams: {}
209
+ streamUpstreams: {}
210
+ apps: {}
211
+ routes: []
212
+ streams: []
213
+ sites: {}
286
214
  ```
287
215
 
288
- ### Route Grouping
216
+ ### Edgefile Field Reference
289
217
 
290
- ```coffee
291
- import { prefix } from '@rip-lang/server'
292
-
293
- prefix '/api/v1' ->
294
- get '/users' -> listUsers!
295
- get '/posts' -> listPosts!
218
+ | Field | Purpose |
219
+ |------|---------|
220
+ | `version` | Schema version (optional, defaults to `1`). |
221
+ | `edge` | Global edge settings: TLS, trusted proxies, timeouts, verification policy. |
222
+ | `upstreams` | Named external HTTP services and their targets. |
223
+ | `streamUpstreams` | Named raw TCP upstreams using `host:port` targets and optional `connectTimeoutMs`. |
224
+ | `apps` | Managed Rip apps with entry paths, hosts, worker counts, and env. |
225
+ | `routes` | Declarative route objects that choose exactly one action. |
226
+ | `streams` | Layer 4 stream routes that match by listen port and SNI. |
227
+ | `sites` | Per-host route groups and policy overrides. |
296
228
 
297
- prefix '/api/v2' ->
298
- get '/users' -> listUsersV2!
299
- ```
229
+ ### Route Shape
300
230
 
301
- ## Middleware
231
+ Common route fields:
302
232
 
303
- ### Built-in Middleware
233
+ - `host?: string` — exact host, wildcard host like `*.example.com`, or `*`
234
+ - `path: string` — must start with `/`
235
+ - `methods?: string[] | "*"`
236
+ - `priority?: number`
237
+ - `timeouts?: { connectMs, readMs }`
304
238
 
305
- Import from `@rip-lang/server/middleware`:
239
+ Each route must define exactly one action:
306
240
 
307
- ```coffee
308
- import { use } from '@rip-lang/server'
309
- import { cors, logger, compress, sessions, secureHeaders, timeout, bodyLimit } from '@rip-lang/server/middleware'
310
-
311
- # Logging
312
- use logger()
313
- use logger format: 'tiny' # Minimal output
314
- use logger format: 'dev' # Colorized (default)
315
- use logger skip: (c) -> c.req.path is '/health'
316
-
317
- # CORS
318
- use cors() # Allow all origins
319
- use cors origin: 'https://myapp.com' # Specific origin
320
- use cors origin: ['https://a.com', 'https://b.com']
321
- use cors credentials: true, maxAge: 86400
322
-
323
- # Compression (gzip/deflate)
324
- use compress()
325
- use compress threshold: 1024 # Min bytes to compress
326
-
327
- # Security headers
328
- use secureHeaders()
329
- use secureHeaders hsts: true, contentSecurityPolicy: "default-src 'self'"
330
-
331
- # Request limits
332
- use timeout ms: 30000 # 30 second timeout
333
- use bodyLimit maxSize: 1024 * 1024 # 1MB max body
334
- ```
241
+ - `upstream: 'name'`
242
+ - `app: 'name'`
243
+ - `static: '/dir'`
244
+ - `redirect: { to: '...', status: 301 }`
245
+ - `headers: { set, remove }`
335
246
 
336
- ### Middleware Options
247
+ WebSocket proxy routes use:
337
248
 
338
- | Middleware | Options |
339
- |------------|---------|
340
- | `logger()` | `format`, `skip`, `stream` |
341
- | `cors()` | `origin`, `methods`, `headers`, `credentials`, `maxAge`, `exposeHeaders`, `preflight` |
342
- | `compress()` | `threshold`, `encodings` |
343
- | `sessions()` | `secret`, `name`, `maxAge`, `secure`, `httpOnly`, `sameSite` |
344
- | `secureHeaders()` | `hsts`, `hstsMaxAge`, `contentSecurityPolicy`, `frameOptions`, `referrerPolicy` |
345
- | `timeout()` | `ms`, `message`, `status` |
346
- | `bodyLimit()` | `maxSize`, `message` |
249
+ - `websocket: true`
250
+ - `upstream: 'name'`
347
251
 
348
- ### Session Usage
252
+ ### Stream Shape
349
253
 
350
- ```coffee
351
- import { get, use, before, session } from '@rip-lang/server'
352
- import { sessions } from '@rip-lang/server/middleware'
254
+ Common stream fields:
353
255
 
354
- # Sessions parses cookies directly from request headers
355
- use sessions secret: process.env.SESSION_SECRET
256
+ - `listen: number` TCP port to bind
257
+ - `sni: string[]` — exact or wildcard SNI patterns
258
+ - `upstream: 'name'`
259
+ - `timeouts?: { handshakeMs, idleMs, connectMs }`
356
260
 
357
- before ->
358
- session.views ?= 0
359
- session.views += 1
261
+ When `listen` matches the active HTTPS port, Rip uses a shared-port
262
+ multiplexer. Matching SNI traffic is passed through to the stream upstream, and
263
+ everything else falls through to Rip's internal HTTPS server.
360
264
 
361
- get '/profile' ->
362
- { userId: session.userId, views: session.views }
363
-
364
- get '/login' ->
365
- session.userId = 123
366
- { loggedIn: true }
367
-
368
- get '/logout' ->
369
- delete session.userId
370
- { loggedOut: true }
371
- ```
372
-
373
- The `session` import works anywhere via AsyncLocalStorage — no `@` needed, works in helpers and nested callbacks.
374
-
375
- **Security note:** Without `secret`, sessions use plain base64 (dev only). With `secret`, sessions are HMAC-SHA256 signed (tamper-proof). Always set `secret` in production.
376
-
377
- ### CORS with Preflight
265
+ ### Example: Pure Proxy Shape
378
266
 
379
267
  ```coffee
380
- import { use } from '@rip-lang/server'
381
- import { cors } from '@rip-lang/server/middleware'
382
-
383
- # Handle OPTIONS early (before routes are matched)
384
- use cors origin: 'https://myapp.com', preflight: true
385
- ```
386
-
387
- ### Custom Middleware
268
+ export default
269
+ version: 1
270
+ edge:
271
+ hsts: true
272
+ trustedProxies: ['10.0.0.0/8']
273
+ verify:
274
+ requireHealthyUpstreams: true
275
+ requireReadyApps: true
276
+ includeUnroutedManagedApps: true
277
+ minHealthyTargetsPerUpstream: 1
278
+ upstreams:
279
+ app:
280
+ targets: ['http://app.incusbr0:3000']
281
+ healthCheck:
282
+ path: '/health'
283
+ routes: [
284
+ { path: '/ws', websocket: true, upstream: 'app' }
285
+ { path: '/*', upstream: 'app' }
286
+ ]
287
+ ```
288
+
289
+ ### Example: TLS Passthrough For Incus
388
290
 
389
291
  ```coffee
390
- # Authentication middleware
391
- use (c, next) ->
392
- token = @req.header 'Authorization'
393
- unless token
394
- return @json { error: 'Unauthorized' }, 401
395
- @user = validateToken!(token)
396
- await next()
397
-
398
- # Timing middleware
399
- use (c, next) ->
400
- start = Date.now()
401
- await next()
402
- @header 'X-Response-Time', "#{Date.now() - start}ms"
403
- ```
292
+ export default
293
+ version: 1
404
294
 
405
- ### Request Lifecycle Filters
295
+ edge: {}
406
296
 
407
- Three filters run at different stages: `raw` → `before` → handler → `after`
297
+ streamUpstreams:
298
+ incus:
299
+ targets: ['127.0.0.1:8443']
408
300
 
409
- ```coffee
410
- import { raw, before, after, get } from '@rip-lang/server'
411
-
412
- # Runs first — modify raw request before body parsing
413
- raw (req) ->
414
- if req.headers.get('X-Raw-SQL') is 'true'
415
- req.headers.set 'content-type', 'text/plain'
416
-
417
- skipPaths = ['/favicon.ico', '/ping', '/health']
418
-
419
- # Runs before handler (after body parsing)
420
- before ->
421
- @start = Date.now()
422
- @silent = @req.path in skipPaths
423
- unless @req.header 'Authorization'
424
- return @json { error: 'Unauthorized' }, 401
425
-
426
- # Runs after handler
427
- after ->
428
- return if @silent
429
- console.log "#{@req.method} #{@req.path} - #{Date.now() - @start}ms"
301
+ streams: [
302
+ { listen: 8443, sni: ['incus.example.com'], upstream: 'incus' }
303
+ ]
430
304
  ```
431
305
 
432
- **Note:** `raw` receives the native `Request` object (before parsing). `before` and `after` use `@` to access the context.
433
-
434
- **How `@` works:** Handlers are called with `this` bound to the context, so `@foo` is `this.foo`. This gives you Sinatra-like magic access to:
435
- - `@req` — Request object
436
- - `@json()`, `@text()`, `@html()`, `@redirect()`, `@send()` — Response helpers
437
- - `@header()` — Response header modifier
438
- - `@anything` — Custom per-request state
439
-
440
- **Imports that work anywhere** (via AsyncLocalStorage or Proxy):
441
- - `read` — Validated request parameters
442
- - `session` — Session data (if middleware enabled)
443
- - `env` — `process.env` shortcut (e.g., `env.DATABASE_URL`)
444
-
445
- ## Context Object
446
-
447
- Use `@` to access the context directly — no parameter needed:
306
+ ### Example: Unified Port For Apps + Incus
448
307
 
449
- ### Response Helpers
308
+ If a stream route listens on the active HTTPS port, Rip automatically switches
309
+ that port into multiplexer mode. Matching SNI traffic is passed through at
310
+ Layer 4, and everything else falls through to Rip's internal HTTPS server.
450
311
 
451
312
  ```coffee
452
- get '/demo' ->
453
- # JSON response
454
- @json { data: 'value' }
455
- @json { data: 'value' }, 201 # With status
456
- @json { data: 'value' }, 200, { 'X-Custom': 'header' }
457
-
458
- # Text response
459
- @text 'Hello'
460
- @text 'Created', 201
461
-
462
- # HTML response
463
- @html '<h1>Hello</h1>'
464
-
465
- # Redirect
466
- @redirect '/new-location'
467
- @redirect '/new-location', 301 # Permanent
313
+ export default
314
+ version: 1
315
+ edge: {}
468
316
 
469
- # Raw body
470
- @body data, 200, { 'Content-Type': 'application/octet-stream' }
317
+ streamUpstreams:
318
+ incus:
319
+ targets: ['127.0.0.1:8443']
471
320
 
472
- # File serving (auto-detected MIME type via Bun.file)
473
- @send 'public/style.css' # text/css
474
- @send 'data/export.json', 'application/json' # explicit type
321
+ streams: [
322
+ { listen: 443, sni: ['incus.example.com'], upstream: 'incus' }
323
+ ]
475
324
  ```
476
325
 
477
- ### Request Helpers
326
+ This means:
478
327
 
479
- ```coffee
480
- get '/info' ->
481
- # Path and query parameters — use read() for validation!
482
- id = read 'id', 'id!'
483
- q = read 'q'
484
-
485
- # Headers
486
- auth = @req.header 'Authorization'
487
- allHeaders = @req.header()
488
-
489
- # Body (async)
490
- json = @req.json!
491
- text = @req.text!
492
- form = @req.formData!
493
- parsed = @req.parseBody!
494
-
495
- # Raw request
496
- @req.raw # Native Request object
497
- @req.method # 'GET', 'POST', etc.
498
- @req.url # Full URL
499
- @req.path # Path only
500
- ```
328
+ - `https://incus.example.com` keeps Incus's own TLS and client-certificate flow
329
+ - every other HTTPS host on `:443` still terminates TLS in Rip and uses the
330
+ normal HTTP/WebSocket edge runtime
331
+ - if no stream route shares the HTTPS port, Rip stays on the normal direct
332
+ `Bun.serve()` path with no extra hop
501
333
 
502
- ### Request-Scoped State
334
+ ### Example: Mixed Apps + Upstreams
503
335
 
504
336
  ```coffee
505
- # Store data for later middleware/handlers
506
- use (c, next) ->
507
- @user = { id: 1, name: 'Alice' }
508
- @startTime = Date.now()
509
- await next()
510
-
511
- get '/profile' ->
512
- @json @user
337
+ export default
338
+ version: 1
339
+ edge: {}
340
+ upstreams:
341
+ api:
342
+ targets: ['http://api.incusbr0:4000']
343
+ apps:
344
+ admin:
345
+ entry: './admin/index.rip'
346
+ hosts: ['admin.example.com']
347
+ routes: [
348
+ { path: '/api/*', upstream: 'api' }
349
+ { path: '/admin/*', app: 'admin' }
350
+ ]
351
+ sites:
352
+ 'admin.example.com':
353
+ routes: [
354
+ { path: '/*', app: 'admin' }
355
+ ]
513
356
  ```
514
357
 
515
- ## File Serving
516
-
517
- ### `@send(path, type?)`
358
+ ### Example: Manual Wildcard TLS
518
359
 
519
- Serve a file with auto-detected MIME type. Uses `Bun.file()` internally for
520
- efficient streaming — the file is never buffered in memory.
360
+ Use manual cert/key paths for wildcard TLS. ACME HTTP-01 cannot issue
361
+ `*.domain` certificates.
521
362
 
522
363
  ```coffee
523
- # Auto-detected content type (30+ extensions supported)
524
- get '/css/*', -> @send "css/#{@req.path.slice(5)}"
525
-
526
- # Explicit content type
527
- get '/files/*', -> @send "uploads/#{@req.path.slice(7)}", 'application/octet-stream'
528
-
529
- # SPA fallback — serve index.html for all unmatched routes
530
- notFound -> @send 'index.html', 'text/html; charset=UTF-8'
364
+ export default
365
+ version: 1
366
+ edge:
367
+ cert: './certs/wildcard.example.com.crt'
368
+ key: './certs/wildcard.example.com.key'
369
+ hsts: true
370
+ upstreams:
371
+ web:
372
+ targets: ['http://web.incusbr0:3000']
373
+ routes: [
374
+ { path: '/*', upstream: 'web', host: '*.example.com' }
375
+ ]
531
376
  ```
532
377
 
533
- ### `mimeType(path)`
378
+ ## Host Blocks
534
379
 
535
- Exported utility that returns the MIME type for a file path:
380
+ Use `hosts` to group TLS certificates, routes, and static file serving
381
+ under each hostname. This is the cleanest way to configure multi-domain hosting.
536
382
 
537
- ```coffee
538
- import { mimeType } from '@rip-lang/server'
539
-
540
- mimeType 'style.css' # 'text/css; charset=UTF-8'
541
- mimeType 'app.js' # 'application/javascript'
542
- mimeType 'photo.png' # 'image/png'
543
- mimeType 'data.xyz' # 'application/octet-stream'
544
- ```
383
+ `hosts` defines per-domain configuration. `version` and `edge` are optional --
384
+ they default to `1` and `{}` respectively.
545
385
 
546
- ## Error Handling
547
-
548
- ### Custom Error Handler
386
+ ### Host Blocks Shape
549
387
 
550
388
  ```coffee
551
- import { onError } from '@rip-lang/server'
552
-
553
- onError (err, c) ->
554
- console.error 'Error:', err
555
- c.json { error: err.message }, err.status or 500
556
- ```
557
-
558
- ### Custom 404 Handler
389
+ export default
390
+ hosts:
391
+ '*.trusthealth.com':
392
+ cert: '/ssl/trusthealth.com.crt'
393
+ key: '/ssl/trusthealth.com.key'
394
+ root: '/mnt/trusthealth/website'
395
+ routes: [
396
+ { path: '/*', static: '.', spa: true }
397
+ ]
398
+
399
+ '*.zionlabshare.com':
400
+ cert: '/ssl/zionlabshare.com.crt'
401
+ key: '/ssl/zionlabshare.com.key'
402
+ routes: [
403
+ { path: '/api/*', upstream: 'api' }
404
+ { path: '/*', static: '/mnt/zion/dist', spa: true }
405
+ ]
406
+
407
+ upstreams:
408
+ api: { targets: ['http://127.0.0.1:3807'] }
409
+
410
+ streamUpstreams:
411
+ incus: { targets: ['127.0.0.1:8443'] }
412
+
413
+ streams: [
414
+ { listen: 443, sni: ['incus.trusthealth.com'], upstream: 'incus' }
415
+ ]
416
+ ```
417
+
418
+ ### How Host Blocks Work
419
+
420
+ - Each server block is keyed by hostname (exact or wildcard)
421
+ - Per-server `cert`/`key` enable SNI-based certificate selection (multiple domains on one port)
422
+ - `edge.cert` and `edge.key` are the optional fallback TLS identity
423
+ - A server with just `root` and no `routes` serves static files automatically
424
+ - A server with `passthrough` routes raw TLS to another address (no termination)
425
+ - `streams`, `streamUpstreams`, `upstreams`, and `apps` stay top-level and are shared
426
+ - Routes inside a server block inherit the server hostname automatically
427
+ - Hosts not matching any server block or stream route fall through to the default app
428
+
429
+ ### Host Block Fields
430
+
431
+ | Field | Purpose |
432
+ |-------|---------|
433
+ | `passthrough` | Raw TLS passthrough to `host:port` (no cert, no routes needed) |
434
+ | `cert` | TLS certificate path for this hostname (must pair with `key`) |
435
+ | `key` | TLS private key path for this hostname (must pair with `cert`) |
436
+ | `root` | Default filesystem base; if present with no `routes`, serves static files |
437
+ | `spa` | Server-level SPA fallback (boolean, used with `root`-only blocks) |
438
+ | `routes` | Array of route objects (optional if `root` or `passthrough` is set) |
439
+ | `timeouts` | Per-server timeout defaults |
440
+
441
+ ### Static Routes
442
+
443
+ Static routes serve files from disk with auto-detected MIME types:
559
444
 
560
445
  ```coffee
561
- import { notFound } from '@rip-lang/server'
562
-
563
- notFound (c) ->
564
- c.json { error: 'Not found', path: c.req.path }, 404
446
+ { path: '/*', static: '.', spa: true }
447
+ { path: '/assets/*', static: '/mnt/assets' }
565
448
  ```
566
449
 
567
- ## Server Options
450
+ - `static` is the directory to serve from (absolute path or relative to `root`)
451
+ - `root` on a route overrides the server-level `root`
452
+ - `spa: true` enables SPA fallback: when a file is not found and the request
453
+ accepts `text/html`, serves `index.html` instead
454
+ - Directory requests serve `index.html` if present
455
+ - Path traversal is rejected
568
456
 
569
- ### Basic Server
457
+ ### Redirect Routes
570
458
 
571
459
  ```coffee
572
- import { start } from '@rip-lang/server'
573
-
574
- start port: 3000
575
- start port: 3000, host: '0.0.0.0'
460
+ { path: '/old/*', redirect: { to: 'https://new.example.com', status: 301 } }
576
461
  ```
577
462
 
578
- ### Handler Only (for custom servers)
463
+ ## Operator Runbook
579
464
 
580
- ```coffee
581
- import { startHandler } from '@rip-lang/server'
465
+ ### Start With An Explicit Edgefile
582
466
 
583
- export default startHandler()
584
- ```
585
-
586
- ### App Pattern
587
-
588
- ```coffee
589
- import { App, get, post } from '@rip-lang/server'
590
-
591
- export default App ->
592
- get '/', -> 'Hello'
593
- post '/echo', -> read()
467
+ ```bash
468
+ rip server --edgefile=./Edgefile.rip
594
469
  ```
595
470
 
596
- ## Context Utilities
597
-
598
- ### ctx()
471
+ ### Validate Config Without Serving
599
472
 
600
- Get the current request context from anywhere (via AsyncLocalStorage):
601
-
602
- ```coffee
603
- import { ctx } from '@rip-lang/server'
604
-
605
- logRequest = ->
606
- c = ctx()
607
- console.log "#{c.req.method} #{c.req.path}" if c
608
-
609
- get '/demo' ->
610
- logRequest()
611
- { ok: true }
473
+ ```bash
474
+ rip server --check-config
475
+ rip server --check-config --edgefile=./Edgefile.rip
612
476
  ```
613
477
 
614
- ### resetGlobals()
615
-
616
- Reset all global state (routes, middleware, filters). Useful for testing:
617
-
618
- ```coffee
619
- import { resetGlobals, get, start } from '@rip-lang/server'
478
+ ### Reload Config Safely
620
479
 
621
- beforeEach ->
622
- resetGlobals()
480
+ Send `SIGHUP` to the long-lived server process:
623
481
 
624
- get '/test', -> { test: true }
482
+ ```bash
483
+ kill -HUP "$(cat /tmp/rip_myapp.pid)"
625
484
  ```
626
485
 
627
- ## Utility Functions
486
+ The default PID file is `/tmp/rip_<app-name>.pid`. If you use a custom socket
487
+ prefix, the PID file follows `/tmp/<socket-prefix>.pid`.
488
+ Reload success or rejection is printed to stderr and reflected in `/diagnostics`.
628
489
 
629
- ### isBlank
490
+ You can also trigger reload through the control socket:
630
491
 
631
- ```coffee
632
- import { isBlank } from '@rip-lang/server'
633
-
634
- isBlank null # true
635
- isBlank undefined # true
636
- isBlank '' # true
637
- isBlank ' ' # true
638
- isBlank [] # true
639
- isBlank {} # true
640
- isBlank false # true
641
- isBlank 'hello' # false
642
- isBlank [1, 2] # false
492
+ ```bash
493
+ curl --unix-socket /tmp/rip_myapp.ctl.sock -X POST http://localhost/reload
643
494
  ```
644
495
 
645
- ### toName
646
-
647
- Advanced name formatting with intelligent capitalization:
496
+ When file watching is enabled, application code changes use the same staged
497
+ reload path automatically. You can always reload Edgefile changes explicitly via
498
+ `SIGHUP` or the control socket API.
648
499
 
649
- ```coffee
650
- import { toName } from '@rip-lang/server'
500
+ ### Inspect Active Config And Diagnostics
651
501
 
652
- toName 'john doe' # 'John Doe'
653
- toName 'JANE SMITH' # 'Jane Smith'
654
- toName "o'brien" # "O'Brien"
655
- toName 'mcdonald' # 'McDonald'
656
- toName 'los angeles', 'address' # 'Los Angeles'
502
+ ```bash
503
+ curl http://localhost/diagnostics
657
504
  ```
658
505
 
659
- ### toPhone
506
+ The `config` block reports:
660
507
 
661
- US phone number formatting:
508
+ - config kind (`edge`, `legacy`, or `none`)
509
+ - active path
510
+ - schema version
511
+ - counts for apps, upstreams, routes, and sites
512
+ - active compiled route descriptions
513
+ - last result (`loaded`, `rejected`, etc.)
514
+ - reload timestamp
515
+ - last error, if any
516
+ - active edge runtime inflight/WS counts
517
+ - retired edge runtimes still draining after a config swap
518
+ - last reload attempt record
519
+ - bounded reload history with source, versions, result, and reason
520
+ - structured rollback/rejection code and details for failed reloads
662
521
 
663
- ```coffee
664
- import { toPhone } from '@rip-lang/server'
522
+ The top-level diagnostics payload also includes:
665
523
 
666
- toPhone '5551234567' # '(555) 123-4567'
667
- toPhone '555-123-4567' # '(555) 123-4567'
668
- toPhone '555.123.4567 x99' # '(555) 123-4567, ext. 99'
669
- toPhone '+1 555 123 4567' # '(555) 123-4567'
670
- ```
671
-
672
- ## Migration from Hono
524
+ - `upstreams`: per-upstream target counts and healthy/unhealthy target totals
673
525
 
674
- ### Before (Hono)
526
+ ### Verification Policy
675
527
 
676
- ```coffee
677
- import { Hono } from 'hono'
528
+ Use `edge.verify` to tune post-activate verification:
678
529
 
679
- app = new Hono()
680
- app.get '/users/:id', (c) ->
681
- id = c.req.param 'id'
682
- c.json { id }
530
+ - `requireHealthyUpstreams`: require referenced upstreams to prove healthy targets
531
+ - `requireReadyApps`: require referenced managed apps to have ready workers
532
+ - `includeUnroutedManagedApps`: also verify managed apps not directly referenced by a route
533
+ - `minHealthyTargetsPerUpstream`: minimum healthy targets required per referenced upstream
683
534
 
684
- export default app
685
- ```
535
+ ## Validation
686
536
 
687
- ### After (@rip-lang/server)
537
+ The `read()` function is a validation and parsing powerhouse that eliminates
538
+ 90% of API boilerplate. It supports 40+ built-in validators including `email`,
539
+ `phone`, `money`, `uuid`, `json`, `regex`, and composable object schemas.
688
540
 
689
541
  ```coffee
690
- import { get, read, startHandler } from '@rip-lang/server'
691
-
692
- get '/users/:id', ->
693
- id = read 'id', 'id!'
694
- { id }
695
-
696
- export default startHandler()
542
+ email = read 'email', 'email!' # required, validated
543
+ phone = read 'phone', 'phone' # optional, formatted
544
+ role = read 'role', ['admin', 'user'] # enum
545
+ age = read 'age', 'int', [18, 120] # range-checked
697
546
  ```
698
547
 
699
- ### API Compatibility
700
-
701
- | Hono | @rip-lang/server |
702
- |------|------------------|
703
- | `app.get(path, handler)` | `get path, handler` |
704
- | `app.post(path, handler)` | `post path, handler` |
705
- | `app.use(middleware)` | `use middleware` |
706
- | `app.basePath(path)` | `prefix path, -> ...` |
707
- | `c.json(data)` | `@json(data)` or return `{ data }` |
708
- | `c.req.param('id')` | `@req.param('id')` or `read 'id'` |
709
- | `c.req.query('q')` | `@req.query('q')` or `read 'q'` |
710
-
711
- ## Real-World Example
712
-
713
- ```coffee
714
- import { get, post, put, del, use, read, start, before, after, onError } from '@rip-lang/server'
715
- import { logger } from '@rip-lang/server/middleware'
716
-
717
- use logger()
718
-
719
- before ->
720
- @start = Date.now()
721
-
722
- after ->
723
- console.log "#{@req.method} #{@req.path} - #{Date.now() - @start}ms"
724
-
725
- onError (err) ->
726
- @json { error: err.message }, err.status or 500
727
-
728
- get '/', ->
729
- { name: 'My API', version: '1.0' }
730
-
731
- get '/users', ->
732
- page = read 'page', 'int', [1, 100]
733
- limit = read 'limit', 'int', [1, 50]
734
- users = db.listUsers! page or 1, limit or 10
735
- { users, page, limit }
736
-
737
- get '/users/:id', ->
738
- id = read 'id', 'id!'
739
- user = db.getUser!(id)
740
- unless user
741
- throw { message: 'User not found', status: 404 }
742
- { user }
743
-
744
- post '/users', ->
745
- email = read 'email', 'email!'
746
- name = read 'name', 'string', [1, 100]
747
- phone = read 'phone', 'phone'
748
- user = db.createUser! { email, name, phone }
749
- { user, created: true }
750
-
751
- put '/users/:id', ->
752
- id = read 'id', 'id!'
753
- email = read 'email', 'email'
754
- name = read 'name', 'string', [1, 100]
755
- user = db.updateUser! id, { email, name }
756
- { user, updated: true }
757
-
758
- del '/users/:id', ->
759
- id = read 'id', 'id!'
760
- db.deleteUser!(id)
761
- { deleted: true }
762
-
763
- start port: 3000
764
- ```
548
+ See the full [Validation Reference](docs/READ_VALIDATORS.md) for all 37+
549
+ validators, patterns, custom validators, and real-world examples.
765
550
 
766
551
  ## App Path & Naming
767
552
 
@@ -843,14 +628,18 @@ rip server [flags] [app-path]@<alias1>,<alias2>,...
843
628
  |------|-------------|---------|
844
629
  | `-h`, `--help` | Show help and exit | — |
845
630
  | `-v`, `--version` | Show version and exit | — |
631
+ | `--edgefile=<path>` | Load `Edgefile.rip` from an explicit path | Auto-discover or none |
632
+ | `--check-config` | Validate `Edgefile.rip` or `config.rip` and exit | Disabled |
846
633
  | `--watch=<glob>` | Watch glob pattern | `*.rip` |
847
634
  | `--static` | Disable hot reload and file watching | — |
848
635
  | `--env=<mode>` | Environment mode (`dev`, `prod`) | `development` |
849
636
  | `--debug` | Enable debug logging | Disabled |
637
+ | `--quiet` | Suppress startup URL output | Disabled |
850
638
  | `http` | HTTP-only mode (no HTTPS) | HTTPS enabled |
851
639
  | `https` | HTTPS mode (explicit) | Auto |
852
640
  | `http:<port>` | Set HTTP port | 80, fallback 3000 |
853
641
  | `https:<port>` | Set HTTPS port | 443, fallback 3443 |
642
+ | `--socket-prefix=<name>` | Override Unix socket / PID file prefix | `rip_<app-name>` |
854
643
  | `w:<n>` | Worker count (`auto`, `half`, `2x`, `3x`, or number) | `half` of cores |
855
644
  | `r:<reqs>,<secs>s` | Restart policy: requests, seconds (e.g., `5000,3600s`) | `10000,3600s` |
856
645
  | `--cert=<path>` | TLS certificate path | Shipped `*.ripdev.io` cert |
@@ -902,18 +691,7 @@ rip server --debug
902
691
  rip server r:5000,3600s
903
692
  ```
904
693
 
905
- ## Architecture
906
-
907
- ### Edge Planning Docs
908
-
909
- Current M0 artifacts for the unified edge/app evolution live in:
910
-
911
- - [Edge contracts](docs/edge/CONTRACTS.md)
912
- - [Config lifecycle](docs/edge/CONFIG_LIFECYCLE.md)
913
- - [Scheduler policy](docs/edge/SCHEDULER.md)
914
- - [Edgefile contract](docs/edge/EDGEFILE_CONTRACT.md)
915
- - [M0b review notes](docs/edge/M0B_REVIEW_NOTES.md)
916
- - [TLS spike findings](spikes/tls/FINDINGS.md)
694
+ ## Internals
917
695
 
918
696
  ### Self-Spawning Design
919
697
 
@@ -952,18 +730,34 @@ When `RIP_SETUP_MODE=1` is set, the same file runs the one-time setup phase. Whe
952
730
 
953
731
  ### Request Flow
954
732
 
955
- 1. **Main Process** receives HTTP/HTTPS request
956
- 2. **Server** selects available worker from pool
957
- 3. **Request** forwarded via Unix socket
958
- 4. **Worker** processes request, returns response
959
- 5. **Server** forwards response to client
733
+ ```mermaid
734
+ flowchart TD
735
+ request[Incoming Request] --> validate[ValidateRequest]
736
+ validate --> rateLimit[RateLimit]
737
+ rateLimit --> edgeRoute{Edge Route Match}
738
+ edgeRoute -->|upstream| upstream[Proxy To Upstream]
739
+ edgeRoute -->|app| worker[Forward To Managed Worker]
740
+ edgeRoute -->|no edge match| registry[Host Registry]
741
+ registry --> worker
742
+ worker --> response[Response]
743
+ upstream --> response
744
+ ```
745
+
746
+ At runtime:
747
+
748
+ 1. validate request and rate-limit early
749
+ 2. check the active edge route table
750
+ 3. proxy to an upstream if an `upstream` route matches
751
+ 4. otherwise resolve a managed app and forward to its worker pool
752
+ 5. keep retired edge runtimes alive while in-flight HTTP and websocket traffic drains
960
753
 
961
754
  ### Hot Reloading
962
755
 
963
756
  Two layers of hot reload work together by default:
964
757
 
965
- - **API changes** — The Manager watches for `.rip` file changes in the app directory and triggers rolling worker restarts (zero downtime, server-side).
758
+ - **API changes** — The Manager watches `.rip` files per app directory and triggers rolling restarts for the affected app pool only.
966
759
  - **UI changes** (`watch: true` in `serve` middleware) — Workers register their component directories with the Manager via the control socket. The Manager watches those directories and broadcasts SSE reload events to connected browsers (client-side).
760
+ - **Edge config changes** — `Edgefile.rip` changes use the staged reload path with verification, rollback, and bounded reload history.
967
761
 
968
762
  SSE connections are held by the long-lived Server process, not by recyclable workers, ensuring stable hot-reload connections. Each app prefix gets its own SSE pool for multi-app isolation.
969
763
 
@@ -1129,6 +923,9 @@ Returns:
1129
923
  "version": { "server": "1.0.0", "rip": "3.13.108" },
1130
924
  "uptime": 86400,
1131
925
  "apps": [{ "id": "myapp", "workers": 4, "inflight": 2, "queueDepth": 0 }],
926
+ "upstreams": [
927
+ { "id": "app", "targets": 2, "healthyTargets": 2, "unhealthyTargets": 0 }
928
+ ],
1132
929
  "metrics": {
1133
930
  "requests": 150000,
1134
931
  "responses": { "2xx": 148000, "4xx": 1500, "5xx": 500 },
@@ -1138,9 +935,41 @@ Returns:
1138
935
  "acme": { "renewals": 1, "failures": 0 },
1139
936
  "websocket": { "connections": 45, "messages": 12000, "deliveries": 89000 }
1140
937
  },
1141
- "gauges": { "workersActive": 4, "inflight": 2, "queueDepth": 0 },
938
+ "gauges": {
939
+ "workersActive": 4,
940
+ "inflight": 2,
941
+ "queueDepth": 0,
942
+ "upstreamTargetsHealthy": 2,
943
+ "upstreamTargetsUnhealthy": 0
944
+ },
1142
945
  "realtime": { "clients": 23, "groups": 8, "deliveries": 89000, "messages": 12000 },
1143
- "hosts": ["localhost", "myapp.ripdev.io"]
946
+ "hosts": ["localhost", "myapp.ripdev.io"],
947
+ "config": {
948
+ "kind": "edge",
949
+ "path": "/srv/Edgefile.rip",
950
+ "version": 1,
951
+ "counts": { "apps": 2, "upstreams": 1, "routes": 4, "sites": 1 },
952
+ "lastResult": "applied",
953
+ "lastError": null,
954
+ "lastErrorCode": null,
955
+ "lastErrorDetails": null,
956
+ "activeRouteDescriptions": [
957
+ "* /api/* proxy:api",
958
+ "admin.example.com /* app:admin"
959
+ ],
960
+ "activeRuntime": { "id": "edge-123", "inflight": 1, "wsConnections": 2 },
961
+ "retiredRuntimes": [],
962
+ "lastReload": {
963
+ "id": "reload-4",
964
+ "source": "control_api",
965
+ "oldVersion": 1,
966
+ "newVersion": 1,
967
+ "result": "applied",
968
+ "reason": null,
969
+ "code": null
970
+ },
971
+ "reloadHistory": []
972
+ }
1144
973
  }
1145
974
  ```
1146
975
 
@@ -1179,7 +1008,7 @@ Your app must provide a fetch handler. Three patterns are supported:
1179
1008
  ```coffee
1180
1009
  import { get, start } from '@rip-lang/server'
1181
1010
 
1182
- get '/', -> 'Hello!'
1011
+ get '/' -> 'Hello!'
1183
1012
 
1184
1013
  start()
1185
1014
  ```
@@ -1297,7 +1126,7 @@ dir = import.meta.dir
1297
1126
 
1298
1127
  use serve dir: dir, title: 'My App', watch: true
1299
1128
 
1300
- get '/css/*', -> @send "#{dir}/css/#{@req.path.slice(5)}"
1129
+ get '/css/*' -> @send "#{dir}/css/#{@req.path.slice(5)}"
1301
1130
 
1302
1131
  notFound -> @send "#{dir}/index.html", 'text/html; charset=UTF-8'
1303
1132
 
@@ -1322,22 +1151,6 @@ This gives you:
1322
1151
 
1323
1152
  See [Hot Reloading](#hot-reloading) for details on how the two layers (API + UI) work together.
1324
1153
 
1325
- ## Comparison with Other Servers
1326
-
1327
- | Feature | rip server | PM2 | Nginx | Caddy |
1328
- |---------|-----------|-----|-------|-------|
1329
- | Pure Rip | ✅ | ❌ | ❌ | ❌ |
1330
- | Zero Dependencies | ✅ | ❌ | ❌ | ❌ |
1331
- | Hot Reload | ✅ (default) | ✅ | ❌ | ❌ |
1332
- | Multi-Worker | ✅ | ✅ | ✅ | ❌ |
1333
- | Auto HTTPS (ACME) | ✅ | ❌ | ❌ | ✅ |
1334
- | Realtime WebSocket | ✅ (Bam-style) | ❌ | ❌ | ❌ |
1335
- | Runtime Diagnostics | ✅ (latency p50/p95/p99) | ❌ | ❌ | ❌ |
1336
- | mDNS | ✅ | ❌ | ❌ | ❌ |
1337
- | Zero Config | ✅ | ❌ | ❌ | ✅ |
1338
- | Built-in LB | ✅ | ❌ | ✅ | ✅ |
1339
- | Graceful Shutdown | ✅ | ✅ | ✅ | ✅ |
1340
-
1341
1154
  ## Multi-App Configuration (`config.rip`)
1342
1155
 
1343
1156
  If a `config.rip` file exists next to your entry file, the server loads it
@@ -1435,9 +1248,9 @@ Frames flow bidirectionally; close and error propagate between both ends.
1435
1248
 
1436
1249
  ## Roadmap
1437
1250
 
1438
- > *Planned improvements for future releases:*
1439
-
1440
1251
  - [ ] Prometheus / OpenTelemetry metrics export
1252
+ - [ ] Inline edge handlers (run small handlers without a worker process)
1253
+ - [ ] HTTP response caching at the edge
1441
1254
 
1442
1255
  ## License
1443
1256