Screen Recorder
Drop-in browser session recording: a single script tag captures DOM, mouse, scroll and input with rrweb, with no code changes in the host app. A small collector service stores recordings through a pluggable storage backend and ships with a built-in replay viewer. Uploads are numbered and sent in order with retries, so the server can store them exactly once and in sequence. Privacy is controlled per app on the server: input masking, blocked elements, URL parameter redaction and origin allow-lists. Packaged as a Docker image and deployed to Kubernetes through CI.
TypeScript · SvelteKit · rrweb · Docker · Kubernetes
Details
A self-hosted, Hotjar-style session recorder for web apps: one script tag to switch it on, one built-in viewer to watch the replays. Users are never asked to share their screen.
Why I built it
When a customer says “checkout didn’t work”, logs only tell part of the story. Session replay shows the rest.
- Reproduce bugs faster. See the clicks, scrolls and form steps that led to the problem.
- Understand real journeys. Find where people hesitate, rage-click or give up.
- Keep the data in-house. Recordings stay on your own infrastructure and no third-party vendor sees customer behaviour.
- Stay light. Replays are small because the page itself is recorded, not a video.
What it can do
- Full replay: mouse movement, clicks, scrolling, typing and page changes, replayed in a built-in player.
- Multi-page sessions: a visit that spans several pages plays back as one continuous session.
- Per-app setup: each app registers by name, with its own allowed sites, privacy rules and sampling.
- Privacy by default: typed values are masked, sensitive URL parameters are stripped, and chosen areas can be excluded entirely.
- Sampling: record every visitor, or only a percentage.
- Locked-down viewer: replays sit behind a login, and the viewer stays off until a password or SSO is configured.
- Works offline: the player is bundled, so no internet access is needed to watch.
- Zero-code onboarding: no SDK, endpoints or imports for the consuming team.
How it works
- The app loads
recorder.jsfrom the recorder service. - The script fetches its settings for that app.
- It uploads gzipped chunks every 5 seconds, and the service stores them per app and session.
- Replays are watched in the login-protected viewer.
The script is built on rrweb. It records changes to the page itself (DOM, mouse, scroll, input), so there is no permission prompt and replays stay small. It fails closed: if the script can’t fetch its settings, or the site’s origin isn’t allowed, it records nothing.
Adding it to an app
Register the app in the service config:
"my-app": {
"origins": ["https://my-app.example.com"],
"redactParams": ["token", "salt"]
}
Then add one tag to the page’s <head>. Remove it to switch recording off.
<script async
src="https://<recorder-host>/recorder.js"
data-name="my-app"></script>
If the app sets a Content-Security-Policy, the recorder host must be allowed in both script-src and connect-src. If the app is served over HTTPS, the recorder URL must be HTTPS too.
Privacy controls
maskAllInputs: typed values are replaced with***(on by default).maskSelects: hides the chosen dropdown option (on by default).redactParams: URL parameters such as tokens or IDs are removed in the browser and again before storage.data-no-record: add to any element to leave it out of the recording completely.- A mask class on an element masks the text inside it.
sampleRate: record only a share of visitors.- Cross-origin iframes (for example card-entry frames) are not captured.
Ordinary page text is captured, so what gets recorded should be reviewed with security and compliance before turning it on for real users.
Under the hood
- Stack
- SvelteKit (adapter-node), rrweb
- Runtime
- Docker image on Kubernetes (EKS Fargate)
- Storage
- Local volume,
<app>/<sessionId>.jsonl, behind a swappable storage interface - Limits
- 20 MB per upload, 200 MB per session (configurable)
- Sessions
- One per browser tab, kept for 30 minutes of inactivity
- Caching
recorder.jsrevalidates on every load, so a new deploy reaches users immediately
Roadmap
- Persistent storage (EFS or S3) so recordings survive restarts. Planned
- Horizontal scaling with a shared store. Later
- SSO in front of the viewer. Later