Overview
Shipped’s real-time update system ensures that configuration changes are instantly reflected in the UI without requiring server restarts. This is achieved through a reactive architecture using Server-Sent Events (SSE) via ORPC streaming, file watching, and reactive state management.Architecture
The real-time update flow follows this path:Key Components
- File Watcher - Monitors config directory for changes
- Config Loader - Parses and validates config files
- SubscriptionRef - Effect’s reactive state container
- ORPC Stream - Type-safe SSE streaming
- Client State - Vue reactive refs for UI updates
File Watching
Implementation
Shipped useschokidar to watch the config directory for file changes:
server/services/config/loader/service.ts:124-137
Debouncing
File changes are debounced to avoid excessive reloads during rapid edits:server/services/config/loader/service.ts:147
Polling Mode
For network-mounted filesystems where native events may not work:server/config.ts:59-63
Configuration Reloading
Graceful Reload Strategy
When a config file changes, the system:- Detects the file change
- Reloads all config files
- Validates new configuration
- Updates SubscriptionRef if valid
- Keeps old config if validation fails
server/services/config/loader/service.ts:107-117
Never-Crash Philosophy
The reload process never crashes the app due to user errors:- Invalid YAML → Error logged, old config retained
- Schema validation failure → Warning logged, invalid items removed
- Missing files → Defaults used
- Watcher failure → Auto-retry with exponential backoff
server/services/config/loader/service.ts:145-146
ORPC Streaming
Server-Side Stream
The server exposes a streaming endpoint that merges config updates with keepalive pings:server/rpc/routes/config.ts:30-52
Stream Events
Two types of events flow through the stream:| Event Type | Payload | Purpose |
|---|---|---|
config | UserConfig | New configuration data |
ping | None | Keepalive signal (1s dev, 5s prod) |
SSE vs WebSockets
Shipped uses Server-Sent Events instead of WebSockets because:- Simpler reconnection - EventSource API handles it automatically
- HTTP-based - Works through proxies and firewalls
- One-way streaming - Perfect for config updates (server → client)
- Less overhead - No handshake negotiation
- Built-in browser support - No extra libraries needed
Client-Side Consumption
Stream Initialization
The client connects to the stream on app initialization:layers/01-base/app/plugins/02-config.ts
Automatic Reconnection
If the connection drops:onErrorhandler is called- Error is logged and displayed
- Reconnection attempt after 2 seconds
- Infinite retry loop ensures eventual reconnection
Hydration from SSR
On initial page load, config is injected into Nuxt’s payload to avoid a round-trip:- Zero flash of unstyled content - Config available immediately
- Faster initial render - No API call needed
- Progressive enhancement - SSE stream takes over after hydration
Configuration
Enable/Disable Streaming
Streaming can be toggled via config file:- Client receives config once on page load
- No SSE connection established
- Config changes require manual page refresh
server/services/config/loader/adapters/general.ts:9
Environment Variables
| Variable | Type | Default | Description |
|---|---|---|---|
SERVER_CONFIG_DIR | string | "config" | Config directory path |
SERVER_CONFIG_WATCH_POLLING | boolean | false | Use polling for file watching |
server/config.ts:52-65
Performance Characteristics
Latency Breakdown
Resource Usage
- Memory: ~1KB per connected client for stream state
- Network: ~5 bytes/sec per client (keepalive pings)
- CPU: Negligible when idle, less than 10ms for config reload
Monitoring
Client-Side Status
Access connection state via composable:Server-Side Logs
Relevant log messages:server/services/config/loader/service.ts:108-143
Troubleshooting
Config Changes Not Reflecting
-
Check streaming is enabled:
-
Check browser console for connection errors:
-
Verify file location: Files must be in configured config directory
-
Check file extension: Only
.yamlfiles are watched
Network File Systems
If running on Docker/NFS/etc where native events don’t work:High Latency Updates
If updates take >1 second:- Check debounce setting - Default 300ms is usually optimal
- Verify disk I/O - Slow disks delay file reads
- Check validation complexity - Large configs take longer
Best Practices
Atomic File Updates
To prevent partial reads during writes:Validation Before Deploy
Always validate config locally before deploying:Gradual Rollout
For production changes:- Deploy new config to staging
- Monitor for warnings in logs
- Verify UI updates correctly
- Deploy to production
Summary
Shipped’s real-time update system provides:- Sub-second latency from file save to UI update (~480ms)
- Zero downtime - No server restarts needed
- Automatic recovery - Reconnects on connection loss
- Graceful degradation - Invalid configs don’t crash the app
- SSR compatibility - Config available on initial render