KeepPeek
Status: KeepPeek is in Alpha qualification and is not yet production-ready. The active Alpha gate tracks promotion evidence.
KeepPeek is a local-first network video recorder and WebRTC media gateway for IP cameras. It runs on Linux, macOS, and Windows and keeps camera media on hardware controlled by the user without requiring a vendor cloud relay.
The core service has a deliberately focused job:
- discover and connect supported cameras;
- ingest and record their encoded media without re-encoding it;
- store recordings as standard MP4 files;
- provide live and recorded media through WebRTC;
- keep camera, stream, recording, and server health observable;
- accept and store events from supported cameras or independent services;
- locate, review, and export bounded evidence clips;
- deliver rule-driven notifications and forward normalized events to MQTT.
KeepPeek does not require object detection or another AI service to record and review video. Camera-native analytics can publish events directly, while optional inference, transcoding, home automation, and commercial services connect through the open API. Recording remains useful when those services are absent or unavailable.
The server is written in Rust for predictable, memory-safe concurrent media handling. The first-party interface uses Svelte and runs in the browser, relying on the browser and operating system for compatible video decoding. KeepPeek remains codec-aware without bundling a video codec pack into the core service.
KeepPeek is developed with AI assistance through a human-directed process. AI is used as a tool for implementation, validation, documentation, and review; design decisions and accountability remain with people.
KeepPeek aims to remain a compact native service. Optional services own their detector runtimes, models, and codec dependencies, so an installation can choose the extra work it needs.
Reading the book
- Feature guide and navigation maps the current screens and settings to everyday tasks and distinguishes implemented features from unavailable controls.
- How KeepPeek works connects media, recording, events, persistent state, clients, and integrations in one overview.
- Users and design choices explains who KeepPeek is for and why its product, protocol, platform, licensing, and integration boundaries exist.
- Reporting bugs describes how to file a reproducible defect and where to take setup questions or feature proposals.
- Get started covers installation, persistent data, secrets, and the first camera.
- Authentication explains roles, credentials, and camera access; Camera controls covers discovery, setup, editing, and device operations.
- Live wall covers saved dashboards, layout sharing, kiosk display, and browser resource choices. Digital zoom explains local inspection controls.
- Camera and stream health defines the authoritative health model and evidence used across the server, API, metrics, and interface.
- Recording and evidence explains recording policies, coverage, event presentation, and durable evidence exports.
- Notifications and integrations covers server-owned rules, Pushover delivery, MQTT 5 forwarding, retries, and failure isolation.
- Native camera events, External analysis, and Home Assistant explain the independent integration paths.
- Visual configuration management and the configuration reference cover supported settings and safe edits.
- Backup and restore, Upgrades, Recording maintenance, and Archive recovery cover ongoing operation and recovery.
- Release readiness and known limitations separates automated checks from the physical mixed-fleet, recovery, and soak evidence required for Alpha promotion.
- Demo videos shows complete workflows against the real application.
- Open source and licensing credits the projects KeepPeek builds on and explains the AGPL server and MIT API split.
- Contributing explains scope review, interoperability contributions, AI-assisted work, pull requests, and validation.
Feature guide and navigation
Use this chapter to find the part of KeepPeek that owns a task. It describes the current application, including controls that are visible but unavailable. A design reference or generated API type does not, by itself, mean that a feature works in the running server.
KeepPeek remains in Alpha qualification. Availability also depends on your role, camera grants, device capabilities, recording coverage, and browser codec support.
Find the right screen
The desktop shell and mobile navigation lead to the same workflows. On a phone, More opens Settings; Administrator-only navigation is hidden for Users.
| Screen | Route | Use it for | Read next |
|---|---|---|---|
| Dashboard, also called Peek | / | Watch the fleet, select a saved dashboard, arrange cameras, and adjust wall display settings. | Live wall |
| Viewer | /viewer | Focus on a camera, switch feeds, listen to supported audio, and enter History. | Live wall, digital zoom |
| Keep | /keep | Investigate a recorded day, seek to an exact moment, inspect gaps and events, and create evidence exports. | Recording and evidence |
| Events | /events | Search stored events by time, camera, type, source, zone, confidence, image availability, and text. Review or dismiss events and open their recording context. | Recording and evidence |
| Cameras | /cameras | Inspect the inventory and open camera configuration. Start discovery or manual setup at /cameras/new. | Camera controls |
| Camera details | /camera | Inspect and edit the selected camera and use the device controls it advertises. | Camera controls |
| System health | /system-health | Diagnose recorder, storage, camera, stream, and integration health. Open an individual camera for detailed evidence. | Camera and stream health |
| Camera health | /system-health/camera/[cameraId] | Inspect lifecycle, stream progress, and recording evidence for one camera. | Camera and stream health |
| Settings | /settings | Manage server configuration, dashboards, storage, backups, access, events, and integrations. | Visual configuration management |
| Logs | /settings/logs | Inspect live server and browser logs and collect diagnostics. | Reporting bugs |
| Recording integrity | /recordings | Inspect recording coverage, gaps, and integrity before opening maintenance. | Recording maintenance |
| Recording maintenance | /recordings/maintenance | Inspect recording integrity and explicitly plan and confirm maintenance. | Recording maintenance |
| Initial setup | /setup | Complete the first-run setup workflow. | Get started |
Camera and event links carry the selected identity and, when appropriate, a timestamp. Keep those query parameters when sharing a deep link. A link does not grant its recipient permission to view the camera.
From live view to evidence
- Select a camera in Dashboard or Viewer and inspect whether its live stream is advancing.
- Use History to enter Keep, or open an event’s recording context from Events.
- Check the selected camera, date, and UTC time. A gap means recorded coverage is absent for that interval; a loaded player or thumbnail is not proof of continuous recording.
- Seek, play, and zoom to investigate. Digital zoom changes the browser view; PTZ moves a physical camera and has separate capability and permission requirements.
- Review the event, add the supported bookmark or evidence selection, and export the needed interval. Check the export result and retained coverage before relying on the file.
For controls, limits, time handling, and failure states, use Recording and evidence. For missing footage, start with camera health, then use maintenance or archive recovery when the evidence points to stored data.
Find a setting
Settings presents sections on desktop and a focused section index on mobile. Saving a draft, applying a change to a running camera, and restarting the server are distinct actions; inspect the reported result rather than assuming that a successful file write proves activation.
| Settings area | What it controls | Documentation |
|---|---|---|
| Dashboards | Saved layouts, camera placement, audience, and wall presentation/resource choices. | Live wall |
| Storage & retention | Recording paths, retention, pressure thresholds, policies, and maintenance. | Configuration reference, recording maintenance |
| Backup & restore | Backup lifecycle, uploads, restore planning, activation, and rollback evidence. | Backup and restore |
| Event sources | Read-only catalog and origin context. The live source registry is unavailable; configure native adapters through the camera/configuration workflows. | Native events, external analysis |
| Groups | The declared group model and its current unavailable runtime controls. | Groups: current status |
| Notifications | Rules, delivery destinations, history, and errors. | Notifications and integrations |
| Access | Named credentials, Administrator/User roles, camera grants, and session revocation. | Authentication |
| Integrations | MQTT configuration and connection evidence, alongside reference cards whose configuration is unavailable. Home Assistant setup is documented separately. | Notifications and integrations, Home Assistant |
| Appearance & time | Dark, Light, or Match system appearance and the browser’s time and reduced-motion context. | Visual configuration management |
| System & updates | Server version and health context and the explicit restart action. Follow the documented upgrade procedure to replace the server. | Upgrades |
| Logs & diagnostics | Live logs and downloadable diagnostic evidence. | Reporting bugs |
Set appearance and inspect system context
For appearance, open Settings > Appearance & time and select Dark, Light, or Match system. The choice applies immediately and is saved in this browser when local storage is available; it does not change another browser or the server configuration. Video surfaces stay dark. Without a saved preference, the interface starts in Dark mode.
The browser’s reported time zone and reduced-motion preference are context, not server settings. Server time zone, clock format, week-start preferences, and an alternate interface language are not configurable through this panel. Keep’s UTC selection and export timestamps retain their documented meaning. System & updates shows server evidence and supports restart; an automatic update check or update-channel selector is not available. Use the upgrade procedure.
Choose an event or integration path
- Record without analytics: No detector or notification service is required for recording and playback.
- Use camera analytics: Enable a supported native adapter and verify real events and its connection health. Camera support differs by model and firmware.
- Use external analysis: Run a separate service that subscribes to media and publishes events through the API. KeepPeek does not install or manage detector models in its core.
- Send alerts or automate: Match committed events with notification rules, use Pushover where configured, or forward normalized events through MQTT. A rule match and successful delivery are different states. The notification rule’s Forwarder action is currently unavailable; the MQTT integration is configured separately.
- Embed a camera in Home Assistant: Use the direct browser card with an appropriate named credential and browser-to-recorder connectivity.
The integration chapters explain their requirements and failure behavior. Protocol scenarios that describe future client possibilities are not an installation checklist for a shipped service.
Understand unavailable controls
Read the reason shown by an unavailable control. Do not interpret an unavailable count as zero, a saved configuration as a healthy connection, or advertised camera support as a successful operation. Browser Talk and live participant groups remain unavailable.
Camera access labels, dashboard audiences, and planned live participant groups are different concepts. Groups explains that distinction. The release-readiness chapter records remaining qualification boundaries; reporting bugs explains how to capture a reproducible failure safely.
Get started
Status: KeepPeek is in Alpha qualification and is not yet production-ready. The active Alpha gate tracks promotion evidence.
Choose the installation that fits the host. Linux has first-class Docker support; macOS and Windows use native packages and service integration.
Before installation
Choose a host with enough disk capacity for the camera bitrates and retention you need. KeepPeek stores camera media without transcoding; a browser still needs to support the selected codec. Start with a small camera set and measure it before increasing the workload.
Use the release page to select a build for the host. Record that version and retain its checksum information. By default, clients on private and local networks receive Administrator access without signing in; review the access policy before exposing the listener to other devices.
Linux with Docker
The published image supports linux/amd64 and linux/arm64. It runs KeepPeek directly as the
non-root user 65532 and listens on port 8081.
KeepPeek stores configuration, owner-only secrets, recordings, the recording catalog, thumbnails,
and logging settings under /config/keeppeek in the container. Mount /config from the host so
that data survives container replacement:
mkdir -p keeppeek-data
sudo chown 65532:65532 keeppeek-data
docker run --rm --name keeppeek -p 8081:8081 \
-v "$(pwd)/keeppeek-data:/config" \
ghcr.io/xnorpx/keeppeek:latest
The ownership command is for the newly created Linux directory. Use the permissions appropriate
to your existing storage if the directory already contains data. This example runs in the foreground
and removes the container when it stops; the mounted host data remains. Configure your deployment’s
restart policy for unattended operation. Select an explicit release tag or digest instead of the
moving latest tag when you need to reproduce the installation.
Open http://localhost:8081 after startup. If the container cannot create its configuration or
recordings, check the host mount and permissions for user 65532.
macOS
macOS packages target Apple Silicon (arm64). Download the release DMG and its matching .sha256
file, then verify the checksum:
shasum -a 256 -c keeppeek-<version>-macos-aarch64.dmg.sha256
Replace <version> with the downloaded version. Drag KeepPeek.app into /Applications and open
it once. The application installs and starts the per-user com.keeppeek launch agent, which starts
after that user logs in. Open http://localhost:8081 in a browser.
Signing and notarization depend on the release workflow’s configured credentials; check the chosen artifact rather than assuming every DMG has both. The macOS installation guide explains packaging and service logs. Use Upgrades and migrations when replacing an existing installation.
Windows
Windows releases provide signed x86-64 and ARM64 binaries and an installer. Choose the architecture
that matches the host and run the installer. Select Run KeepPeek as a Windows service for
background operation through the Service Control Manager. The service is named KeepPeekService.
Without that component, run the installed keeppeek.exe as a standalone process.
Open http://localhost:8081 after startup. To inspect the service from PowerShell:
sc.exe query KeepPeekService
sc.exe qc KeepPeekService
The service reads configuration in its own process account’s environment, which can differ from
your interactive %APPDATA%. Inspect the service identity and actual configuration location before
moving data between standalone and service operation. See
Upgrades and migrations and the
Windows service guide for
service lifecycle and logging.
Configuration and secrets
The first start creates the KeepPeek configuration directory for the selected installation. Reusable
private values belong in owner-only secrets.toml beside config.toml:
| Installation | Default secrets path |
|---|---|
| macOS | ~/Library/Application Support/keeppeek/secrets.toml |
| Linux | ${XDG_CONFIG_HOME:-$HOME/.config}/keeppeek/secrets.toml |
| Windows | %APPDATA%\keeppeek\secrets.toml |
| Docker | /config/keeppeek/secrets.toml |
Never add secrets.toml to source control, logs, screenshots, support bundles, or command
arguments. Enter camera credentials and other private values directly on the machine running
KeepPeek. See the
configuration reference for references,
precedence, and migration behavior.
These paths belong to the account running KeepPeek. --config <path> selects a different
configuration file with secrets.toml beside it. Inspect the effective recording paths separately;
omitted storage paths still use the operating system’s default KeepPeek directory.
On first startup, use a trusted-local browser to retrieve and save the Initial Administrator key if you need remote access. The browser shows it once. Create named credentials for individual remote users or integrations in Settings > Access & roles. See Authentication and access control.
Create a recovery backup
Review Settings storage and retention values before relying on the recorder. Confirm the actual recording, catalog, and thumbnail paths, available space, and retention policy. Defaults are starting values, not a sizing recommendation for every camera fleet.
Open Settings → Backup and restore as an Administrator and create a validated configuration
bundle before relying on the recorder. Download the ZIP and retain it separately from the recorder.
The ZIP contains exactly config.toml and plaintext secrets.toml, so handle it as sensitive data.
It intentionally omits recordings.db, sessions, MP4 recordings, and thumbnail JPEG bytes.
Test recovery on an isolated installation. Export uses GET /config/export; apply uses
POST /config/apply with the ZIP body. Apply validates both files, capacity, and target paths before
staging. Live files change only after a controlled restart. See
Backup and restore for the complete workflow and limits.
For footage recovery, also create and test a separate catalog and media archive. A successful ZIP export alone does not prove recordings can be recovered after disk loss.
Add the first camera
With no cameras configured, an Administrator can select Add camera directly from the Dashboard’s No cameras yet message. The same wizard is available from Cameras → Add camera. KeepPeek discovers common cameras where possible and keeps manual RTSP entry available. Do not trust discovery alone: authentication, the requested main or sub streams, keyframes, finalized MP4 recordings, and playback all need to validate.
Edit an existing camera from its Camera page. Use Settings only for server-wide configuration. Camera-specific credentials and stream choices belong with the camera.
Set a recording policy that matches the footage you need: the built-in default is event-boost, not
continuous recording of both streams. Open live view, then review a new finalized interval in
Keep. An incompatible browser codec is different from absent footage; retain a compatible
substream when the camera provides one.
The Users and design choices chapter describes supported vendor paths and the pragmatic camera compatibility policy.
After the first stream is verified, continue with Camera and stream health and Recording and evidence. Review Release readiness and known limitations before relying on the installation for evidence.
Complete the first-use check
- Verify live view, recorded playback, recording integrity, and a small downloaded evidence export.
- Test each remote credential from an address that is not trusted-local. Check allowed and denied cameras, and the separate dashboard audience.
- If you enable native events, external analysis, notifications, or MQTT, test a controlled event through its intended destination and inspect failures.
- Restart once and confirm recording resumes, durable settings remain, and remote users can sign in again. Pending notification and MQTT work does not survive that restart.
Authentication and access control
KeepPeek is local-first: cameras and recordings stay on the recorder, and KeepPeek does not expose itself through a cloud relay. Access depends on two decisions:
- Is the client on a trusted local network?
- If not, which remote credential and role does the client have?
Local clients use KeepPeek as Administrator without signing in. Remote clients must sign in with a named bearer credential or an explicitly configured OIDC/identity-proxy method, assigned either the Administrator or User role. External methods are opt-in; they do not change the trusted-local policy.
Treat local network configuration as an authentication boundary. Any device in a configured local range receives Administrator access.
First run
On first start, KeepPeek creates a cryptographically random Initial Administrator credential. Open the setup page from a local client and select Retrieve initial key. The key is displayed only for that operation.
Save it immediately using Copy or Download, then choose Hide permanently. KeepPeek does not log the key and cannot display it again. If it is lost, a local Administrator can rotate the Initial Administrator credential from Settings > Access & roles.
KeepPeek does not report remote access as ready until the initial key has been retrieved. Local camera setup remains available while remote access is pending.
Local and remote clients
KeepPeek classifies the effective client address before checking a credential.
- Local: the address is in
access.local_networks. The client becomes Administrator without a sign-in screen. - Remote: every other address. The client must provide an active, unexpired bearer credential or use an explicitly configured external authentication method.
- Unknown or malformed: treated as remote and denied without a valid credential.
The default local networks include IPv4 and IPv6 loopback, RFC 1918 private networks, IPv4 and IPv6 link-local networks, and IPv6 unique-local networks. IPv4-mapped IPv6 addresses are normalized before matching.
Container bridge networks commonly fall within 172.16.0.0/12, which is local by default. Narrow
the list if other containers on the host must not receive Administrator access.
Configure the policy in config.toml:
[access]
local_networks = ["127.0.0.0/8", "192.168.50.0/24", "::1/128"]
trusted_proxies = []
require_secure_remote = true
failed_authentication_limit = 5
failed_authentication_window_secs = 60
session_idle_timeout_secs = 1800
session_absolute_timeout_secs = 86400
max_sessions_per_principal = 64
max_sessions_per_address = 128
Setting local_networks replaces the defaults. Include the loopback ranges explicitly when you
customize it.
A VPN can use either policy:
- Add its client range to
local_networkswhen every VPN client should be Administrator. - Leave it out when each VPN client should sign in with an individual credential and role.
Restart KeepPeek after changing the network policy.
External sign-in
Administrators configure providers, exact origins, explicit role/camera mappings, identities, and browser-session revocation under Settings → Access. Browser cookies are secure, HTTP-only, host-only, and bound to one allowed origin. Provider tokens never reach the application UI. Sign-out revokes dependent connections; restart invalidates all external browser sessions.
Provider secrets remain references into secrets.toml. Changing the last usable
remote Administrator path requires fresh proof against the exact proposed
configuration and explicit confirmation on the original control connection.
The same protection applies to configuration ZIP restore. An unavailable provider,
stale proof, or failed write cannot activate the candidate.
See the field reference and deployment and recovery guide for TLS, reverse-proxy header sanitation, mixed-mode deadlines, and troubleshooting.
Roles
KeepPeek has two fixed roles.
| Operation | Administrator | User |
|---|---|---|
| View live and recorded media and events | Yes | Yes |
| Operate PTZ and presets | Yes | Yes |
| Use personal notification and review state | Yes | Yes |
| Configure cameras, storage, recording, and integrations | Yes | No |
| Export, delete, or change retention | Yes | No |
| View logs, health diagnostics, and security audit | Yes | No |
| Manage credentials and sessions | Yes | No |
The server enforces this policy for HTTP and every WebRTC control operation. Hidden navigation and controls make the User interface clearer, but they are not the security boundary.
The User operations above are limited to that credential’s allowed cameras. Custom roles are not currently available.
Camera and dashboard access
In Settings > Access & roles, choose the person icon beside a User credential to set User access. The default is Everything, including current and future groups and cameras. To restrict that user, choose Selected groups and cameras. Select camera groups, individual cameras, or both: either a matching group or an explicit camera selection grants access. Selecting neither grants no cameras. Existing explicit restrictions remain unchanged.
Camera groups use configuration namespaces. For example, [outdoor.front_door] puts the camera in
the outdoor group. These groups are separate from saved dashboards and live-sharing rooms.
Camera grants are enforced by the server for live and recorded media, event previews, notification inbox/history, recording coverage, and camera controls. A grant does not permit Administrator-only operations such as export or camera configuration. Saving permissions closes that User’s existing sessions; they sign in again with the same key. Stale saves return a conflict without discarding the draft.
Use the dashboard’s access controls to select its viewers separately. Sharing a dashboard does not grant its cameras. A User sees only allowed camera tiles and can select visible dashboards, but cannot modify or share the server-owned layouts. The complete layout remains intact for its Administrator even when a User’s view has fewer tiles.
Both settings live in the existing configuration: camera grants in each
access_credentials.credentials[].camera_access record and dashboards in peek_layouts. See the
configuration reference for field names and limits.
Trusted-local clients still receive Administrator access. To restrict someone on your LAN or VPN,
ensure their address is not in access.local_networks and have them sign in with a User credential.
Remote sign-in
Open the recorder through HTTPS and enter the issued UUID in the Remote sign-in form. The
browser keeps the key only in memory for the current page session. It is not placed in the URL,
rendered as hidden text, logged, or written to localStorage or sessionStorage.
Reloading or closing the page discards the key and requires sign-in again. Expired or revoked sessions cannot retrieve protected resources. The browser normally returns to sign-in; if a session change during page initialization leaves an expired-session error visible, reload and sign in again. See Session expired after a permission change.
API clients send the same credential in the HTTP header:
Authorization: Bearer 550e8400-e29b-41d4-a716-446655440000
KeepPeek never accepts credentials in query parameters. Do not put a key in a copied recording link, guest link, SDP document, or command line that may be retained in shell history.
Managing credentials
Open Settings > Access & roles as an Administrator. The credential table shows each identity’s name, description, role, status, last use, expiry, and revision without revealing its key.
Create
Choose New credential, enter a descriptive name, select Administrator or User, and optionally set an expiry. The new key is shown once. Store it before hiding the result.
Use a separate named credential for each person, browser installation, service, or integration. This makes audit records useful and allows one client to be revoked without affecting the others.
Rotate
Rotate a credential when its key may have been copied or exposed. Rotation returns a replacement key once, invalidates the previous revision, and closes work authenticated with the old key.
Disable and enable
Disable a credential to suspend it without deleting its identity or history. Disabling closes its active sessions. Enabling it increments the revision; clients must establish new sessions.
Revoke
Revocation is permanent. It closes active sessions and prevents reconnecting with that credential. A revoked credential cannot be enabled or rotated; create a replacement identity instead.
Credential metadata, camera grants, and SHA-256 verifiers are stored under [access_credentials]
in config.toml. Raw keys are not stored there. The compatibility Initial Administrator key remains
protected in owner-only secrets.toml.
Sessions and revocation
Each WebRTC session has a random opaque ID and is bound to its principal, role, effective client address, network classification, credential revision, and expiry. A session ID does not authorize a request by itself.
By default, sessions expire after 30 minutes without control activity or after 24 hours absolute, whichever comes first. Credential expiry can shorten that lifetime. KeepPeek allows up to 64 active sessions per principal and 128 per effective address.
Administrators can list and revoke sessions from Settings > Access & roles. A client can delete only its own session. Repeating deletion for an already closed session is safe.
Credential rotation, disable, revoke, or expiry closes matching WebRTC and authenticated log streams. KeepPeek checks expiration every 100 ms. WebRTC gets up to one second for graceful close, after which KeepPeek reports a warning. Normal cleanup releases PTZ movement, stored-media cursors, searches, discovery, and queued transfers owned by the session.
Reverse proxies and HTTPS
Prefer a private VPN for remote access. If KeepPeek is Internet-reachable, terminate HTTPS at a maintained reverse proxy, firewall the KeepPeek listener so clients cannot bypass that proxy, and trust only the proxy’s exact address.
Direct remote HTTP is rejected when require_secure_remote = true. A configured trusted proxy is
the declared TLS boundary: KeepPeek cannot inspect the client-side TLS connection after the proxy
terminates it.
KeepPeek accepts one forwarded-client contract: exactly one X-Forwarded-For field containing a
comma-separated chain of IP addresses without ports or quoted values. The proxy must remove any
client-supplied forwarding headers before setting it. KeepPeek walks the chain from right to left
and selects the first address that is not a trusted proxy.
Other forwarding headers make the contract ambiguous. Remove Forwarded, X-Real-IP,
X-Forwarded-Host, and X-Forwarded-Proto before forwarding to KeepPeek.
Example Nginx configuration:
location / {
proxy_pass http://127.0.0.1:8081;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header Forwarded "";
proxy_set_header X-Real-IP "";
proxy_set_header X-Forwarded-Host "";
proxy_set_header X-Forwarded-Proto "";
proxy_set_header Host $host;
}
Pair it with an exact proxy address:
[access]
trusted_proxies = ["127.0.0.1/32"]
require_secure_remote = true
Do not trust a proxy unless its listener, forwarding-header sanitation, network path, and bypass firewall are all under the same administrative control.
Audit and metrics
KeepPeek keeps a bounded security audit for remote authentication, credential changes, session lifecycle, denied commands, sensitive Administrator operations, and proxy-classification failures. Each record includes the principal, role, action, target, result, client classification, and UTC timestamp. It never contains a raw key, verifier, Authorization header, cookie, SDP, log payload, or media.
Audit history, active sessions, and credential last-use activity stay in memory and reset on
restart. Graceful shutdown does not persist them. Credential identities, verifiers, roles,
revisions, lifecycle metadata, and camera grants persist atomically in config.toml; this does
not make audit records durable.
If an installation requires durable security evidence, retain protected service logs outside the recorder and verify that the required actions are represented. Logs are not a guaranteed complete copy of the bounded audit. Collect required audit records before eviction or restart using an Administrator client and retain them in the installation’s external evidence system. Without that collection, the pre-restart audit cannot be recovered from a configuration backup.
GET /metrics exposes label-free counters and gauges for authentication successes and failures,
authorization denials, session creation and revocation, active sessions, and active credentials.
Logs, metrics, diagnostics, and audit access require Administrator.
Troubleshooting
A local browser shows Remote sign-in
Check the browser’s source address and access.local_networks. A forwarding header from an
untrusted peer intentionally forces remote classification. If a proxy is involved, configure its
exact address in trusted_proxies and send only the supported X-Forwarded-For contract.
Remote access returns HTTP 426
The request reached KeepPeek over unprotected direct HTTP while require_secure_remote is enabled.
Use HTTPS through a configured trusted proxy. A VPN does not by itself change the server’s address
classification or secure-transport check. A VPN client kept outside local_networks still needs the
supported secure boundary for remote sign-in; adding its range to local_networks instead grants
Administrator access to that range.
Remote sign-in is temporarily limited
Five failed attempts from one effective address within 60 seconds are rejected by default. Wait for the window to expire, verify the key, and confirm the credential is enabled, unexpired, and not revoked.
A credential works for viewing but not Settings
It has the User role. Issue an Administrator credential only when that client must change recorder configuration or manage security.
Session expired after a permission change
Saving User camera grants deliberately closes that credential’s existing sessions. Rotation, disable, revoke, and expiry also invalidate authenticated work. If a dashboard opened during that change remains at API session expired or was revoked, reload the page and sign in again with the current key. The current UI can retain this error during initialization even though the server continues to deny requests from the invalid session.
For a rotated credential, use the replacement key. A disabled, expired, or revoked credential will not work merely because the page was reloaded. Ask the recorder Administrator to inspect its status.
A shared dashboard is empty or has fewer cameras
Check both the dashboard audience and the User’s camera grants. Audience membership makes the dashboard visible; it does not grant cameras. Verify the intended group or individual cameras in User access, save the current revision, then sign the User in again. The Administrator’s complete layout is retained even when the User sees fewer tiles.
A supposedly restricted person still has Administrator access
Check their effective source address first. A trusted-local request receives Administrator access
before a remote key is considered. Exclude that client’s network from local_networks if it must
use a restricted User credential, while retaining the local recovery access you need.
A key was lost or exposed
From a local Administrator session, rotate the affected credential. Store the replacement key before hiding it, then update only the client or integration that owns that named credential.
Verify access before deployment
Create a disposable User credential and test from a genuinely remote-classified address. Check an allowed camera and a denied camera through live view, Keep, event images, and controls. Confirm that Settings, exports, and diagnostics remain unavailable to that User. Test a dashboard whose audience includes the User but whose tiles include a denied camera.
Change the grants and confirm the old session loses access. Reconnect and verify the new scope, then disable or revoke the test credential when finished. A successful local browser check does not test remote restrictions. Record the proxy or VPN topology with your release qualification evidence.
Credentials and grants are included in configuration ZIPs. Before applying a ZIP from another recorder, ensure you can use its restored access policy and retain a local recovery path. Sessions, last-use activity, and audit history are not restored by that ZIP. See what survives a restart.
Camera controls
As an Administrator, open Cameras, select a camera, and use its overview and control panels to inspect capabilities and operate supported devices. A live picture, a manufacturer name, or a capability badge alone does not prove that a camera command will succeed. KeepPeek reports command failures separately from camera and stream health.
The Cameras navigation and configuration editors are Administrator workflows. The API also allows authorized Users to read motion-detection status and issue supported PTZ commands for their granted cameras. Changing detector configuration requires Administrator access. Camera grants and advertised capabilities still apply to each operation. See Authentication and access for the trusted-local policy and camera grants.
Move a PTZ camera
On a supported camera, the PTZ panel provides directional movement, optical zoom, Stop PTZ, and existing presets.
- Check the live image and select the intended camera.
- Press and hold a direction or optical zoom control. Release it to request a stop. Keyboard users can focus a movement button and hold Space or Enter.
- Use Stop PTZ to stop movement explicitly.
- Select an existing preset to recall that device position. Preset creation and deletion are not available through the shared KeepPeek controls; manage them in the camera’s own interface.
Continuous movement belongs to the controlling WebRTC connection. KeepPeek requests a stop on disconnect and after an uncertain movement acknowledgement. If a stop cannot be confirmed, it reports the failure and retains ownership instead of treating the camera as safely idle. Check the camera directly before another operation. Keep the device’s own movement timeout enabled.
Hikvision/Annke controls use ISAPI HTTP. The current Reolink control adapter uses Reolink HTTP even
when the recording backend is reo-proto. Their movement speed scales differ. Reolink HTTP controls
currently address channel 0; do not assume arbitrary NVR channel control is supported. A missing
PTZ axis or unavailable preset is not supplied by software emulation.
For magnifying a picture without moving the device, use Digital zoom and pan. Digital zoom also works on recordings and event images.
Enable or disable motion detection
The camera detail page’s Motion detection > Enabled switch changes the camera’s detector configuration. It is independent of whether motion is occurring at this instant.
KeepPeek reads the current alarm configuration, changes the enabled field, and reads it back. Unrelated settings such as masks and sensitivity remain intact. Wait for the result before making another change. An error or mismatched read-back is a failed operation, and KeepPeek does not automatically reboot the camera to apply it.
Three settings have different purposes:
| Setting | Effect |
|---|---|
| Motion detection enabled | Turns the device’s detector on or off where supported. |
| Store generic motion events | Controls whether KeepPeek stores unclassified motion notifications. |
| Camera recording policy | Chooses which stream KeepPeek records and whether events boost quality. |
Turning off generic motion retention does not turn off the detector or native subscriptions. Smart person or vehicle classifications may still depend on those subscriptions. Configure detector regions, schedules and vendor-specific target filters in the camera interface. See Native camera events and Recording and evidence.
Audio and unsupported controls
Listening to an available camera audio stream is separate from sending microphone audio back to its speaker. Talk is not implemented in the browser application, including on devices that advertise a speaker or two-way-audio capability. KeepPeek’s reusable vendor libraries contain audio transport support, but this does not provide an application talk session.
Relative PTZ movement, preset save/delete and vendor-specific focus, guard, sensitivity or smart rule editing are not shared browser controls. Disabled controls describe unavailable application operations; they are not a reason to grant broader access or enable an unsupported protocol.
Troubleshooting
| Symptom | What to check |
|---|---|
| Controls are disabled | The operation’s role requirement, camera grant, capability evidence and current connection state. |
| PTZ works in the vendor app only | The configured HTTP port, device credentials, selected channel and supported KeepPeek adapter. RTSP playback alone does not validate these. |
| Motion is enabled but no events appear | Camera schedule, regions, notification linkage and generic-motion retention; then inspect native event status. |
| A capability appears but its command fails | Reachability and the command’s error. Cached discovery evidence is not a current successful command. |
| Zoom changes only the displayed crop | You are using Digital zoom. Optical zoom is a separate PTZ operation. |
The camera control reference details vendor differences and synthetic test coverage. Physical support still depends on the camera model, firmware, account permissions and channel mapping.
Demo videos
These audio-paced H.264/AAC recordings exercise KeepPeek’s production controls and are regenerated from the canonical Paper scenarios. Azure OpenAI narration controls each visual phase, and captions are available from every player.
These are recorded demonstrations, not a live preview of the current release. Use the source commit and generation timestamp in the manifest below to identify the version shown. Labels, layout, and available controls may differ from the current application; the feature guide and workflow chapters describe current behavior.
Add nine cameras in Settings
Begin with an empty KeepPeek server, enter nine real-time RTSP camera configurations through Settings, then inspect the completed live wall and one feed’s WebRTC evidence.
The virtual-camera source is Big Buck Bunny, © 2008 Blender Foundation, licensed under CC BY 3.0.
Review what just happened
Focus a live camera, then use History to open its timeline in Keep.
Delete and re-add a camera
Remove and restore the deterministic H.264 camera through the real KeepPeek server and production Settings UI.
Set up a camera with catalog context
Discover a model, review the embedded catalog facts, open CCTV Database for deeper manual research, then apply only the stream declarations you choose before the first configuration write.
The published manifest contains the source commit, generated timestamp, content hashes, and metadata links for every recording.
Live wall and kiosk display
The Dashboard is KeepPeek’s multi-camera live wall. Open Wall display settings with the sliders icon in the upper-right corner to change its presentation and resource policy. These choices belong to the selected dashboard on the server, not to camera recording or detection.
When no cameras are configured, Administrators see No cameras yet and an Add camera action that opens the camera wizard. Users with no available cameras see a message to ask their administrator for access.
Choose a dashboard or focus a camera
Use the dashboard-name selector to choose a saved layout. All cameras is the automatic fleet view; changing camera configuration updates its generated grid. A saved dashboard can arrange a subset of cameras for a room, entrance or display.
Select a camera tile to open the focused Viewer. Use its camera selector or filmstrip to change the focused camera. Live and History are viewing modes; pausing or leaving a browser view does not stop server recording. Open Keep for a recorded-day investigation, event context or evidence export. See Recording and evidence.
The information control on a tile opens camera evidence without making a paused browser preview look like a server outage. For detailed device settings and supported PTZ or detector operations, open the camera from Cameras. See Camera controls.
Arrange and share a dashboard
An Administrator can create, duplicate and edit dashboards. In the layout editor, add available cameras, select a tile, and move or resize it in the grid. Keyboard controls support moving the selected tile; the editor’s hints identify the available actions. Save the draft to publish the layout. Cancel or discard to leave the saved layout intact. The automatic All cameras grid cannot be rearranged like a custom dashboard; create a custom layout for that purpose.
The dashboard audience determines who can see a saved layout. It does not grant access to the cameras in it. Camera groups are labels used for organization and access grants; the server filters each viewer’s layout to the cameras that identity is allowed to see. A shared layout can therefore be empty for one User while containing cameras for another. Do not broaden camera permissions merely to make a layout match another person’s screen.
Layout export/import carries dashboard definitions, not camera credentials or media. Review the imported camera references and audience before use on another recorder. A stale save is rejected instead of overwriting a newer revision; reload the current saved layout before trying again. See Authentication and access and Configuration export and import.
Defaults and ownership
| Setting | Default | Supported choices |
|---|---|---|
| Tile shape | 16:9 | 16:9, 4:3, Native |
| Media fit | Contain | Contain, Cover (cropped) |
| Gap | 10 px | Whole pixels from 0 through 24 |
| Corner radius | 10 px | Whole pixels from 0 through 24 |
| Streaming mode | Smart | Smart, Continuous |
| Stream limit | 12, further limited by the browser estimate | Whole numbers from 1 through 12 |
| Keep display awake | Off | Opt-in Screen Wake Lock |
Administrators can preview changes in the current wall and select Save to store them in the selected dashboard. Discard changes restores the saved values. A rejected or conflicting save leaves the preview intact; discarding after an error reloads the server’s latest values. Users without dashboard-edit permission see read-only controls.
Reset wall settings previews the defaults and releases any requested wake lock. Select Save to make the reset permanent. Shape, fit, and radius changes do not recreate the WebRTC session, restart recording, or alter exported media. Gap changes can naturally change which tiles are visible.
The server stores each dashboard’s versioned display object under [peek_layouts] in the existing
config.toml. Settings follow the dashboard through duplication, layout export/import, configuration
export/import, and restart. A kiosk and a phone can use different dashboards with different settings;
loading the same dashboard uses the same saved choices, subject to each browser’s runtime limits.
Older dashboards without a display object use defaults. Writes from older clients that omit the object preserve the stored settings. Invalid versions, fields, types, and values are rejected before the configuration changes. Browser-local wall preferences are no longer read or written and are not automatically uploaded. See the layout registry reference.
Gap, corners, and presets
The appearance controls combine live sliders with exact pixel inputs:
| Preset | Gap | Corner radius |
|---|---|---|
| Current | 10 px | 10 px |
| Hairline | 2 px | 0 px |
| Flush | 0 px | 0 px |
Adjust either value to create a custom appearance. The preset indicator follows the actual values; presets change only gap and radius, leaving shape, fit, streaming, and wake-lock settings intact. Zero radius gives square corners and removes the normal video-tile border. Health warnings retain an inset outline. Zero gap joins neighboring tile slots; Contain can still add letterboxing when source and tile ratios differ. No appearance setting stretches pixels or forces Cover fit.
The automatic All cameras dashboard retains its generated camera grid, but its display settings are editable. Other dashboards retain their own independent display settings.
Tile shape and media fit
16:9 and 4:3 select the wall’s presentation ratio. Native uses the first valid decoded dimensions for each camera, with 16:9 reserved until dimensions arrive. Native ratios from 1:16 through 16:1 are accepted. Invalid or missing dimensions retain the reserved frame.
Media frames fit inside stable grid slots. Native metadata can change the frame inside its slot, but it does not repack neighboring tiles. A later main/sub quality switch retains the camera’s latched presentation ratio for that mounted wall. Labels, health evidence, and diagnostics remain outside the resized media frame, including for portrait and panoramic cameras.
The wall reserves a compact toolbar strip above the camera grid. On narrow screens, tile slots retain at least 160 pixels of height for labels and status, while media keeps the selected ratio. The wall scrolls inside its viewport without extending under the surrounding navigation.
- Contain preserves the complete source image. Black bars are expected when the source and presentation ratios differ.
- Cover (cropped) fills the presentation frame without stretching. A Cropped label identifies the omitted edges. Select Contain to reverse the crop.
- Selecting a tile opens the focused Viewer, which always offers uncropped video independently of the wall’s fit choice.
These settings do not add per-camera crop positions or alter the saved grid’s tile coordinates.
Smart and Continuous streaming
Smart prioritizes the focused camera, visible tiles, nearby offscreen tiles, and the existing audio/visibility signals. It permits bounded prefetch and a one-second release grace period for a tile that leaves the viewport.
Continuous attempts to keep every visible tile subscribed, independently of motion and events. It does not prefetch offscreen tiles. Both modes prefer the lowest browser-compatible quality rank for wall previews and preserve the focused Viewer’s separate quality selection. When ranks are unavailable, the existing resolution, bitrate, frame-rate, and stream fallback ordering applies. A codec rejected by the browser is not requested.
The settings panel shows requested streams and decoders, the effective budget, the active count, and any excess demand before Continuous is selected. The initial browser estimate is half of reported hardware concurrency, rounded down and bounded to 4-12 decoders. Missing hardware information uses four. The effective ceiling is the smaller of that estimate and Stream limit. The estimate is not a guarantee that every codec or camera will decode smoothly; lower the limit when this device cannot sustain the workload.
Admission uses batches of up to three cameras, with a 40 ms delay while more admissions are queued. Focused and warm background streams use the same ceiling. Existing subscriptions are released before their replacements are requested. The server still authorizes every subscription and enforces its negotiated transport and resource limits; the current capability snapshot does not advertise a numeric server quota.
A server-refused stream shows Server did not admit this stream, while other admitted streams continue. A failed eviction stops the local live session and reports an unavailable state instead of retaining unconfirmed media work. Lower the stream limit, inspect camera diagnostics, and retry the live view when the server or network is available.
Both modes suspend media subscriptions when the document becomes hidden. Wake lock does not override suspension. Returning to the visible wall resumes admission within the same budget. None of these actions stops camera ingest, recording, detection, or retention on the server.
Live video quality
The focused Viewer’s Auto quality uses native RTP bandwidth feedback to choose between compatible Main and Sub streams. It begins with Sub when available. A shared WebRTC connection has one network estimate for all of its tracks: KeepPeek reserves audio and explicitly selected video demand, then divides the remaining estimate among Auto video subscriptions. Adding another camera can therefore reduce the budget available to each Auto camera.
Auto waits for sustained headroom before upgrading and sustained pressure before downgrading. The existing picture remains active until the replacement stream supplies a compatible keyframe. Changing quality does not start a second peer connection or change camera recording settings. High and Low keep the requested quality when it is compatible and available; Auto does not override a manual choice. The panel reports an unavailable alternative rather than treating it as a completed handover. A manual choice can still exceed the available network capacity.
The bandwidth estimate describes the shared connection, not a camera’s received bitrate or capture-to-display latency. An unavailable estimate is unknown. Reliable data delivery uses a fixed variant and does not use native RTP Auto adaptation. Browser decoder limits and the wall’s stream admission budget remain separate from network quality selection.
Open the live video’s information panel to compare Session network estimate, Camera budget and Receive bitrate. The first describes the connection, the second describes an adaptive Auto camera’s allocation, and the third describes media arriving in this browser. Manual and fixed selections do not receive an adaptive budget. A zero allocation is different from an unknown estimate or unavailable diagnostics on an older server.
Requested quality, Active stream, Pending stream and Auto status distinguish the user’s choice from the server’s runtime selection. A pending change waits for a keyframe; the active source remains unchanged until that gate admits the replacement. An active source does not prove that this browser has decoded or displayed a new frame. The panel also identifies holds, missing network evidence, a single variant and unavailable alternatives. Reconnecting or removing a subscription clears its previous diagnostic values.
Auto keeps the current picture if an alternative lacks the codec configuration needed to verify compatibility. That alternative is reported as unavailable. It does not replace a working stream with an unverified H.264 profile.
Recorded playback does not use this live controller. A recording with a matching time range is not necessarily an aligned alternative view of the same captured frames.
Frame freshness and health
A camera’s server-reported health and a browser tile’s live admission are separate facts. A healthy camera can have a paused tile when the browser has exhausted its budget.
Paused tiles identify whether they are waiting for admission, over the device budget, offscreen, hidden, unsupported by the browser, or unavailable. A retained frame shows its age when the current viewer observed it. A cached frame without a capture timestamp says Frame time unknown. No frame received means there is no observed frame to present. A paused frame is not evidence that the camera itself stopped recording.
Use the camera-information control for transport, decoded video, and recording evidence. See camera and stream health for the authoritative health meanings.
Keep a display awake
Enable Keep display awake after interacting with the Dashboard, then save it for that dashboard. The setting uses the browser’s Screen Wake Lock API. Support requires a secure context, normally HTTPS or localhost, and permission from the browser, embedding policy, and device.
A saved enabled preference waits for a user interaction before its first request. Only one request is outstanding at a time. The status reports waiting, requesting, active, released, unsupported, or denied. A request that receives no response within ten seconds is also reported as denied.
Only the intent is stored on the server. The active wake-lock handle, browser permission, current visibility, and measured decoder capacity are runtime facts, not persisted settings. Switching to a dashboard with wake lock disabled releases the previous dashboard’s lock.
The lock is released when the setting is turned off, the document is hidden, the user leaves the Dashboard, or the view is destroyed. A late request result is released too. After a visibility return, KeepPeek may reacquire the lock if it is still enabled and the browser permits it. Rapid toggles and visibility changes reconcile to the latest choice after release completes.
The browser or operating system can release or deny a lock, including in battery-saving mode. KeepPeek does not repeatedly prompt or immediately reacquire a system-released lock. To try again after denial, turn the setting off and on. If Release failed appears, close the Dashboard tab to end its use of the browser resource. Wake lock is not native operating-system kiosk management and does not guarantee that a device can never sleep.
Recording and evidence
KeepPeek treats a recording as evidence only when the media is finalized, indexed, and playable. A connected camera or an MP4 filename alone does not prove that the requested interval can be reviewed later. The underlying live evidence dimensions are defined in Camera and stream health.
Find and play a recording
Open Keep, choose the camera and recorded day, then select footage or an event on the timeline. The four modes serve different investigations:
| Mode | Use it for |
|---|---|
| Timeline | Follow footage and event intervals around the selected playback time. |
| Stories | Review available event stories and their associated media. |
| Swimlanes | Compare the available camera histories around a common time. |
| Export | Prepare and inspect an evidence-export job; creation requires Administrator access. |
Use play/pause, skip, the recording-position slider, volume/mute, speed and fullscreen controls below the image. Available stream quality depends on retained media and browser codec support. Changing the quality preference is separate from digital zoom. Stored playback does not provide automatic transcoding for an unsupported codec; retain a compatible H.264 stream when your viewing devices need one.
With the recorded player focused, Left and Right step frames. Space toggles playback; J, K and L
provide shuttle/pause controls. [ and ] set export boundaries. Form fields keep their normal
keyboard behavior, and arrow keys pan a focused digitally zoomed image instead of stepping frames.
Use Keep on a phone
In portrait, Keep keeps a compact camera/date row, the player and a horizontal history timeline in the main view. Tap the camera name or date to open Camera and date. Search for a camera or use previous/next camera; camera navigation wraps at the ends. The recorded-day selector and its arrows choose available days, with unavailable directions disabled.
Tap Playback options for volume, playback speed, quality, Copy link to this moment, and Refresh recordings. Changes apply immediately. Done, the close button, Escape or browser Back closes the sheet and returns focus to a visible control. Opening a sheet keeps the player open. In recording fullscreen, rate and volume remain available beside the player controls.
The compact controls do not remove Timeline, Stories, Swimlanes or Export. Rotate the device or close the sheet to return to the history view. The timeline changes recording time; the separate Digital controls above the image change only the inspection crop.
Keep the requested UTC time explicit
Recorded days and event time filters use UTC. Switching cameras preserves the selected absolute moment, including when the destination camera has no footage there. Choosing another recorded day keeps the UTC clock time on that chosen day. At midnight this means midnight of the selected day, not the following day.
A gap stays visible until you explicitly choose available footage. Use Previous recording or Next recording when offered, or select a recorded interval on the timeline. KeepPeek does not silently move an unavailable requested moment to the latest clip. Loading, no recorded days, unavailable footage and decode failure are distinct states; use the displayed action or refresh after checking the camera’s recording policy and retained coverage.
Choose what each camera records
Each camera has one recording policy:
| Policy | Recorded media |
|---|---|
off | Records neither video stream. This is intentional and is not reported as a gap. |
sub | Records the substream continuously. |
main | Records the main stream continuously. |
both | Records main and sub independently. |
event-boost | Records the substream normally, switches to main at a keyframe after an event, then returns to substream. |
event-only | Records the selected main or sub stream only for accepted events, including available pre-event history. |
event-boost is the default. It writes one logical recording rather than recording main and sub
at the same time. Transitions happen only at keyframes, so the stored file remains seekable. A new
event extends the configured main-stream interval.
Event-only and event-boost can retain up to 30 seconds of encoded pre-event media in memory. Pre-recording defaults to zero, preserving existing camera behavior. Choose the duration in camera setup or the camera editor. Event-only also lets you choose main or sub. Repeated events extend the post-event window. Event-only excludes frames at or after its deadline.
With event-boost pre-recording enabled, KeepPeek delays the continuous substream by at most the selected duration so available main-stream history can replace that interval in one recording. A crash can lose this uncommitted interval. Memory pressure releases continuous sub media and shortens optional history. Disable pre-recording to restore the existing immediate admission path.
The requested interval is a ceiling, not a guarantee. Recordings begin at a retained, decodable keyframe. Startup, missing keyframes, long GOPs, and byte limits can shorten the interval. Camera health reports the requested and available history, retained bytes, and the current reason. Pending replay means media is queued; it does not prove a successful disk or catalog commit. Use Recording integrity and playback to inspect committed evidence. Audio packets crossing the selected video boundary are omitted without re-encoding.
If a runtime profile reports a camera as Degraded because storage rejected its recording settings or could not queue the change, new recording input for that camera stops. Live viewing can continue, and other cameras still apply their profile changes. Pending means the recording writer has not yet accepted the configuration. A missing writer acknowledgement also reports Degraded and stops new recording input for the current request. Correct the reported settings or restore storage availability, then open Settings → Runtime profiles and select Reapply active policy. This retries the accepted Base or named profile without switching to another policy. Save or discard profile edits before retrying. An authorized WebRTC client can also reapply the current policy, and restarting KeepPeek restores the saved selection. Applied confirms that the writer accepted the recording configuration; use Recording integrity and playback to verify retained footage.
An active recording write failure after acceptance can also change Applied to Degraded. This status uses the camera source identity even when its recording folder has a different name. Recording can continue to attempt writes. After repairing storage, use Reapply active policy to clear the failure observation after fresh configuration acceptance. A failed retry keeps the desired selection and shows its error; reload current state before trying again if another Administrator changed it. Fresh configuration acceptance does not prove media recovery, so check Recording integrity and playback. Idle event recording and ordinary write progress do not clear the failure status automatically. Use integrity checks, playback and server logs to assess cleanup, finalization, shutdown and storage-pressure failures.
The default limits are 64 MiB per buffered stream and 256 MiB globally. Administrators can change them in advanced storage settings. History is not saved across restart. Privacy activation, reconnect, and storage pause discard stale history; recording resumes from a valid keyframe. See the configuration reference for persisted fields and limits.
Before downgrading to a version without event-only support, change affected cameras to a supported policy and set pre-recording to zero. Save a configuration backup first. Existing committed media does not need conversion or deletion, and disabling pre-recording does not remove it.
Choose a browser-compatible H.264 substream even when the main evidence stream is H.265. KeepPeek stores the camera’s encoded media without re-encoding it, so browser support still determines which recordings can play directly.
Recording admission and media retention are separate. The current camera modes and event pre-recording do not implement independent continuous, motion, alert and detection retention deadlines. Storage pressure can shorten available coverage. The recording policy audit tracks verified behavior, missing retention rules and their implementation owners under #168.
Use the local archive tier
KeepPeek separates short-lived RAM buffering, active recording files, finalized primary media,
and the fourth ARCHIVE tier. A fourth-tier archive is a verified replica. The named-volume
archive placement role still relocates primary recordings; it does not create a second replica.
As an Administrator, open Settings → Storage → Local archive. Create a private destination directory owned by the recorder account on an already mounted local disk, then edit and save its target ID, root, limits, and optional source selection. The archive root must not overlap primary or metadata roots. Save requires a configuration revision; a conflict preserves your draft. Reload archive settings explicitly discards the draft after confirmation and loads current settings. Restart KeepPeek to apply the saved configuration, then test the destination and refresh archive jobs to observe the result. A pending probe is not a successful destination test.
You can queue a manual copy of finalized recordings or completed exports from the catalog object selector. Automatic mode selects eligible sources after the configured minimum age; an empty source list selects all. Copies use bounded background work and independently verify the retained replica before reporting success. Jobs show identity, state, bytes, attempts, failures, and any restore or eviction action. Refresh before retrying an uncertain result. A lost copy reply is reconciled using the same durable job identity.
Primary eviction defaults to off. A successful copy alone does not remove the primary file. Enable automatic eviction only deliberately, or confirm Evict primary for an individual job. The server checks the verified archive, current reader leases, and retention holds before retiring the primary. Timeline and seek metadata remain indexed. Open archived recordings through the normal Keep timeline; the player displays ARCHIVE — Playing from archive. Startup and seeking may take longer. It reads the required archive ranges directly, without a full restore or an artificial delay. Existing waiting, failure, seek, and cancellation controls still apply.
Restore primary queues a verified transfer back to the original primary destination. Cancellation stops cancellable copy or restore work and cleans up partial staging. Retry / finish cleanup retains the cancelled copy’s job identity. Copying resumes only after cleanup is acknowledged and current source evidence is validated. Refresh to distinguish pending cleanup from resumed copying. Once a primary eviction intent is durable, it must finish reconciliation; cancellation is unavailable for that state. Preserve the job evidence and restore the destination before retrying recovery.
An offline archive reports unavailable health without blocking new recording ingestion. Archived media whose primary was evicted can remain temporarily inaccessible until the mounted destination returns. Do not recreate or substitute a missing mount: the catalog binds the target ID to its root identity. Retain the catalog, archive tree, and configuration together for filesystem recovery. Broader offsite providers remain planned; the supported target in this phase is the local filesystem only.
Prove recording integrity
Open Recording integrity to inspect the fleet before footage is needed. The workspace reports:
- whether recording is requested by policy;
- current writer state and the last frame, write, finalization, and catalog commit;
- oldest and newest retained media;
- effective retained duration, attributed bytes, and estimated daily growth;
- playable coverage for the selected 24-hour, 7-day, or 30-day interval;
- exact recent gaps and the evidence that explains each one.
Fleet states have narrow meanings:
| State | Meaning |
|---|---|
| Recording healthy | Every requested stream has current writer progress. |
| Recording degraded | A requested writer is stalled or failed. |
| Paused by policy | The effective policy intentionally requests no recording. |
| Not configured | No usable recording stream is configured. |
| Unknown | KeepPeek lacks enough current evidence to make a truthful determination. |
The Not configured summary appears only when its count is nonzero. Policy-disabled streams and unconfigured cameras remain distinct from unexpected recording loss.
Coverage comes from keyframe-indexed fragments, not from file presence. Current gaps remain open and never claim an end time. Causes include source silence, transport outage, stale frames, decode failure, writer failure, disk pressure, retention deletion, storage migration, catalog mismatch, and an explicit unknown state. Gap actions open nearby footage, camera health, or relevant logs.
Long histories remain bounded: fleet pages contain 25 cameras by default, recent exact detail keeps at most 256 ranges per stream, and longer periods use deterministic time buckets while retaining exact totals.
Review events consistently
Open Events to search across authorized cameras. Choose the UTC date and optional start/end times, then narrow by camera, event kind, origin, zone, confidence, image availability or text. Review and bookmark filters compose with the same search. If a filter reports unavailable support, check the server capability instead of assuming that an empty result means no events.
Select a card to inspect its canonical image, description, timestamps and revision. Use Open at this moment to investigate recorded footage or the export action to prepare a clip. The return link restores the selected event and supported filter context. For an empty result, check the effective filters and UTC date before broadening the search.
Mark events reviewed or dismissed without deleting evidence. Those flags belong to your authenticated identity, or to a persistent Local workspace in trusted-LAN use. Event updates do not reset them. Use the visible or selected count on bulk actions; undo applies only to the acknowledged targets. Review and bookmark filters use server-computed counts, not the current page length.
Reviewed and Dismissed are independent flags. Unreviewed means neither flag is set; dismissal removes an event from that review queue without deleting its attachments or recording. Mark N visible reviewed affects the rendered page, while Mark N selected reviewed sends the explicit selection, which may span pages. The selection limit is 128 events. A stale or unconfirmed write requires Reload and retry; note drafts and action intent remain available.
Named credentials retain their review identity across credential rotation. A trusted-local browser uses a persistent Local workspace identity rather than attributing work to a person. Clearing that site’s browser storage creates a new workspace; unavailable persistent storage disables its mutations instead of silently using a temporary identity.
Bookmarks are shared with authorized camera viewers. Their creator or an Administrator can edit the bounded note or remove the bookmark. Saved bookmarks retains honest metadata-only references when media, events, or sources are unavailable. A bookmark does not pin video through retention. Exports keep their source event/bookmark revision relationship without creating a retention hold.
Bookmark notes are plain text, limited to 1,024 UTF-8 bytes. The bookmark library has its own source, date and creator scope, independent of the main Events filters. Recording retention can remove footage without removing the bookmark. References for deleted events are retained for 90 days and explicitly identify missing event, source or media.
Events, Keep, notifications, MQTT, and export entry points use the same event revision and canonical preview. An authorized producer may name the canonical attachment. Otherwise KeepPeek chooses a supported snapshot, then a story frame, then a retained thumbnail using stable ordinal, capture time, and attachment ID ordering.
If canonical image bytes are unavailable, KeepPeek shows that state instead of silently choosing a different image. Bounding boxes appear only when their coordinate-space attachment matches the canonical image. Event type remains authoritative for filtering and automation; semantic icons are presentation-only and come from a fixed allowlist.
Notifications and integrations explains how those same event and operational identities remain stable across delivery, retry, and broker recovery.
Copy a recording moment
Use digital zoom and pan to inspect recorded frames and canonical event images without changing their source resolution or the evidence. Inspection crops are not included in copied moment links or exports.
Use the link icon in Keep’s command bar to copy the current recording moment. The command reads the playback clock when invoked; it does not pause, seek, reload the video, or change browser history. Event detail has the same command beside Open at this moment, using the event’s start time. Both event actions retain the selected event and active filters. Back to event returns to that context from any Keep mode.
These are authenticated navigation links, not public shares or exports. Recipients need access to the same KeepPeek server and camera, and remote recipients must sign in. A link does not contain an access key, session ID, temporary media URL, recording filename, or filesystem path. It does include the server-provided camera identifier and, for event links, event and filter context. Treat those details as private operational information. Changing camera identity or deleting footage can make an old link unavailable.
Links use the existing /keep route, including any configured application base path:
| Parameter | Meaning |
|---|---|
camera | Server-provided source identifier, encoded without changing its value. |
at | Absolute Unix timestamp in integer milliseconds. |
date | UTC date derived from at; the timestamp wins if an incoming date conflicts. |
stream | Requested auto, high, low, main, or sub preference. |
mode | Keep view; omitted for Timeline, otherwise stories, swimlanes, or export. |
event | Optional event identifier. |
returnTo | Optional local Events route with supported filters and selected event identity. |
The timestamp remains the same across browser timezones. For retained, supported media, playback restoration is verified within one second or one source-frame duration, whichever is larger. Browser codec support still applies. KeepPeek explains a compatible stream fallback and preserves the requested preference in copied links.
An exact link into a gap does not open nearby footage automatically. It keeps the requested UTC time visible and offers previous or next recordings found in the bounded five-minute window on each side, within that UTC day. Selecting one explicitly moves the playhead. If no retained footage is found, the view explains that it may have expired or never been recorded. Missing and inaccessible cameras share an unavailable-or-not-authorized message to avoid disclosing hidden sources. Malformed links fail visibly instead of selecting another camera.
Successful copying shows a checkmark and announces confirmation. If browser clipboard access is denied, unsupported, or takes more than 2.5 seconds, a dialog presents the same link in a selected, read-only field for manual copying. Escape closes that dialog and returns focus to the command.
Export evidence
An Administrator can select up to two minutes in Keep and create a standalone MP4. Event export opens the same editor with 15 seconds of context before and after the event while preserving the camera, stream, timestamp, event revision, filters, and return route.
Export jobs can be running, ready, partial, failed, cancelled, or expired. KeepPeek:
- returns an identical running job instead of starting hidden duplicate work;
- offers an existing ready artifact before creating a fresh one;
- names every missing interval before a partial export proceeds;
- stops jobs after 30 seconds without progress or five minutes total runtime;
- removes partial output after failure or cancellation;
- marks interrupted jobs failed and retryable after restart;
- verifies SHA-256 again immediately before download.
Ready files remain available for 24 hours. Bounded job history remains for 30 days, up to 500 jobs, so an expired or missing artifact can still explain what happened and support an explicit retry. History and files remain scoped to the Administrator identity that created them.
Check the completed job’s interval, stream and missing-range explanation before downloading. A partial artifact contains only its available intervals and must not be described as continuous coverage of the whole selection. Cancellation and retry are explicit actions; opening another page does not turn a running job into a completed download. Download integrity failure or an expired artifact requires a fresh job if the original recordings are still available.
Timestamp burn-in requires a configured re-encoding worker and otherwise fails explicitly. The normal export path preserves source frames and timestamps without re-encoding.
For the availability and safety limits of manual deletion and catalog drift checks, see Recording maintenance. The Administrator workflow is under qualification; confirmed jobs can remove selected recordings permanently.
For the detailed coverage model and export lifecycle, see the recording integrity, event presentation, and evidence export references.
Digital zoom and pan
Digital zoom enlarges the media already decoded by your browser. It is available in the focused Peek Viewer, Keep recorded playback, and event detail images. It does not move the camera, request a different stream, or increase the source resolution.
Compact Dashboard tiles and the camera filmstrip do not capture inspection gestures. Open a camera in Viewer before using digital zoom.
Inspect a frame
The Digital controls sit at the top-left above the image without covering the picture. In Peek, they share one row and height with Live/History, quality, and camera information. The row scrolls horizontally when needed rather than wrapping. They show the current magnification. The minimum is 1.0x, which fits the complete image, and the maximum is 8.0x. The image keeps its original aspect ratio. Panning is bounded to the fitted image area, so a drag cannot move the image out of view.
| Input | Result |
|---|---|
| Zoom-in icon | Double the magnification, up to 8.0x. |
| Zoom-out icon | Halve the magnification, down to 1.0x. |
| Reset icon | Return to the complete, centered frame. |
| Two-finger pinch inside the image | Zoom around the midpoint between the fingers; moving that midpoint also pans. |
| Mouse, pen, or one-finger drag | Pan while magnification is above 1.0x. |
| Alt + wheel or trackpad scroll | Zoom around the pointer. Use Option on macOS keyboards. |
| Double-click or double-tap | Switch between 1.0x and 2.0x around the pointer or tap. |
+ or = | Zoom in when the inspection surface has focus. |
- | Zoom out when the inspection surface has focus. |
0 | Reset when the inspection surface has focus. |
| Arrow keys | Pan while zoomed and focused. |
| Escape | Reset digital zoom before the normal Viewer or event-detail exit action. |
Tab reaches the named zoom buttons. Activating a zoom button focuses the inspection surface, where the keyboard commands work. Peek’s desktop control groups are 32 pixels high. Touch input uses at least 44-pixel button targets. All buttons have visible focus indicators and disabled states at the limits. The zoom value is also available to assistive technology.
Ordinary wheel scrolling does not change magnification. Control/Command zoom shortcuts still belong to the browser. Touch gestures are captured only inside the focused image; scrolling and browser gestures remain available elsewhere on the page. Zoom and reset do not require animated transitions.
Keep playback controls
Keep’s play/pause, skip, position, volume, mute, speed, and fullscreen controls remain outside the transformed video. Timeline scrubbing still selects recording time, not an image crop. Arrow keys pan when the zoomed image has focus; Keep’s normal frame-stepping shortcuts remain available at full-frame view or outside the inspection surface.
On a portrait phone, the Digital strip remains above the image while Playback options opens volume, speed and quality in a separate sheet. Opening that sheet does not replace the recording or move its playhead. Its quality choice can request different available media; the Digital strip only enlarges pixels already decoded. Close the sheet to inspect the horizontal recording history.
The fullscreen button expands the whole recorded player, including its controls. Zoom bounds update when entering or leaving fullscreen. Browser-native Escape handling can leave fullscreen before the page receives the key. A disabled fullscreen button indicates that the browser does not offer the Fullscreen API in that context; digital zoom remains available without fullscreen.
Reset and evidence
Changing the camera, recording, or canonical event image resets digital zoom. Play/pause and ordinary progress within the same selected media preserve the inspection position. Resizing the window, rotating the device, or changing decoded dimensions recomputes the bounds. Reset always returns to the complete frame at the current size.
Event bounding boxes use the canonical image’s coordinate space and move with that image. Player controls, camera information, and diagnostics do not scale. Live camera information reports digital magnification separately from decoded resolution.
The inspection crop exists only in the current browser view. It is not saved into configuration, recording metadata, bookmarks, copied moment links, or exported evidence. Exports still contain the original recorded frame. See Recording and evidence.
Digital zoom is not PTZ
Digital zoom is labelled Digital and works for any media you can already view, including recordings and offline-camera event images. It does not require PTZ permission or a PTZ-capable camera. Physical pan, tilt, and optical zoom remain separate camera commands with their existing capability and permission checks.
See Camera controls for physical PTZ, detector configuration and the current limits of browser Talk.
Magnifying a low-resolution stream does not reveal detail that the camera did not send. Choose a higher available stream quality separately when you need more decoded pixels.
Camera and stream health
KeepPeek computes camera and stream health on the server. Peek, Cameras, Health, camera diagnosis, fleet counts, the control API, and Prometheus metrics consume that same projection. Clients do not infer health from display text, local video playback, or issue severity.
Investigate an unhealthy camera
- Open Cameras and use Not healthy to narrow the fleet. Select the camera for its current state, primary reason, stream profiles and recording evidence.
- Open Health for fleet findings. Refresh to request a current snapshot, then follow a finding’s camera or timeline link to the affected source and time.
- Compare transport, advancing frames, keyframes and recording progress. A connected socket can still deliver stale or undecodable video; a decoded live picture does not prove recording.
- Use Recording integrity when the question is whether footage was retained. Its historical coverage and gaps answer a different question from current-process health counters.
- Check native event and external service status separately when only detections or alerts are missing. Their failures do not make otherwise healthy camera media offline.
An active health finding clears when new evidence shows recovery. It has no dismiss action that changes the underlying state. Acknowledging an alert or marking an event reviewed is a separate workflow. See Notifications and integrations and Recording and evidence.
Health and administrative diagnostics depend on your role. If a User can view video but cannot load fleet health, check access permissions before treating the unavailable diagnostic panel as a camera failure.
The current health contract version is 1. A client that cannot understand a state or reason must
treat it as unknown, never as healthy.
Evidence dimensions
Health keeps these dimensions separate:
| Dimension | Evidence |
|---|---|
| Configured and expected | Camera configuration, recording policy, and expected stream IDs |
| Transport | Router lifecycle plus the expected and connected stream IDs |
| Report freshness | Monotonic age of the latest periodic ingress report |
| Frame freshness | Monotonic age of the latest advancing frame counter |
| Decodability | Monotonic age of the latest advancing video keyframe counter |
| Ingress quality | Measured frame rate and recent reconnect, drop, and error deltas |
| Recording | Requested stream IDs, writer attempts, successful MP4 appends, and bounded writer errors |
| Battery sleep | Fresh registration heartbeat plus no pending wake request from the optional battery wake service |
Native events, external detectors, and other optional services have their own health. Their failure does not make otherwise healthy media or recording appear offline.
Canonical states
| State | Meaning |
|---|---|
starting | KeepPeek is inside the initial evidence grace period. |
healthy | Required transport, frame, keyframe, frame-rate, and recording evidence is current. |
degraded | Media is current, but one or more quality, decodability, transport, ingress, or recording dimensions failed. |
stale | A report is missing or old, or frame counters stopped advancing. |
reconnecting | Transport is reconnecting and recent media evidence still exists. |
offline | Transport is disconnected without recent evidence. |
stopped | Media is not expected, the camera is shutting down, or a registered battery camera is sleeping. |
unknown | The server or client lacks enough authoritative evidence. |
healthy is the only healthy state. starting, unknown, and stopped are not included in healthy
counts. A health request failure makes the current view unavailable; it does not rewrite the last
server camera state.
Precedence
The server applies this order. The first matching rule supplies the canonical state and primary reason code.
- Not expected, shutting down, or stopped:
stopped. - Fresh battery registration without a connected transport or pending wake request:
stoppedwithbattery_sleeping. An accepted wake remains pending until connected media clears it; failures therefore becomereconnectingoroffline, not sleeping. - During initial grace, incomplete or transiently unhealthy evidence remains
starting; a complete healthy snapshot may promote immediately. - Reconnecting transport:
reconnecting, orofflineafter recent evidence expires. - No report, an old report, or frames that stopped advancing:
stale. - Missing keyframes, stalled requested recording, low frame rate, partial transport, recent errors,
drops, or reconnects:
degraded. The primary degraded reason is ordered as partial transport, missing keyframes, recording failure, low frame rate, errors, drops, then reconnects; all matching codes remain inreason_codes. - Required evidence unavailable:
unknown. - All required evidence current:
healthy.
A connected transport cannot override stale frames, missing keyframes, or failed recording. A decoded browser frame cannot promote server state. A fresh capability snapshot is not media evidence.
Thresholds
Ingress publishes every 10 seconds.
- A stream report is stale after 30 seconds.
- Reconnecting becomes offline when the latest stream evidence is older than 90 seconds.
- Frame freshness is $\max(20\text{ s}, 3 / \text{expected fps})$, capped at 120 seconds, and also requires a fresh stream report.
- Keyframe freshness is three configured GOP intervals, or three observed keyframe intervals when configuration is unavailable. It is clamped to 30 through 120 seconds.
- Recording progress is fresh for the configured short-term buffer plus flush interval plus 30 seconds.
- Initial startup grace is two ingress report intervals, currently 20 seconds.
Frame-arrival jitter measures the absolute difference between each observed frame gap and the configured frame interval. Its P99 is computed over the current 10-second ingress report window; it is not WebRTC network jitter. KeepPeek displays it as diagnostic evidence but does not create a finding from jitter alone. Stale frames, low sustained frame rate, decode failure, ingress drops/errors, recording failure, and a maximum observed frame gap above 2 seconds remain actionable health signals.
The first ingress report establishes the baseline for cumulative reconnect, drop, and error
counters. An initial connection is therefore not reported as a recent reconnect. During startup
grace, partial-window frame rates and other transient evidence remain starting; if they have not
recovered when grace expires, the canonical degraded or offline reason becomes visible.
These thresholds are the bounded debounce window. One missed 10-second ingress report does not
change a camera to stale; report evidence must age beyond 30 seconds. Recovery is intentionally
asymmetric: a new advancing frame or keyframe counter can recover the state immediately. The API
still returns the actual evidence ages, and clients do not add another timer or rewrite the state.
These ages use monotonic clocks inside the server. Wall-clock timestamps are included for display, but wall-clock changes do not make stale evidence fresh.
Health findings are live snapshot evidence rather than acknowledgements or retained alerts. They disappear automatically after a later ingress report no longer meets the condition. The refresh button requests a current snapshot immediately, but an active finding cannot be dismissed from the Health page. Acknowledging a delivered alert belongs to the notification inbox rather than changing server health evidence.
Operational intervals
KeepPeek projects four durable event kinds from the independent health dimensions:
| Event kind | Scope | Evidence |
|---|---|---|
camera_offline | Camera | No expected transport is connected, or aggregate transport is disconnected |
stream_stale | Camera stream | The stream report is missing or stale, or frames stopped advancing |
decode_unavailable | Camera stream | No recent decodable keyframe exists |
recording_interrupted | Camera stream | Recording was requested but writer progress stopped |
These event kinds remain independent. One stream can be stale and undecodable while another stays healthy, and recording failure does not rewrite transport evidence. Native event and external analysis failures are not camera, stream, or recording outages.
A candidate interval starts as soon as evidence fails. Monotonic elapsed time controls warning, critical, and recovery thresholds; server wall time supplies the displayed start and end timestamps. The default policy is:
[operational_events]
warning_hold_down_secs = 15
outage_hold_down_secs = 60
recovery_debounce_secs = 10
record_short_flaps = false
[operational_events.cameras.front-door]
warning_hold_down_secs = 30
Camera overrides may use the stable camera ID or IP address and inherit omitted global values. A zero duration requests an immediate transition. Durations cannot exceed 24 hours, and the warning hold-down cannot exceed the outage hold-down.
Recovery before the warning hold-down removes the candidate unless record_short_flaps is true.
Once visible, an interval keeps one event ID. Cause or severity changes increment its revision;
stable recovery increments the same event again, records the recovery timestamp, and reports total
duration from the original evidence-loss instant. Open intervals are restored from the recording
catalog after restart. Stable revision replay makes notification processing at least once while
the notification store collapses duplicate revisions.
The stored timeline renders open and recovered intervals beside recording availability. Event payloads include severity, bounded cause and explanation, affected streams, recording consequence, evidence source, duration, and recovery state. Health findings link to the corresponding camera and timeline instant.
Session durations
Camera health reports current-process session durations separately from historical archive coverage:
session_duration_msis the longest observed ingress-worker duration for the camera’s expected video streams;recorded_main_duration_msandrecorded_sub_duration_msaccumulate video sample durations only after those samples are successfully appended to MP4 during the current process run;recorded_total_duration_msis the sum of all requested stream durations for that camera session.
The total can exceed camera session elapsed time when Main and Sub record concurrently. These counters reset when KeepPeek restarts or the corresponding recording-health registry is replaced; they do not claim lifetime or retained archive coverage. A recording indicator uses writer progress state, not a comparison between these counters and wall time.
Reasons and detail
Every camera and stream includes:
state: the canonical state;reason: the primary stable reason code;reason_codes: all applicable ordered reason codes;detail: bounded human-readable text;dimensions: the independent raw evidence and counts.
Stable reasons include transport_disconnected, transport_reconnecting,
transport_partially_connected, no_stream_report, stream_report_stale,
frames_not_arriving, frames_below_expected, keyframes_missing, ingress_reconnects,
ingress_drops, ingress_errors, and recording_not_progressing.
Use reason codes for automation. Display text may change.
Fleet counts
Fleet summaries name the dimension they count:
- configured cameras and expected video streams;
- cameras and streams with connected transports;
- cameras and streams with fresh frames;
- cameras and streams with recent keyframes;
- cameras and streams requested for recording;
- cameras and streams with current writer progress;
- cameras whose evidence is unknown.
Fleet aggregates count expected video streams. Audio freshness remains available on each stream snapshot and is not folded into the video decodability or recording totals.
Unknown evidence is not counted as connected, fresh, decodable, recording, or healthy. The Cameras
Not healthy filter counts every state except healthy.
API and metrics
HealthCommand returns the versioned protobuf health snapshot. Version 1 includes the canonical
state, stable reasons, camera and stream dimensions, media progress timestamps, and explicit fleet
counts. Unknown future state or reason strings are presented as unknown.
GET /metrics exposes the same projection. Important gauges include:
keeppeek_cameras_connected,keeppeek_cameras_fresh,keeppeek_cameras_decodable, andkeeppeek_cameras_recording;- corresponding
keeppeek_video_streams_*gauges; keeppeek_camera_infowith the canonical state label;keeppeek_camera_health_dimensionandkeeppeek_camera_health_dimension_known;- corresponding per-stream dimension and known gauges;
keeppeek_operational_event_active, labeled by camera, stream, event kind, and severity.
There is no separate online or degraded gauge. Consumers use the canonical state label and
explicit dimensions.
Recovery
Recovery requires new positive evidence:
- a newly advancing frame counter refreshes frame age;
- a newly advancing keyframe counter refreshes decodability;
- a successful writer append clears the previous bounded recording error;
- drop, error, and reconnect reasons use per-report deltas, so old cumulative counters do not keep a recovered stream degraded;
- a fresh report can recover stale state, while cached metadata cannot.
Operational interval recovery uses this canonical projection. Delivery failure cannot change or close an interval because catalog persistence happens before notification publication and provider work runs independently from camera media.
Use Recording and evidence to inspect retained coverage and explain gaps. Use Notifications and integrations to deliver these operational transitions without coupling providers or brokers to camera ingest.
Visual configuration management
Use Cameras to inspect and change shared camera defaults, effective inherited values, versioned
templates, and explicit camera sets. These operations require Administrator access and the server’s
keeppeek.configuration.v1 capability. If it is unavailable, the fleet remains readable and the
unsupported actions stay disabled.
Choose the right editor
| Task | Location |
|---|---|
| Change one camera’s connection, streams, or recording policy | Open its Camera page and edit its configuration. |
| Change inherited policy, manage templates, or preview a fleet change | Open the Cameras fleet configuration controls. |
| Change storage, retention, or listener settings | Settings. Use the storage editor’s move/restart review when changing paths. |
| Change a dashboard or its display and audience | The live-wall layout controls. |
| Change User camera grants or credentials | Settings > Access & roles. |
| Change notification rules or MQTT | The notification and integration editors. |
| Transfer the complete settings and file-backed secrets | Configuration ZIP export and apply. |
| Configure file-only native event policy | Stop the server and edit the supported camera event table. |
The browser has no general secret editor or raw TOML editor. Use the configuration reference for supported file fields. Preserve unrelated sections and existing secret references when editing a stopped installation.
Inspect inheritance before changing it
Every inheritable policy shows its configured default, camera override, final effective value, source, and current runtime state. Use inherited value removes an override so future default changes continue to flow to that camera.
A built-in value is the fallback shipped by KeepPeek. A shared default changes the fallback for cameras without that override. An explicit camera value takes precedence. Use built-in value removes a shared default. Removing an override does not freeze the current value into the camera.
For example, changing a shared recording mode affects cameras that inherit it. A camera with an
explicit main override keeps that policy. Check both the effective value and runtime status after
applying a change, especially if a camera is disconnected.
Preview and apply a fleet change
- Open the shared-default or bulk-change view and choose the fields to change.
- For bulk changes, select explicit camera IDs, the complete filtered result, one configured group, or all configured cameras. The all-cameras scope requires explicit confirmation.
- Request a preview. Confirm the server’s authoritative camera count, old and new values, skipped targets, validation issues, and reconnect or restart consequences.
- Resolve issues before applying. A missing target makes the plan invalid; an oversized preview requires a smaller selection or fewer changed fields.
- Apply the current preview, then check the per-camera activation result and camera health.
The filtered scope means the complete filter result, including rows outside the visible viewport. One plan includes at most 64 cameras and expires after ten minutes. The server retains at most 128 plans and bounds control responses to 64 KiB. A preview that cannot fit fails explicitly.
Template and bulk operations always produce a server-owned preview before apply. The preview lists the exact authoritative cameras, semantic old and new values, skipped targets, validation issues, and reconnect or restart consequences. Applying a template creates explicit overrides; later template edits or deletion do not silently change cameras.
Configuration writes use the edit-start revision, validate the complete candidate, preserve unrelated fields and secret references, and replace the configuration atomically. A conflict reloads current evidence without discarding the local draft. If a committed change cannot activate on one camera worker, KeepPeek reports that camera and the required restart recovery action.
Create and reuse a camera template
In the templates view, create a named template with supported connection or recording-policy fields you want to reuse. Templates can be edited, duplicated, deleted, imported, and exported. They do not include notification rules; manage those in their own editor.
Credential fields accept complete references such as {secret:CAMERA_PASSWORD}. Create the
corresponding secret in the existing companion secrets.toml through your protected local workflow.
A template does not carry the secret value to another recorder.
To use a template, choose it for a bulk target set, review the server preview, then apply. Applying creates explicit camera overrides. Later template edits or deletion do not change cameras that previously used it. To return a camera to shared defaults, remove the relevant overrides with Use inherited value.
Template exchange uses versioned JSON, not a configuration ZIP or raw TOML. Export the template document, choose it for import on the target, review its validated contents, and apply the preview. Unsupported versions, unknown fields, duplicate IDs or names, inline credentials, invalid values, and oversized documents are rejected before mutation. Documents are limited to 16 KiB and contain at most 64 templates. Template names are unique ignoring case.
Resolve conflicts and activation failures
| Result | What to do |
|---|---|
| The configuration changed while you were editing | Read the refreshed evidence, compare it with the retained draft, and request a new preview. There is no blind overwrite. |
| A preview expired | Request and review a new preview. A ten-minute-old target set may no longer be valid. |
| A target is missing or the preview is too large | Correct the target selection or split it into smaller explicit batches. |
| A value fails validation | Correct the indicated field. Check the reference for units, bounds, inheritance, and supported secret syntax. |
| Saved values differ from runtime | Follow the reported per-camera reconnect or restart action, then verify health and effective values again. |
| The capability is unavailable | Check the connected server version. The visible fleet does not imply configuration writes are supported. |
Conflicts retain the complete local draft while reloading current evidence. Single-camera edits use the configuration revision; template transactions also account for the template document. A template-only edit therefore does not create a false conflict for an unrelated camera draft.
The detailed contract, limits, and ownership boundaries are in configuration engineering guide. Use configuration exchange for full-recorder settings recovery.
Configuration reference
KeepPeek stores settings in one application configuration file, named config.toml by default.
The existing companion secrets.toml holds reusable private strings. Layouts, notification rules,
credential metadata, and camera templates belong in the application configuration, not in separate
settings files. Media files and recording catalogs are data, not additional settings stores.
Live-wall display settings belong to each saved dashboard in [peek_layouts].
Shape, fit, gap, corner radius, streaming mode, stream ceiling, and wake-lock intent are stored
on the server and included in configuration and layout exchange. Browser permission and active
media/wake-lock handles remain runtime state.
This chapter describes the supported on-disk sections and their serialized Rust types. It is not a list of every API response or runtime struct. Use visual configuration for normal administration and configuration exchange for validated export and import.
Location and editing
| Environment | Default application configuration |
|---|---|
| macOS | ~/Library/Application Support/keeppeek/config.toml |
| Linux | ${XDG_CONFIG_HOME:-$HOME/.config}/keeppeek/config.toml |
| Windows | %APPDATA%\keeppeek\config.toml |
| Docker | /config/keeppeek/config.toml |
--config <path>, --config=<path>, or -c <path> selects another file. Its companion secrets
file is always named secrets.toml in the same directory. These flags do not create a second
settings store.
Default paths belong to the account running the process. A Windows service can resolve a different
%APPDATA% from an interactive launch; the macOS package runs a per-user launch agent. Confirm the
service identity and effective paths before transferring an installation. See
Upgrades and migrations.
Use the UI and typed configuration operations while KeepPeek runs. Stop the server before manual edits so a concurrent settings update cannot overwrite them. Server writers validate candidates, preserve unrelated sections and secret references, and replace the file atomically under the shared configuration-update lock. Some changes apply live; listener or storage changes can require the restart or migration reported by the configuration plan.
On Unix, KeepPeek creates its default private directory with mode 0700 and private files with
mode 0600. Restrict the directory to the service account on every platform. Secret values are
plaintext at rest, not encrypted by TOML.
In the tables below, optional means omit the field when unset: TOML has no null. Integer types
refer to the Rust model; values must also fit TOML’s integer representation. Vec<T> is an array,
and a map is a table keyed by the indicated identity. Byte limits and character limits are different.
Root fields must appear before a table header or they belong to that table.
Section index
| TOML section | Owning type | Ownership |
|---|---|---|
| Root fields | Config | Operator settings |
[access] | AccessConfig | Administrator security policy |
[direct_card] | DirectCardConfig | Exact browser-origin allowlist |
[storage] | StorageToml | Recording paths, retention, and safety |
[battery_wake] | BatteryWakeConfig | Reolink battery-camera wake service |
[logging] | LoggingConfig | Service log destination |
[operational_events] | OperationalEventsConfig | Health-event timing |
[operational_events.cameras."<camera-id-or-ip>"] | OperationalEventOverride | Per-camera timing overrides |
[privacy.default] | PrivacySchedule | Inherited server-enforced recurring privacy |
[privacy.cameras."<camera-id-or-ip>"] | PrivacySchedule | Per-camera server-enforced recurring privacy |
[event_forwarder.mqtt] | MqttForwarderConfig inside EventForwarderConfig | MQTT configuration; server-owned revision |
[camera_defaults] | CameraCredentialDefaults | Shared camera defaults |
[<namespace>.<camera-key>] | CameraConfig | Camera settings |
[<namespace>.<camera-key>.events] | EventConfig | Native camera event selection and filters |
[access_credentials] | PersistedAccessCatalog | Server-managed credential records |
[peek_layouts] | StoredRegistry | Server-managed layouts and per-user selection |
[configuration_templates] | StoredTemplateDocument | Server-managed camera templates |
[notifications] | NotificationConfiguration | Server-managed rule drafts and active rules |
[runtime_profiles] | ProfileRegistry | Server-managed profile definitions and selection |
[storage_migration] | StorageMigration | Server-managed pending storage move |
homekit is a reserved root section, not an implemented HomeKit configuration schema. Do not use
it as a camera namespace. Every other non-reserved root table is interpreted as a camera namespace;
cameras is the conventional name, not the only allowed one. A typo in a section name can therefore
be interpreted as camera configuration. Some structs ignore unknown fields, so successful TOML
parsing alone does not prove a field has an effect.
[runtime_profiles] is server-managed internal data. Its registry is bounded to 32 profiles, and
each profile can target at most 127 cameras with sparse runtime-safe overrides. Do not edit this
section manually. The partial implementation supports Administrator management and activation
through WebRTC StateStore, including bounded definition reads and staged bounded writes. Privacy,
recording, notifications, and camera availability reach their current runtime owners. Delayed
profile commands cannot replace newer pending runtime status; an already running camera stop is
not preempted. An active profile’s saved target that is no longer configured remains degraded
through startup, completion and live removal; re-adding the camera replaces that observation.
Activating Base clears missing-target observations from the previously active profile.
Events-only camera edits also remain in effect after a profile disables the camera and Base
enables it again. Profile targets use the server-managed profile_identity described below.
An Administrator can retry a pending or degraded selection with Reapply active policy in
Settings. Complete privacy leak, asynchronous convergence, capacity and performance qualification
remain open under #202.
Sources: configuration,
cameras, and the subsystem sources
linked below. Config.source is an in-memory copy of the original TOML table, marked serde(skip);
there is no writable source field in the file.
Passwords and secret references
Secrets is a flat BTreeMap<String, String>. Its keys must match [A-Z_][A-Z0-9_]*.
Nested tables, arrays, numbers, and booleans are not secret values. Enter real values directly on
the server; do not put them in source control, screenshots, logs, or shell arguments.
Example companion secrets file, with placeholders that must be replaced locally:
CAMERA_USERNAME = "replace-with-camera-user"
CAMERA_PASSWORD = "replace-with-camera-password"
FRONT_CAMERA_PASSWORD = "replace-with-front-camera-password"
MQTT_PASSWORD = "replace-with-broker-password"
The application configuration refers to those keys:
host = "0.0.0.0"
port = 8081
[camera_defaults]
username = "{secret:CAMERA_USERNAME}"
password = "{secret:CAMERA_PASSWORD}"
[cameras.front_door]
ip = "192.0.2.10"
password = "{secret:FRONT_CAMERA_PASSWORD}"
main_rtsp_url = "rtsp://{secret:CAMERA_USERNAME|url}:{secret:FRONT_CAMERA_PASSWORD|url}@192.0.2.10:554/stream1"
[cameras.back_door]
ip = "192.0.2.11"
[event_forwarder.mqtt]
username = "keeppeek"
password = "{secret:MQTT_PASSWORD}"
{secret:KEY}substitutes the string directly. It can occupy the entire value or part of it.{secret:KEY|url}percent-encodes a URL component. Use it for a password, username, or query value embedded in a URL, not for an entire URL. Bytes other than RFC 3986 unreserved characters are encoded, including@,:,/, spaces, and non-ASCII UTF-8 bytes.KEEPPEEK_SECRET_<KEY>in the server process environment overrides the same key in the file. A literal value does not consult either source.- A missing referenced key, malformed reference, unsupported modifier, or invalid secrets file fails loading or the corresponding settings operation. Diagnostics identify the key without including its value.
- Resolution substitutes strings, not TOML syntax. It does not convert a quoted numeric secret into an integer or expand table keys. Inserted secret values are not recursively expanded.
- Reusing a key gives several cameras the same credential. A per-camera key overrides the shared default. Rotate an ordinary secret in the companion file and restart KeepPeek to reconnect with the new value.
Reference boundaries
| Setting | Supported resolution |
|---|---|
| Root settings, camera defaults, camera entries, and ordinary settings sections | String values and strings inside arrays/tables are resolved before typed deserialization. The resolved string must still satisfy the field’s type and validation. |
[event_forwarder.mqtt].password | Direct or referenced string. The MQTT settings writer stores a supplied password in the companion file under MQTT_PASSWORD. |
Notification active.actions[].destination and draft.actions[].destination | The notification loader explicitly resolves destination references. Non-browser destinations saved through the rule store use managed KEEPPEEK_NOTIFICATION_... secret keys. |
Camera-template username_secret_reference and password_secret_reference | Complete reference tokens only, not inline credentials or interpolated surrounding text. Values are resolved when applying camera configuration. |
[access_credentials], [peek_layouts], other notification fields, and other template fields | Not blanket-expanded as secret templates. These sections have dedicated loaders. |
[storage_migration] | Concrete paths written by the server, not a secret-template interface. |
Configuration responses and unrelated settings updates preserve existing reference tokens instead of returning or persisting resolved credentials. Inline camera credentials remain supported but are not automatically moved to the secret file. Intentionally replacing a camera credential in the editor can replace its reference with an inline value. Ordinary layout exchange must not include credentials, verifiers, or unrelated user state. Full recovery archives have a different security contract; see backup and restore.
The first-run access key is generated as KEEPPEEK_ACCESS_KEY in the companion file and referenced
from the root access_key field. Named credentials created later retain only verifiers in the
application configuration and return their raw keys once. See
authentication and the
secret lifecycle guide.
Root settings
Type: Config. Nested section fields are listed in the section index.
| Field | Type | Default | Meaning |
|---|---|---|---|
host | String | "0.0.0.0" | HTTP listener bind address. |
port | u16 | 8081 | HTTP listener port. |
access_key | AccessKey | Unset in the model; generated on startup | Compatibility initial Administrator key: a UUID string, a reference resolving to a UUID, or integer 0 as the unset marker. 0 is not a switch that disables authentication. |
Access policy
Type: AccessConfig, section [access].
| Field | Type | Default | Validation and meaning |
|---|---|---|---|
local_networks | Vec<IpNet> | Networks below | At most 64 CIDRs. Matches become trusted-local Administrators without sign-in. Setting the array replaces the defaults. |
trusted_proxies | Vec<IpNet> | [] | At most 64 CIDRs, checked separately from local_networks. Only these peers can supply the supported forwarded-client header contract. |
require_secure_remote | bool | true | Require the declared secure boundary for remote traffic. |
failed_authentication_limit | u32 | 5 | Nonzero failed-attempt limit per address/window. |
failed_authentication_window_secs | u64 | 60 | Nonzero authentication-rate window, in seconds. |
session_idle_timeout_secs | u64 | 1800 | Nonzero; cannot exceed the absolute timeout. |
session_absolute_timeout_secs | u64 | 86400 | Nonzero maximum session lifetime. |
max_sessions_per_principal | u32 | 64 | Nonzero concurrent-session limit per identity. |
max_sessions_per_address | u32 | 128 | Nonzero concurrent-session limit per address. |
Default local networks are 127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16,
169.254.0.0/16, ::1/128, fc00::/7, and fe80::/10. A container bridge or VPN within those
ranges is trusted unless the operator narrows the list. Forwarding headers and CORS do not replace
authentication. Review authentication and access control before exposing the
listener.
Direct card origins
Type: DirectCardConfig, section [direct_card].
| Field | Type | Default | Validation and meaning |
|---|---|---|---|
allowed_origins | Vec<String> | [] | Unique canonical exact HTTP(S) origins, such as "https://ha.example". No credentials, path, query, fragment, wildcard, or trailing slash. |
External authentication configuration
Type: access::external::Config, optional section [external_auth]. Configured
OIDC providers use browser authorization-code login with PKCE. Configured identity
proxies use exact assertions from their immediate trusted transport peer. Both
methods use revocable browser sessions and the existing server-side camera grants.
Omitting the section preserves existing local and bearer behavior.
| Field | Type | Default | Validation |
|---|---|---|---|
allowed_origins | Vec<String> | Required | 1–16 unique canonical exact HTTPS origins, without credentials, path, query, fragment, wildcard, or trailing slash. |
providers | Vec<Provider> | Required | 1–4 providers with unique IDs. |
bearer_enabled | bool | false | Mixed mode requires an explicit transition deadline. |
bearer_transition_until_ms | Optional i64 | Absent | Positive Unix milliseconds; present if and only if bearer_enabled is true. |
Each [[external_auth.providers]] has id, name, mappings, and a method
table. IDs contain 1–64 ASCII letters, digits, underscores, or hyphens. Names
contain 1–64 bytes without control characters and cannot be whitespace-only.
At least one mapping is required per provider; there are at most 128 across all
providers. Unknown fields are rejected.
Each [[external_auth.providers.mappings]] contains claim (1–64 bytes), value
(1–256 bytes), and role (administrator or user). Claim names and values cannot
be whitespace-only or contain control characters. User mappings require an explicit
camera_access table using the existing all_cameras, camera_ids, and group_ids
fields and limits, with at most 128 camera and group IDs combined per external
identity policy. Set all_cameras and camera_ids explicitly; group_ids defaults
to an empty list. With all_cameras = false, empty camera and group lists grant
no cameras. Administrator mappings must
omit camera_access because Administrators are unrestricted. Unmapped identities
and identities matching multiple rules are denied. Mapping claims must be strings
or lists of at most 128 strings, each at most 256 bytes; the claims object is limited
to 16 KiB serialized JSON.
For [external_auth.providers.method] with kind = "oidc":
| Field | Type | Default | Validation |
|---|---|---|---|
issuer | String | Required | HTTPS URL without user-info, query, or fragment. |
client_id | String | Required | 1–256 bytes, nonblank, no control characters. |
client_secret | Optional String | Absent | Complete {secret:KEY} or {secret:KEY|url} reference; resolved value is nonblank, at most 4,096 bytes, without control characters. Inline secrets are rejected. |
redirect_uri | String | Required | Allowed HTTPS origin with exactly /auth/callback, no query or fragment. |
scopes | Vec<String> | ["openid"] | At most 16 unique OAuth scope tokens of 1–64 bytes; must include openid. |
display_name_claim | String | "name" | 1–64 bytes, nonblank, no control characters. |
endpoint_origins | Vec<String> | [] | At most 16 unique canonical HTTPS origins for additional provider endpoints. |
private_networks | Vec<IpNet> | [] | At most 64 explicitly permitted private issuer networks. |
logout_uri | Optional String | Absent | HTTPS URL without user-info, query, or fragment. |
All external-authentication URLs are limited to 2,048 bytes. A configured URL does not authorize arbitrary discovery redirects or other network destinations.
For [external_auth.providers.method] with kind = "proxy":
| Field | Type | Default | Validation |
|---|---|---|---|
trusted_peers | Vec<IpNet> | Required | 1–64 immediate transport-peer CIDRs, independent of forwarded-client classification. |
subject_header | String | Required | Exact identity assertion header. |
role_header | String | Required | Exact role assertion header; values still require explicit mapping. |
name_header | Optional String | Absent | Optional display metadata header. |
secret_header | Optional String | Absent | Present if and only if shared_secret is present. |
shared_secret | Optional String | Absent | Same reference and resolved-value rules as client_secret. |
Header names are distinct ignoring ASCII case, at most 64 ASCII letters, digits,
or hyphens, start with X-, and cannot start with X-Forwarded-. Secret references
remain unchanged in config.toml; only the runtime copy resolves them. Existing
configuration validation rejects invalid candidates before the atomic writer runs.
Trusted peer networks from different identity proxy providers must not overlap. KeepPeek checks the immediate peer after normalizing IPv4-mapped IPv6 addresses; configure IPv4 CIDRs for those peers.
Durable external identities
[external_identities] has version = 1 and at most 1,024 records. Each record
contains id (non-nil UUID), provider_id, subject_fingerprint (64 lowercase
hexadecimal characters), display_name, role, camera_access, positive
revision, enabled, and nonnegative created_at_ms. Provider and display names
are nonblank, at most 64 bytes, and contain no control characters. Subject
fingerprints and IDs are unique. Administrator camera access is unrestricted.
The optional admission_policy_fingerprint is a 64-character lowercase hexadecimal
digest of the resolved provider policy at successful authentication. KeepPeek
updates it after ordinary or replacement-Administrator authentication. An absent
or stale digest cannot count as a retained Administrator path; the identity must
authenticate under the current policy first. Legacy records remain readable and
still trigger lockout protection. Raw subjects, claims, and tokens are not stored.
Camera defaults and camera entries
Source: camera types and the camera loader in configuration.
Type: CameraCredentialDefaults, section [camera_defaults].
| Field | Type | Default or inheritance |
|---|---|---|
username | String | Empty string; accepts secret references. |
password | String | Empty string; accepts secret references. |
backend | Optional CameraBackend | No shared override; camera model defaults to "auto". |
transport | Optional CameraTransport | No shared override; camera model defaults to "tcp". |
record_generic_motion_events | Optional bool | No shared override; camera model defaults to false. |
recording_mode | Optional CameraRecordingMode | No shared override; camera model defaults to "event-boost". |
event_recording_duration_secs | Optional u64 | No shared override; camera model defaults to 60 seconds. |
event_pre_recording_duration_secs | Optional u64 | No shared override; camera model defaults to 0; range 0..30 seconds. |
event_recording_stream | Optional EventRecordingStream | No shared override; camera model defaults to "main"; "main" or "sub". |
Username and password fall back to their shared defaults when the per-camera resolved value is
empty, including when the field is omitted. The other shared defaults apply only when the
per-camera field is absent. Ports and RTSP URLs are not fields of CameraCredentialDefaults.
Type: CameraConfig, section [<namespace>.<camera-key>], usually [cameras.front_door].
Each camera table also contains the server-managed profile_identity string. It is a unique,
canonical, 36-character UUID v4 assigned at startup or when the camera is created. It has no
operator-supplied default and does not accept secret references. The configuration owner preserves
it across ordinary camera edits; changing the IP in the same saved entry keeps profile references.
Deleting the camera and adding another at the same IP assigns a new identity. Saved profile targets
for the deleted identity remain unavailable until an Administrator explicitly removes or retargets
them. Other camera controls and media source IDs continue to use their existing IP identities.
Camera ip supports references such as {secret:CAMERA_IP}; the resolved value must be an IP
address. Identity migration, the profile identity map and camera definitions use the same loaded
disk secrets.toml snapshot. The raw reference stays in config.toml after migration instead of
being replaced by the resolved address. Editing the secrets file does not change an already loaded
configuration snapshot; the next configuration load uses the updated address and preserves the
camera’s saved profile_identity.
On upgrade, KeepPeek assigns identities and converts existing IP-based profile references in one
atomic configuration write before media starts. A current, unambiguous target becomes its saved
identity. An absent target becomes an unavailable missing-ip:<address> reference that cannot bind
to a later device at that address. Changed profile and registry revisions advance once, so stale
clients must reload. Invalid or duplicate UUIDs and ambiguous active targets fail validation without
writing the configuration. Keep the identity metadata when exporting or restoring the full
configuration; do not edit it manually.
A migrated [runtime_profiles] section carries server-managed identity_schema = 1. Keep it
with the profile definitions. Earlier KeepPeek binaries reject this section rather than starting
with UUID targets that they cannot apply; restoring an older binary requires restoring a
compatible pre-migration configuration as well. Unknown identity schema versions fail validation.
| Field | Type | Default | Meaning |
|---|---|---|---|
ip | IpAddr | Required | Camera IP address, not a DNS hostname. |
name | Optional String | Camera table key | The loader replaces this value with the table key. Use display_name to change the UI label without changing storage identity. |
display_name | Optional String | Falls back to name | Human-readable camera label. |
manufacturer | Optional String | No override | Nonempty trimmed value takes precedence over discovery. |
username | String | Shared default or empty | Camera username; literal or secret reference. |
password | String | Shared default or empty | Camera password; literal or secret reference. |
onvif_port | Optional u16 | 8000 when unset | ONVIF service port. Typed configuration operations require 1..65535. |
http_port | Optional u16 | 80 when unset | Direct camera HTTP control port; typed operations require 1..65535. |
main_rtsp_url | Optional String | Discover stream | Explicit main-stream RTSP URL takes precedence over ONVIF discovery. Blank values are not explicit URLs. |
sub_rtsp_url | Optional String | Discover stream | Explicit sub-stream RTSP URL. Use URL-escaped references in credential components. |
uid | Optional String | None | Reolink P2P UID for direct BCUDP discovery. |
backend | CameraBackend | Shared default or "auto" | "auto", "retina", or "reo-proto". |
transport | CameraTransport | Shared default or "tcp" | "tcp" or "udp". |
record_generic_motion_events | bool | Shared default or false | Opt in to supported generic motion events. |
recording_mode | CameraRecordingMode | Shared default or "event-boost" | "off", "sub", "main", "both", "event-boost", or "event-only". |
event_recording_duration_secs | u64 | Shared default or 60 | Event-triggered recording duration; typed operations and TOML loading for event-only or pre-roll-enabled event-boost require 1..3600 seconds. |
event_pre_recording_duration_secs | u64 | Shared default or 0 | Range 0..30 seconds; 0 disables pre-roll allocation. |
event_recording_stream | EventRecordingStream | Shared default or "main" | "main" or "sub"; selects the event-only recording stream. EventBoost always boosts to main. |
events | EventConfig | Defaults below | Camera-native event policy, independent of video backend; configured through the file. |
These are stored configuration types, not promises that every camera supports every backend,
transport, or recording capability. Discovery results such as CameraCapabilities, CameraPorts,
MediaProfile, VideoConfig, and AudioConfig are not additional editable camera tables.
Native camera events
Type: EventConfig, section [<namespace>.<camera-key>.events], for example
[cameras.front_door.events]. Source:
camera event policy.
These settings are not part of CameraCredentialDefaults and are not exposed by the fixed
protobuf camera-settings form. Ordinary camera API edits preserve the saved event table.
| Field | Type | Default | Values and limits |
|---|---|---|---|
mode | EventMode | "auto" | "auto", "vendor", "onvif-pullpoint", "rtsp-metadata", or "disabled". |
metadata_stream | MetadataMode | "auto" | "auto", "enabled", or "disabled". Enabling metadata does not override vendor or disabled event mode. |
event_service_url | Optional String | None; discover service | HTTP(S) URL of at most 4,096 UTF-8 bytes using the exact configured camera IP. No hostname alias, wildcard/multicast address, port zero, credentials, query, fragment, backslash, whitespace, or control character. |
source_tokens | Vec<String> | [] | At most 32 exact opaque tokens, each at most 256 UTF-8 bytes. Empty or control-character entries are rejected. |
include_topics | Vec<String> | [] | Expanded namespace root plus slash-separated local path. Up to 32 filters combined with exclusions, each at most 1,024 UTF-8 bytes. |
exclude_topics | Vec<String> | [] | Same syntax and shared count/byte limits as inclusions; exclusions take precedence. |
snapshots | bool | true | Allow optional camera snapshots; a missing or failed image does not discard the event. |
An empty inclusion list permits recognized topics. An example filter is
{http://www.onvif.org/ver10/topics}VideoSource/MotionAlarm; unstable document prefixes such as
tns1:VideoSource/MotionAlarm are rejected. Source tokens retain their exact text. Filters require
nonempty namespace and path segments without whitespace, embedded prefixes, or control characters.
The loader resolves supported {secret:KEY} and {secret:KEY|url} references in event strings
before validation. Preserve those references in config.toml and keep reusable private values
in secrets.toml; URL escaping does not relax endpoint restrictions. Direct file edits require
the existing configuration activation/restart path; there is no automatic file watcher. Generic
motion recording still requires record_generic_motion_events = true, independently of event mode.
The native event operator guide
describes transport selection, health evidence, and verification.
Recording storage
Type: StorageToml, section [storage]. Optional paths are resolved by the runtime when absent;
inspect the effective settings instead of assuming an omitted path is the current directory.
Both recording directories default to recordings/ under the default OS KeepPeek configuration
directory, even when --config selects a configuration file elsewhere. An omitted catalog path
becomes recordings.db under the effective long-term directory; the thumbnail directory defaults
to .event-thumbnails under that same directory. See
StorageConfig::from_toml.
| Field | Type | Default | Meaning |
|---|---|---|---|
medium_term_path | Optional String | Runtime-selected | Medium-term recording directory. |
long_term_path | Optional String | Runtime-selected | Long-term recording directory. |
recording_catalog_path | Optional String | Runtime-selected | Recording catalog path. |
event_thumbnail_path | Optional String | Runtime-selected | Event-thumbnail directory. |
event_thumbnail_max_mb | u64 | 1024 | Thumbnail storage budget, in MiB. |
short_term_secs | u64 | 120 | Short-term retention, in seconds. |
medium_term_secs | u64 | 1800 | Medium-term retention, in seconds. |
flush_interval_secs | u64 | 60 | Recording flush interval, in seconds. |
write_buffer_bytes | usize | 8192 | Recording write-buffer size, in bytes. |
long_term_max_gb | u64 | 1024 | Long-term storage budget, in GiB. |
minimum_free_gb | u64 | 10 | Minimum-free-space safety threshold, in GiB. |
maximum_used_percent | Optional u8 | No percentage threshold | When set, 1..99; filesystem-used percentage threshold. |
warning_free_gb | u64 | 20 | Warning-free-space threshold, in GiB. |
critical_free_gb | u64 | 10 | Critical-free-space threshold, in GiB. |
cleanup_hysteresis_gb | u64 | 5 | Cleanup recovery margin, in GiB. |
pre_recording_stream_max_bytes | u64 | 67108864 (64 MiB) | Positive encoded-byte limit per candidate stream; must not exceed the global limit. |
pre_recording_global_max_bytes | u64 | 268435456 (256 MiB) | Positive encoded-byte limit across all candidate streams, including pending replay; must fit the platform address space and TOML signed 64-bit integer range. |
The effective critical threshold is the greater of critical_free_gb and minimum_free_gb.
If warning_free_gb is zero while the effective critical threshold is nonzero, the warning
threshold is derived as critical plus hysteresis. Otherwise, warning must be at least critical.
Effective media paths come from named owners, and catalog paths come from the metadata binding.
Use named-volume move controls for stored objects and the confirmed metadata restart workflow
for the catalog. The general storage editor displays these paths read-only.
These values are policy defaults, not a capacity guarantee. Storage budgets and free-space thresholds must suit the actual disks and camera bitrates. A configuration ZIP preserves the target’s existing storage paths on apply; it does not move a recording archive. See storage migration and recording archive recovery.
event_thumbnail_max_mb applies to catalog-referenced image files in the configured thumbnail
directory. Unindexed images and temporary files are excluded from this quota and are not removed
at startup. Missing image files retain their catalog references so that restoring an unavailable
thumbnail directory does not require rebuilding event metadata.
Recording retention rules
Optional [storage.retention] rules set independent recording lifetimes in days.
Without this section, recordings keep the existing capacity-based cleanup behavior.
[storage.retention.default]
continuous_days = 1.0
motion_days = 7.0
[storage.retention.default.events]
person = 30.0
[storage.retention.cameras."192.0.2.10"]
continuous_days = 0.0
default supplies global rules. Each cameras entry overrides rules for the exact
catalog camera identity, which is the camera IP address for RTSP recordings.
Omitted fields inherit defaults; explicit zero disables that rule. Event entries
use exact canonical event kind strings. Motion rules match canonical motion
events. Stream-specific events apply to their matching logical recording stream;
events without a stream apply to all recording streams for that camera.
Durations must be finite, nonnegative, and representable as whole milliseconds within a signed 64-bit lifetime. Settings allow at most 127 camera overrides and 16 effective rules per camera. Event selectors contain 1–128 ASCII letters, digits, underscores, hyphens, or periods. Unknown fields are rejected. Numeric durations do not accept secret references.
The longest matching rule sets a whole-file deadline from the recording end. Files can therefore retain unrelated footage beside a matching event. Protection blocks automatic deletion. Shorter settings, removed rules, or corrected events never shorten an already committed deadline. Files whose event evidence exceeds the bounded evaluation limit remain excluded from automatic cleanup until their evidence can be evaluated successfully.
Apply file edits through the existing configuration activation/restart workflow; there is no file watcher or separate retention settings file. Activation persists a bounded catalog sweep and fences new cleanup admissions until evaluation finishes. Finalized recordings and event changes then receive automatic reevaluation. Expiry removes eligible whole files even when storage has spare capacity. Interrupted cleanup resumes through the existing storage owner.
To diagnose activation, inspect structured log events retention_started and
retention_activation_ready, correlated by run_id. The first reports whether
retention is enabled and the settings request was accepted; the second reports
whether initial evaluation encountered quarantined files. Per-file evaluation
warnings identify the recording and cause. Expiry logs identify removed recordings
and whole-file bytes. These answer whether activation finished, which evidence
needs repair, and whether expiry made progress. The worker logs no serialized
configuration or event payloads.
Named storage volumes
[storage.named_volumes] stores volume and placement definitions in config.toml.
A fresh installation creates four private roots beneath storage/ beside the configuration:
media for active and archived recordings, exports, images for thumbnails, and metadata
for the catalog and export history. Global placement rules use these roots without fallback.
The initial media and image caps use long_term_max_gb and event_thumbnail_max_mb;
after initialization, edit each volume’s byte cap directly.
Saved volume and placement changes take effect after restart and affect future placement. Existing objects keep their catalog owner. Create custom root directories before enabling them. A missing bound root stays unavailable; startup does not recreate it or redirect writes.
The optional section has two arrays: volumes (at most 32) and placement (at most 256).
Both default to empty. Ordinary settings updates that omit named_volumes preserve the section.
An explicit empty section requests removal, subject to ownership and metadata checks.
Unknown fields in this section and its entries are rejected.
Administrator runtime settings carry these definitions in the optional protobuf named_volumes
field. Updates that include it require the current configuration revision. String fields,
including IDs, roots, selectors, and candidate IDs, support existing secret references.
Validation uses resolved values; saved settings and responses retain the references.
Administrators can edit volumes under Settings → Storage → Named storage volumes. Byte limits use exact whole-byte values. Refreshing status or a failed save preserves unsaved inputs. Removing a saved volume definition requires confirmation. Root probes inspect existing configured roots without creating directories; a successful probe does not establish write permission. Individual move preview, confirmation, status, and cancellation appear only when a volume runtime is available. The same runtime controls let administrators stop new writes to a volume or clear that operator drain, after confirmation. Already admitted writes finish normally. Clearing an operator drain does not override a draining or read-only saved configuration. Operator drain survives restart and does not itself move existing media.
The move controls preview up to 16 objects from the loaded page and show exact file and byte totals before confirmation. Each confirmed object has a durable move job. Stop dispatch after the current request or cancel admitted work under Move jobs. Refresh jobs to inspect progress; refresh the object page after completed batches. A lost confirmation reply is checked against the same job identity before further work is admitted.
Removing a bound definition requires stopped writes, zero owned and reserved bytes, and completed cleanup receipts. An offline enabled runtime must be stopped through operator drain, or disabled and restarted before removing an unbound definition. Remove archive-policy references and restart before removing their destination; already captured archive work must finish first. Removal keeps the catalog’s immutable binding and completed history and does not delete files. The removal check inspects up to 1,024 pending archive policies; finish a larger backlog before retrying removal.
[storage.metadata] is a server-managed binding, not an ordinary placement draft. It contains
volume_id, catalog_file, history_file, catalog_id, generation, filesystem, and
root_identity. The filenames use one shared canonical UUID with catalog-<uuid>.db and
exports-<uuid>.json. The catalog ID is a UUID, generation is 1 through i64::MAX, and both
filesystem identity strings contain 1–256 bytes without control characters. The volume must
exist, have the metadata role, and be enabled; a separate recording_catalog_path override is
incompatible with this binding. Startup verifies the saved directory and catalog authority.
It does not create an empty replacement catalog if the selected owner is unavailable.
Ordinary settings cannot install, clear, disable, or relocate the metadata owner. Unrelated
updates preserve its existing secret references. Use the administrator metadata preview and confirmation controls to schedule relocation;
do not construct or edit this binding manually.
The internal [storage.metadata_pending] restart record contains a 32-byte source-configuration
digest and the target binding. It retains the previous effective paths until a stopped transfer
verifies both the catalog snapshot and copied export history, then atomically commits the new
binding. Failed or interrupted transfers retain their source files and pending state for retry.
Pending handoffs reject effective storage-setting changes while allowing unrelated settings saves.
Startup rechecks available space, the byte cap, and the larger minimum/critical free-space reserve
before fencing the source. Managed export history must already exist, fit within 8 MiB, and contain
valid, uniquely owned jobs; missing or invalid history is preserved and reported unavailable.
In Settings, add and save a disabled volume with only the Metadata role, then use Preview metadata move to check its root and required space. Confirm metadata move saves the plan without interrupting recording. The service restart copies the latest catalog and export history before workers resume. Cancel a pending plan before restarting if needed. Returning to an earlier metadata volume uses the same workflow and a new catalog generation; never point the service at a retained old catalog. An interrupted startup leaves the plan available for retry after its reported storage problem is corrected.
Each [[storage.named_volumes.volumes]] entry has these fields:
| Field | Type | Default | Meaning |
|---|---|---|---|
id | String | Required | 1–64 lowercase ASCII letters, digits, _ or -; legacy- is reserved. |
root | Path string | Required | Absolute UTF-8 directory path, at most 4096 bytes and 64 components; no traversal, control characters, duplicate or nested roots. Windows requires a local drive path and rejects device names and reserved characters. |
roles | Role array | Required | 1–5 distinct values: active, archive, export, thumbnail, metadata. Only one bound metadata owner is active; other metadata-capable volumes can be relocation targets. |
state | State | enabled | enabled admits new writes; read_only and draining retain reads but stop new writes; disabled does not open the root. |
priority | u16 | 0 | Smaller values rank first for priority placement. |
capacity_bytes | Optional u64 | No cap | Positive owned-data byte cap. |
minimum_free_bytes | u64 | 0 | Minimum filesystem free space to retain. |
critical_free_bytes | u64 | 0 | Placement retains the larger of minimum and critical free space. |
warning_free_bytes | u64 | 0 | Must be at least both minimum and critical free space. |
sources, groups | String arrays | Empty | At most 256 unique exact selectors each, 1–256 bytes without control or surrounding whitespace. Empty lists allow all sources. When either list is populated, matching either a source or a group permits placement. Metadata volumes cannot restrict sources or groups. |
All byte thresholds must fit a signed TOML 64-bit integer. A root can use the existing
{secret:KEY} or {secret:KEY|url} resolver; unchanged root references are preserved during
settings updates. Paths are redacted from the volume model’s debug output. Lexical validation
does not establish filesystem identity, writability, or protection against path replacement.
On Windows, owned storage requires local NTFS volumes with persistent ACLs and directory metadata flushing. ReFS, FAT, and network shares are not qualified for owned writes or deletion; they remain unavailable rather than bypassing these checks. Keep the configuration directory on NTFS when using the automatically created default volumes. An unavailable bound root is never recreated.
Each [[storage.named_volumes.placement]] entry contains:
| Field | Type | Default | Meaning |
|---|---|---|---|
role | Role | Required | One of the roles above. |
source, group | Optional string | Absent | Select either a source or a group, using the same string limits as volume allowlists. Absence selects the role default. |
candidates | Volume ID array | Required | 1–8 distinct configured volumes supporting the role. |
strategy | Strategy | priority | priority ranks smaller priorities first; free_space ranks usable free bytes after reserve and cap limits. Ties use volume ID order. |
allow_fallback | Boolean | false | False limits selection to the first candidate. True permits ranking all eligible candidates in the explicit pool. |
Source rules take precedence over group rules, which take precedence over role defaults. A failed override never falls through to a broader rule. Duplicate selectors for a role are invalid. Metadata placement requires one global candidate and forbids fallback. Selection is a pure proposal; it does not reserve capacity, authorize writes, or alter existing object locations.
Source: named-volume model. Windows path validation follows the Windows filename rules.
Fourth-tier local archive
Optional [storage.archive] configures one verified local replica destination. Omitting it disables
this tier. It is separate from named-volume archive placement, which relocates the authoritative
recording. A verified fourth-tier copy retires its primary only through an enabled eviction policy
or an Administrator’s confirmed per-job action. Local recording, live viewing and the first three tiers do not wait for archive
availability.
[storage.archive]
target_id = "local-archive"
root = "{secret:ARCHIVE_ROOT}"
enabled = false
automatic = false
source_ids = []
minimum_age_secs = 0
evict_after_archive = false
maximum_object_bytes = 4294967296
maximum_pending_count = 256
maximum_pending_bytes = 68719476736
operation_timeout_secs = 120
maximum_attempts = 3
| Field | Type | Default | Validation and meaning |
|---|---|---|---|
target_id | String | Required | Stable ID with 1–64 lowercase ASCII letters, digits, hyphens or underscores; legacy- is reserved. |
root | PathBuf | Required | Absolute local destination; at most 4,096 UTF-8 bytes and 64 path components. It must already exist when used. Named-volume path safety rules apply. |
enabled | bool | false | Enable archive work; configuration alone does not bind or create the destination. |
automatic | bool | false | Admit eligible finalized recordings automatically when the archive worker is enabled. |
source_ids | Vec<String> | Empty | Automatic-selection filter; empty selects all sources. At most 127 distinct, nonblank IDs, each at most 256 UTF-8 bytes without control or surrounding whitespace. |
minimum_age_secs | u64 | 0 | Minimum finalized recording age for automatic selection, at most 315,360,000 seconds (ten 365-day years). |
evict_after_archive | bool | false | Explicitly permit policy-driven primary-copy eviction after verification; holds, leases and durable archive indexes still apply. |
maximum_object_bytes | u64 | 4,294,967,296 | Per-object admission limit, from 1 byte through 64 GiB; cannot exceed the pending-byte limit. |
maximum_pending_count | u32 | 256 | Retained pending jobs, from 1 through 4,096. |
maximum_pending_bytes | u64 | 68,719,476,736 | Pending-byte admission ceiling, positive and at most i64::MAX. |
operation_timeout_secs | u64 | 120 | Finite archive operation deadline, from 1 through 3,600 seconds. |
maximum_attempts | u32 | 3 | Maximum attempts for one durable job, from 1 through 10. |
Roots cannot overlap named-volume roots or legacy media, thumbnail and metadata directories. Lexical checks do not establish filesystem identity; the bounded owned-object write/read/delete probe and persisted catalog binding detect offline or replaced destinations without recreating their roots. Keep the stable target ID with its binding; changing a configured path must not adopt an old manifest under a different root identity.
Destination availability requires a successful verified operation. Opening or binding the root alone does not establish availability; test the destination after startup and refresh its result.
String settings support existing secret references. Validation uses resolved values; ordinary settings rewrites and configuration export/import preserve raw references and unrelated sections. Older runtime settings updates that omit archive settings preserve the accepted archive section. Unknown archive fields are rejected; cloud provider selectors and credentials are not supported by this local implementation. Job journals, manifests, root bindings and integrity metadata are operational catalog state, not extra settings files.
Archive playback reuses the timeline and normal playback controls. Initial access and seeking may take longer than the first three tiers because target access and integrity checks can add work. Available local archive files can play directly without a full-file restore; unavailable or corrupt objects remain explicit errors. Restoring or removing a primary copy preserves the original media identity and archive timeline indexes.
Battery wake
Type: BatteryWakeConfig, section [battery_wake]. This is the Reolink wake middleman service,
not a general Wake-on-LAN or scheduled privacy configuration.
| Field | Type | Default | Validation and meaning |
|---|---|---|---|
enabled | bool | false | Enable the wake service. |
bind | Optional Ipv4Addr | Automatic | Local IPv4 binding selection. |
middleman_port | u16 | 9999 | Nonzero middleman port. |
register_port | u16 | 58200 | Nonzero registration port; must differ from the middleman port. |
heartbeat_secs | u64 | 20 | Nonzero heartbeat interval. |
stale_after_secs | u64 | 80 | Must cover at least one heartbeat interval. |
Logging
Type: LoggingConfig, section [logging].
| Field | Type | Default | Meaning |
|---|---|---|---|
service | ServiceLogDestination | "file" | "file" or "event_log"; service logging destination, with Event Log relevant to Windows service operation. |
This section does not define arbitrary tracing filters, log payloads, or a diagnostics archive.
The existing diagnostic log-filter control uses a separate sibling file named log-filter, not
another TOML section. The logging service saves and reloads that filter atomically. At startup a
valid saved filter takes precedence over RUST_LOG; the built-in fallback is
info,keeppeek=debug. A bad saved or environment filter is reported and falls back to the next
usable source. This diagnostic setting is outside the two-file configuration ZIP. Preserve it with
the configuration directory if you need it during a recovery rehearsal. Source:
logging service.
Operational events
Type: OperationalEventsConfig, section [operational_events].
| Field | Type | Default | Meaning |
|---|---|---|---|
warning_hold_down_secs | u64 | 15 | Delay before recording a warning transition. |
outage_hold_down_secs | u64 | 60 | Delay before recording an outage. |
recovery_debounce_secs | u64 | 10 | Stable-recovery debounce period. |
record_short_flaps | bool | false | Retain supported brief-flap transitions. |
cameras | Map of OperationalEventOverride | Empty | Overrides keyed by stable camera ID, with camera IP as fallback. |
Type: OperationalEventOverride, section [operational_events.cameras."<camera-id-or-ip>"].
| Field | Type | When absent |
|---|---|---|
warning_hold_down_secs | Optional u64 | Inherit global warning hold-down. |
outage_hold_down_secs | Optional u64 | Inherit global outage hold-down. |
recovery_debounce_secs | Optional u64 | Inherit global recovery debounce. |
record_short_flaps | Optional bool | Inherit the global flag. |
Each effective policy requires warning hold-down no greater than outage hold-down. Outage and
recovery durations cannot exceed 86400 seconds. Override keys contain 1 to 256 bytes after the
nonempty check. The stable camera ID takes precedence over an IP-keyed override.
Privacy schedules
Type: PrivacyConfig, section [privacy]. The optional [privacy.default] schedule is inherited
by cameras without a per-camera entry. Per-camera schedules are keyed by stable camera ID; an
IP-keyed entry is accepted for migration and the runtime maps it to that camera ID.
Types: PrivacySchedule, sections [privacy.default] and
[privacy.cameras."<camera-id-or-ip>"].
| Field | Type | Default | Meaning |
|---|---|---|---|
enabled | bool | true | Retain the policy but disable its scheduled enforcement when false. |
timezone | String | Required | IANA timezone used for recurring local windows. |
windows | Array of PrivacyWindow | Empty | Half-open weekly privacy windows; at most 64. |
temporary_override | Optional PrivacyOverride | None | Bounded persisted override that suspends matching windows. |
keep_camera_connected | bool | true | Keep camera ingress warm while delivery is blocked. |
Each PrivacyWindow has weekdays (ISO values 1 through 7), start, and end in HH:MM.
Equal endpoints are rejected; an end earlier than the start is an overnight interval. Both
occurrences of a repeated daylight-saving local time are protected. A PrivacyOverride requires
nonempty actor and reason, RFC3339 accepted_at and expires_at, and expires within 24 hours.
Control-channel configuration requests replace the override actor and acceptance time with the
authenticated administrator and server time before the candidate configuration is validated.
Invalid privacy policy data fails validation before the server accepts camera media.
MQTT event forwarding
Types: EventForwarderConfig has one field, mqtt: MqttForwarderConfig. Section:
[event_forwarder.mqtt]. Source:
MQTT configuration.
| Field | Type | Default | Validation and meaning |
|---|---|---|---|
revision | u64 | 1 | Server-managed settings revision. |
enabled | bool | false | Enable forwarding. |
broker_url | String | "mqtt://127.0.0.1:1883" | mqtt or mqtts URL with host; no user-info, non-root path, query, or fragment. |
client_id | String | "keeppeek" | Broker client ID. |
instance_id | String | "home-nvr" | KeepPeek instance identity. |
forwarder_id | String | "mqtt" | Forwarder identity. |
topic_prefix | String | "keeppeek" | 1 to 512 bytes; no leading/trailing slash, NUL, +, or #. Interior slashes are allowed. |
username | Optional String | None | Broker username, including supported references. |
password | Optional String | None | Requires username; prefer a reference to the companion secret file. |
tls_ca_path | Optional PathBuf | System trust | Custom CA path; only valid with mqtts. |
qos | u8 | 1 | 0, 1, or 2. |
retain_events | bool | false | Retain event publications. |
retain_health | bool | true | Retain health publications. |
outbox_max_mb | u64 | 64 | 1 to 65536 MiB of bounded outbox storage. |
retry_min_ms | u64 | 250 | Nonzero retry minimum, no greater than maximum. |
retry_max_ms | u64 | 30000 | Retry maximum, at most 3600000 ms. |
Client, instance, and forwarder IDs each contain 1 to 128 bytes without NUL, /, +, or #.
Do not embed credentials in broker_url; use the dedicated fields. MQTT’s bounded outbox and
deduplication state are in memory and reset on restart. outbox_max_mb is a runtime memory budget;
it does not enable persistence. The retired mqtt-forwarder.db is removed during startup and is
not replayed. See Notifications and integrations.
Credential records
Source: access persistence. Use the Administrator credential controls, not hand-written verifiers or revision changes.
Type: PersistedAccessCatalog, section [access_credentials].
| Field | Type | Rule |
|---|---|---|
version | u32 | Required; supported value is 1. |
credentials | Vec<StoredCredential> | Required array, at most 128 records with unique IDs. |
Type: StoredCredential, array [[access_credentials.credentials]].
| Field | Type | Rule |
|---|---|---|
id | Uuid | Stable named-credential identity. |
name | String | 1 to 64 printable bytes after trimming. |
description | Optional String | At most 256 printable bytes; empty descriptions normalize to absent. |
role | AccessRole | "administrator" or "user". |
camera_access | Optional CameraAccess | Per-user group and camera policy. Absent is unrestricted; new credentials explicitly default to everything. |
verifier | AccessKeyFingerprint | SHA-256 fingerprint serialized as 32 byte-valued integers, not the raw access key. Treat it as sensitive authentication material. |
created_at_ms | i64 | Creation time, Unix milliseconds. |
rotated_at_ms | Optional i64 | Last rotation time; absent if never rotated. |
last_used_at_ms | Optional i64 | Accepted by the stored record type for compatibility, but cleared on load and omitted by the canonical writer. Activity is runtime state. |
expires_at_ms | Optional i64 | Expiry time; expired credentials are denied. |
disabled | bool | Disabled credentials cannot authenticate. |
revoked_at_ms | Optional i64 | Permanent revocation time. |
revision | u64 | Nonzero credential revision used to invalidate stale sessions. |
legacy | bool | Compatibility initial-credential marker. |
initial_secret_pending | bool | Initial-key retrieval is still pending. |
The runtime AccessCatalog.audit array is not part of PersistedAccessCatalog. Audit history,
session state, and credential last-use activity reset on restart; credential identities, verifiers,
roles, revisions, and lifecycle metadata persist. The legacy access.toml is a migration input,
not an additional active settings file.
Type: CameraAccess, nested table belonging to the credential record, not to a camera. The
all_cameras and camera_ids fields are required when the policy is present; group_ids defaults
to an empty list for compatibility. Unknown fields are rejected.
| Field | Type | Rule |
|---|---|---|
all_cameras | bool | Explicitly grant all current and future cameras. Must not be combined with a nonempty ID list. |
group_ids | Vec<String> | Camera configuration namespaces, such as outdoor for [outdoor.front_door]. Defaults to empty. At most 128 unique, nonblank names of at most 256 UTF-8 bytes without control characters. |
camera_ids | Vec<String> | At most 128 unique, nonblank IDs of at most 256 UTF-8 bytes without control characters. Use server-advertised camera IDs, not names. |
With all_cameras = false, access is the union of cameras in group_ids and explicit camera_ids;
both lists empty grants nothing. all_cameras = true requires both lists empty and includes future
groups and cameras. New credentials and absent policies default to everything. Existing explicit
restrictions are not broadened on upgrade. Administrators always have full access and cannot be
restricted through this policy. There is no secret substitution inside grant records.
The Administrator’s user-access editor validates camera and group IDs against the current fleet and
saves under the shared configuration lock. Each save increments the credential revision, rejects
stale writes, and invalidates existing sessions. Grants survive configuration reload and export/import.
Group grants are saved as group names, not expanded camera lists; effective access is resolved from
the server’s camera group membership. available_group_ids in management responses is bounded
discovery metadata for the editor, not a persisted field. The editor supports up to 128 available
groups using the same ID limits; an invalid or oversized inventory rejects management requests
before any policy mutation. Runtime group-membership changes close affected group-scoped sessions
and cancel their queued work so playback and event subscriptions cannot retain an obsolete grant.
Grid audiences do not grant camera permissions. Custom roles remain unsupported, and trusted-local
clients still follow the Administrator policy described above.
Peek layout registry
Source: layout persistence.
Layouts use the existing main configuration file and the keeppeek.peek-layouts.v1 capability.
Use the layout controls and revision-checked operations rather than manually constructing this
server-managed section.
Type: StoredRegistry, section [peek_layouts].
| Field | Type | Rule |
|---|---|---|
schema_version | u32 | Required; supported value is 1. |
revision | u64 | Nonzero registry revision. Stale writes conflict. |
shared_layouts | Vec<Layout> | Nonempty server-owned layout array. |
users | Map of StoredUserRegistry | Keyed by principal ID; holds per-user active selection and legacy private layouts. |
Type: StoredUserRegistry, section [peek_layouts.users."<principal-id>"].
| Field | Type | Rule |
|---|---|---|
active_layout_id | String | Selected layout identity. Missing selections are repaired by the registry. |
layouts | Vec<Layout> | Legacy private layout records; migrated to server-owned shared layouts with restricted audiences when opened. |
Type: Layout, under shared_layouts[] or legacy users.<principal-id>.layouts[].
| Field | Type | Rule |
|---|---|---|
id | String | Unique within the registry view; nonblank, at most 128 characters. |
name | String | Nonblank, at most 80 characters. |
scope | LayoutScope | "shared" for canonical layouts; "private" is retained for migration. |
owner_id | String | "server" for shared layouts; private records must match their principal. |
audience | LayoutAudience | Defaults to everyone with an empty credential list when the entire field is absent. |
activity_focus | bool | Whether activity can affect layout focus. |
display | Optional DisplaySettings | Dashboard-owned display settings; absent objects use defaults. Older client writes that omit the object preserve saved values. |
tiles | Vec<LayoutTile> | Ordered tiles, at most 64 per layout; no duplicate camera IDs or overlaps. |
Type: DisplaySettings, nested display table. The dashboard editor writes the complete object
with the existing registry revision. Only Administrators can change it. A User can still change
their selected dashboard but cannot alter its display settings. No field resolves secret references.
| Field | Type | Default | Validation and meaning |
|---|---|---|---|
version | u32 | 1 | Required in present objects; only version 1 is supported. |
tile_shape | TileShape | "16:9" | "16:9", "4:3", or "native". |
media_fit | MediaFit | "contain" | "contain" or "cover"; never stretch. |
streaming_mode | StreamingMode | "smart" | "smart" or "continuous". |
stream_limit | u32 | 12 | Whole number from 1 through 12; each browser also enforces its own capacity. |
keep_awake | bool | false | Saved wake-lock intent; browser gesture, visibility, permission, and device policy still apply. |
gap_px | u32 | 10 | Whole pixels from 0 through 24 between tile slots. |
corner_radius_px | u32 | 10 | Whole pixels from 0 through 24; zero gives square video corners. |
Display objects reject unknown fields, positional arrays, invalid types, and out-of-range values. The All cameras dashboard keeps immutable generated coordinates and identity, but its display object can be edited and survives camera-inventory synchronization. Preset names are derived from gap/radius values and are not an additional stored field.
Type: LayoutAudience, nested audience table.
| Field | Type | Rule |
|---|---|---|
everyone | bool | When true, credential_ids must be empty. |
credential_ids | Vec<String> | At most 128 unique canonical UUID strings. Save operations check that viewer identities exist. Empty with everyone = false gives no ordinary user access; Administrators retain management access. |
Type: LayoutTile, nested tiles[] array.
| Field | Type | Rule |
|---|---|---|
camera_id | String | Stable camera identity, not display name. |
column | u32 | One-based grid column. |
row | u32 | One-based grid row. |
column_span | u32 | Positive horizontal span; tile must fit the 12-column grid. |
row_span | u32 | Positive vertical span; tile must fit the 12-row grid. |
pinned | bool | Tile pinning state. |
The registry has a 256 KiB serialized bound and at most 32 layouts per validated view. Stored
camera references are retained when a camera disappears; additions are validated against known
cameras. The initial registry has revision 1 and an All cameras shared layout with ID
default, activity focus enabled, and up to 64 configured cameras. Opening the store also migrates
the legacy peek-layouts.json; that file is not an active persistence target.
The client-facing LayoutRegistry contains schema_version, active_layout_id, and layouts.
It is not the on-disk StoredRegistry; do not paste a client exchange document over the main
configuration. Grid audiences control grid visibility only. They must not be treated as permission
to view, export, or control an otherwise restricted camera.
The server intersects a User’s returned tiles with their camera grants. Selecting a dashboard from that filtered view updates only the principal’s active selection; it does not remove other cameras from the canonical layout. Unknown or no-longer-visible dashboard selections are rejected.
Camera templates
Source: configuration templates. Templates are versioned, server-managed configuration records, not another settings file.
Type: StoredTemplateDocument, section [configuration_templates].
| Field | Type | Rule |
|---|---|---|
document_version | u32 | Required; supported value is 1. |
templates | Vec<StoredTemplate> | Required array, at most 64 templates. Missing section means an empty document. |
Type: StoredTemplate, array [[configuration_templates.templates]].
| Field | Type | Rule |
|---|---|---|
template_id | String | Unique ID, 1 to 64 ASCII letters, digits, hyphens, or underscores. |
version | u64 | Server-managed template version. |
name | String | Nonblank, at most 128 bytes, no control characters; names are unique ignoring case. |
description | String | Defaults to empty and is omitted when empty; at most 1024 bytes, no control characters. |
values | StoredTemplateValues | At least one supported setting must be present. |
created_at_ms | i64 | Creation time, Unix milliseconds. |
updated_at_ms | i64 | Last update time, Unix milliseconds. |
Type: StoredTemplateValues, nested values table. Every field is optional and omitted when unset.
| Field | Type | Rule |
|---|---|---|
username_secret_reference | String | A complete valid secret reference, not an inline username. |
password_secret_reference | String | A complete valid secret reference, not an inline password. |
onvif_port | u16 | 1..65535. |
http_port | u16 | 1..65535. |
backend | CameraBackend | "auto", "retina", or "reo-proto". |
transport | CameraTransport | "tcp" or "udp". |
record_generic_motion_events | bool | Generic motion-event preference. |
recording_mode | CameraRecordingMode | "off", "sub", "main", "both", "event-boost", or "event-only". |
event_recording_duration_secs | u32 | 1..3600 seconds. |
event_pre_recording_duration_secs | u32 | 0..30 seconds. |
event_recording_stream | EventRecordingStream | "main" or "sub". |
The template writer enforces a 16 KiB serialized document limit. The legacy
configuration-templates.json is imported into this section and removed. Template references stay
as tokens in the stored template; applying a template must validate the resulting camera settings.
Notification rules
Sources: rule persistence, rule model, and cooldowns.
Type: NotificationConfiguration, section [notifications].
| Field | Type | Default and bound |
|---|---|---|
rules | Vec<PersistedRuleRecord> | []; at most 128 rules, with unique rule IDs. |
Type: PersistedRuleRecord, array [[notifications.rules]].
| Field | Type | Meaning |
|---|---|---|
id | String | Stable rule ID. |
owner_id | String | Owning principal ID. |
active | Optional Rule | Published rule; absent before first activation. |
active_revision | u64 | Active revision used for conflict checks. |
draft | Rule | Saved draft, which may be incomplete before activation. |
draft_revision | u64 | Draft revision used for conflict checks. |
created_at_ms | i64 | Creation time, Unix milliseconds. |
updated_at_ms | i64 | Last update time, Unix milliseconds. |
Rule identity, owner, and revisions must agree with the containing record. A draft is bounded
to 64 KiB in its JSON representation. Activating it requires complete rule validation. Runtime
RuleRecord.last_match_at_ms and last_delivery_at_ms are not persisted fields.
Type: Rule, under each record’s draft and optional active tables.
| Field | Type | Validation or default |
|---|---|---|
id | String | 1 to 128 ASCII letters, digits, ., -, or _. |
name | String | Nonblank, at most 128 characters. |
enabled | bool | Required; disabled rules do not match events. |
revision | u64 | Corresponding draft or active revision. |
owner_id | String | Same ID character and byte limits as id. |
triggers | Vec<Trigger> | Nonempty and unique; values below. |
filter | Filter | Defaults to an empty filter. |
schedule | Schedule | Required. |
cooldowns | Vec<Cooldown> | Defaults to []; scopes must be unique. |
rate_limits | Vec<RateLimit> | Defaults to []; scopes must be unique. |
critical_bypass | Optional CriticalBypass | Bounded critical-event quiet-hours bypass. |
enrichment | EnrichmentPolicy | Required. |
actions | Vec<Action> | 1 to 8 actions. |
failure | FailurePolicy | Required. |
Trigger values are "event_created", "event_updated", "event_ended", "outage_started",
"recovery", "storage_health", "recording_health", and "test".
Filters and schedules
Type: Filter, nested filter table.
| Field | Type | Default and rule |
|---|---|---|
source_ids | Vec<String> | []; at most 128 nonblank values. |
group_ids | Vec<String> | []; at most 128 nonblank values. |
event_kinds | Vec<String> | []; at most 128 nonblank values. |
zones | Vec<String> | []; at most 128 nonblank values. |
minimum_confidence | Optional f64 | Finite value in 0..1, inclusive. |
attachment_required | Optional bool | Require or exclude an available non-private attachment. |
minimum_duration_ms | Optional u64 | Minimum matching event duration. |
severities | Vec<Severity> | []; values "info", "warning", "critical". |
reviewed | Optional bool | Match the supplied review state. |
bookmarked | Optional bool | Match the supplied bookmark state. |
Empty filter arrays do not restrict that dimension. Filter fields describe matching inputs; their presence does not mean every camera or event producer supplies them.
Type: Schedule, nested schedule table.
| Field | Type | Default and rule |
|---|---|---|
timezone | String | Required recognized IANA timezone, such as "UTC". |
active_windows | Vec<WeeklyWindow> | [], meaning no active-window restriction. |
quiet_hours | Optional QuietHours | No quiet-hours suppression when absent. |
QuietHours has one required field, windows: Vec<WeeklyWindow>. WeeklyWindow fields are:
| Field | Type | Rule |
|---|---|---|
weekdays | Vec<Weekday> | Nonempty and unique; "monday" through "sunday". |
start_minute | u16 | 0..1439, local minutes after midnight. |
end_minute | u16 | 0..1439, exclusive end; must differ from start. |
An end earlier than the start describes an overnight window.
Cooldowns, limits, and delivery
All policy durations below are in milliseconds and must be between 1 and 2592000000
(30 days), inclusive, unless a tighter rule is stated.
| Type | Fields | Validation |
|---|---|---|
Cooldown | scope: CooldownScope, duration_ms: u64 | Scopes: "event", "camera_event_kind", "group", "rule", "outage". |
RateLimit | scope: RateLimitScope, maximum: u32, window_ms: u64 | Maximum 1..10000; scopes "rule", "channel", "principal", "global". |
CriticalBypass | maximum: u32, window_ms: u64 | Maximum 1..10. |
EnrichmentPolicy | deadline_ms: u64, maximum_revisions: u32, maximum_attempts: u32, maximum_attachment_bytes: u64, wake_after_deadline: bool | Revisions 1..32, attempts 1..8, attachment bytes 1..4194304 (4 MiB). All fields required. |
FailurePolicy | maximum_attempts: u32, maximum_retry_interval_ms: u64, expiry_ms: u64 | Attempts 1..10. An enabled push action requires retry interval at least 5000 ms. All fields required. |
Type: Action, nested actions[] array.
| Field | Type | Default and rule |
|---|---|---|
enabled | bool | true. |
channel | Channel | "browser", "push", "webhook", or "forwarder". Use channels advertised as available by the server. |
destination | String | At most 2048 bytes after resolution. Browser actions use the owner and require an empty destination. Push uses the Pushover destination format; webhook requires an HTTP(S) URL without user-info; forwarder requires a nonblank destination. |
template | Template | Required title: String and body: String. |
attachment | AttachmentPolicy | "never", "when_available", or "required". |
allow_second_delivery | bool | Required flag for staged delivery. |
Template title and body limits are 256 and 4096 characters respectively; provider-specific limits
can be tighter. Allowed {{field}} substitutions are source.id, source.name, event.id,
event.kind, event.zone, event.confidence, event.duration, health.state,
notification.stage, and notification.deep_link. These are notification template substitutions,
not secret references.
Provider destinations can contain credentials, so the rule writer moves non-browser destination strings into the companion secret file and stores references. It keeps active and draft secret versions separate. Do not inline tokens into a book example or export them with a layout.
The legacy notifications.db is a rule-migration input. Delivery jobs, retry state, evaluation
history, and counters are runtime state, not extra [notifications] settings. See
notifications and integrations for provider setup.
Pending storage migration
Type: StorageMigration, section [storage_migration]. The server creates this journal for a
validated move and consumes it during migration. Do not use it as an alternative storage editor.
| Field | Type | Meaning |
|---|---|---|
medium_term | Optional StoragePathMigration | Medium-term recording move. |
long_term | Optional StoragePathMigration | Long-term recording move. |
recording_catalog | Optional StoragePathMigration | Explicit catalog move when not covered by a recording-root move. |
event_thumbnails | Optional StoragePathMigration | Explicit thumbnail move when not covered by a recording-root move. |
recording_catalog_after_move | Optional PathBuf | Catalog whose stored recording paths must be rewritten after moving roots. |
Each StoragePathMigration has two required PathBuf fields: from and to. Paths must be
nonempty, must not contain one another, and must satisfy the migration overlap checks. A single
source cannot be split into conflicting destinations. StorageMigrationPaths is a runtime helper
of borrowed paths, not another TOML section.
Compatibility and completeness
- Preserve server-managed sections when editing ordinary settings. Serializing only
Configwould omit the separate credential, layout, template, and notification records. - The legacy access, layout, template, and notification files are migration inputs only. Do not create new instances of them for new settings.
- There is no generic
[users],[permissions],[groups], or persistent runtime-profile schema in this configuration. Do not invent those sections or assume a grid’s audience restricts camera access elsewhere. - Future layout and permission settings must extend the existing application configuration and its validation, atomic writes, and recovery coverage. Secret values remain in the existing companion secret store.
- When a serialized struct, section name, default, limit, enum, or migration changes, update this reference in the same change and verify both load and write paths with synthetic fixtures.
For an upgrade from separate legacy stores, follow What startup migrates. For retention of runtime and recording data, see Backup and restore.
Event recording pre-roll
event-only stores only accepted event windows from the selected main or sub stream.
event-boost keeps continuous sub recording and uses main quality during event
windows. A nonzero event_pre_recording_duration_secs enables bounded encoded
history for these modes. EventBoost then delays persistence by up to the selected
duration so an event can replace pending sub coverage with main coverage. A crash
can lose this uncommitted window. With zero pre-roll, existing recording modes
keep their immediate admission behavior.
Changes persist in config.toml through the existing atomic writer. Camera
changes use the existing source restart path when available; otherwise the
response reports that a restart is required. Memory-budget changes require an
application restart. Omitted updates retain existing values; cleared camera
values inherit camera defaults. These settings contain no credentials. Existing supported secret references
in camera credentials and URLs remain preserved. Whole-GOP eviction can shorten coverage under duration or memory
pressure; health diagnostics report available coverage and the reason. Privacy,
stream discontinuities and storage failures invalidate optional history.
Export and import configuration
Use a configuration ZIP to back up your settings, return to an earlier configuration, or transfer settings to another KeepPeek recorder. KeepPeek calls the import operation Apply configuration. You download one ZIP, upload that same ZIP to the target recorder, and restart to activate it.
This is a configuration backup, not a backup of your recordings or database.
Before you start
- Connect to the source or target recorder as an Administrator. See Authentication and access control for access setup.
- Export the target recorder’s current configuration before importing a different one. This gives you a ZIP you can apply later if you want to return to those settings.
- Plan for a recorder restart when importing. Export does not require a restart or stop recording.
- If you use remote access, make sure you can authenticate with the imported access configuration after restart. Its access credentials and camera/integration secrets replace the target’s values.
What the ZIP contains
Every export contains exactly two files at the root of the archive:
| File | Contents |
|---|---|
config.toml | Durable settings, including cameras, storage policy, access credential metadata, layouts, templates, notification rules, and MQTT settings. |
secrets.toml | The file-backed secret values used by the configuration. |
The ZIP contains plaintext secrets and is not encrypted. Restrict access to it as you would to camera passwords or access keys. Do not attach it to a public issue or share it in logs, and delete copies you no longer need. Values supplied only through environment variables are not exported; configure those separately on the target recorder.
The ZIP excludes recordings.db, recording media, thumbnails, sessions, caches, and notification or
MQTT runtime work. Import keeps the target’s recording, catalog, and thumbnail paths. It neither
restores nor moves the source recorder’s database or media. Other storage settings, such as retention
policy, are part of the imported configuration and should be reviewed afterward.
Keep the ZIP as exported. The current format stores its versioned manifest and checksums in the ZIP comment, so there is no third file. Do not edit the TOMLs inside it, remove the comment, or repack the archive before importing. Apply accepts current format-3 exports, not arbitrary two-file ZIPs or older backup formats.
Export in Settings
- Open Settings, then Backup and restore on the source recorder.
- Choose Export ZIP.
- Keep the downloaded ZIP in a location with restricted access.
The download is the backup artifact. Export does not delete or replace either live TOML file, and KeepPeek does not retain a server-side copy or add it to a backup list.
Import in Settings
- Open Settings, then Backup and restore on the target recorder.
- Under Configuration ZIP, select the original exported ZIP. Do not extract it first.
- Select Replace config.toml and secrets.toml on restart.
- Choose Apply configuration and wait for the staged confirmation.
- Choose Restart to apply when you are ready for the recorder to restart.
Apply validates the archive, both TOML documents, target permissions, and available space before staging. A successful upload leaves the running configuration and live TOML files unchanged. The replacement takes effect during startup, before the configuration and database owners open.
Only one apply can be pending. Finish its restart and health verification before uploading another configuration. To return to earlier settings, use the same import steps with the ZIP you exported before the change.
Verify after restart
- Reconnect to the target recorder, using the imported access configuration if it changed.
- Confirm the expected camera definitions, notification rules, integrations, and storage policy.
- Check camera and stream health and confirm that the target’s existing recordings are available.
- Check the logs if startup or an integration fails. Environment-only secrets and external services must still be available on the target.
KeepPeek uses before-images and a crash-recovery journal to restore the prior configuration pair if configuration loading or startup health fails. Do not manually modify its journal or staging files. See Backup and restore for recovery details.
Use the CLI
The CLI performs the same export and apply operations as Settings. These examples connect to a recorder on the local machine:
keeppeek config --server http://localhost:8081 export --output keeppeek-config.zip
keeppeek config --server http://localhost:8081 apply keeppeek-config.zip --confirm
Export requires an output path and creates a new, owner-only file; choose a different filename if it
already exists. Apply requires --confirm. Commands write machine-readable JSON to standard output
and diagnostics to standard error.
Keep --server explicit: a new recorder defaults to 8081, while this CLI subcommand currently
defaults to http://localhost:3000 when the option is omitted. Use the recorder’s actual address.
On Windows, export protects
the new file for the current user, SYSTEM, and Administrators and verifies those permissions before
writing any ZIP bytes. On Unix, it creates the file with mode 0600. A destination whose required
protection cannot be verified is rejected; choose a private filesystem destination. Browser
downloads and direct HTTP clients use their own destination permissions.
Check the command’s exit status and returned state. A successful apply reports
RESTORE_STATE_AWAITING_RESTART; restart the service through your normal service manager, or use
Restart to apply in Settings. The CLI does not restart the recorder automatically.
For a remote recorder, use HTTPS and provide its Administrator credential through
KEEPPEEK_ACCESS_KEY using your normal secure environment setup. Do not put the key in a URL or CLI
argument.
Use HTTP
| Request | Result |
|---|---|
GET /config/export | A fresh application/zip download. |
POST /config/apply | Validate and stage a raw ZIP body; return 202 and RESTORE_STATE_AWAITING_RESTART. |
For a local recorder, a POSIX shell example is:
umask 077
curl --fail http://localhost:8081/config/export --output keeppeek-config.zip
curl --fail -H 'Content-Type: application/zip' \
--data-binary @keeppeek-config.zip http://localhost:8081/config/apply
Send the ZIP as the request body, not as a multipart form. Apply requires Content-Length; the curl
command above supplies it for the file upload. Both endpoints require Administrator access and use
Cache-Control: no-store.
HTTP 202 means staged, not active. Restart the service only after checking the successful apply
response. There is no separate public dry-run, section selection, or path-mapping step, and the old
/api/backups endpoints are not supported.
If an import fails
| Result | What to do |
|---|---|
Invalid or unsupported ZIP (400) | Use an unmodified current KeepPeek export. A hand-made ZIP, missing comment, changed checksum, or invalid TOML is rejected. |
Authentication or permission denied (401 / 403) | Connect with Administrator access to the target recorder. |
Apply already pending (409) | Complete the staged apply and restart before submitting another. Do not delete its journal to bypass this check. |
Missing length or wrong media type (411 / 415) | Send a raw ZIP with Content-Length and Content-Type: application/zip. |
Upload too large (413) | Verify that you selected the configuration export, not a media archive. Each TOML is limited to 16 MiB; compressed and expanded ZIP totals are each limited to 1 GiB. |
Archive operation busy (503) | Let the active export or apply finish, then retry. |
A rejected upload does not replace the live configuration. In Settings, the selected filename remains available after a validation error so you can correct the selection and retry. Do not restart in response to a failed upload as though it had been accepted.
If startup fails after a successful apply, preserve its logs and allow the documented automatic two-file recovery to run. If both activation and rollback fail, preserve the full configuration directory, including its journal and staged files, before manual recovery. Do not clear a pending apply by deleting the journal. See Safe activation.
For a recorder upgrade or disk failure, also use Upgrades and migrations and Recording archive recovery. Applying settings cannot restore missing recording bytes.
Backup and restore
KeepPeek can create and validate a configuration bundle while recording continues. A format-3 ZIP
contains exactly config.toml and plaintext secrets.toml; it contains no database or media.
For a step-by-step walkthrough, see Export and import configuration.
Open Settings → Backup and restore as an Administrator. Export ZIP downloads the two files directly. Select a Configuration ZIP, confirm replacement, and choose Apply configuration. After the upload succeeds, choose Restart to apply.
The HTTP surface is GET /config/export for download and POST /config/apply for a ZIP upload.
There are no retained server backups, upload reservations, or separate dry-run requests. The old
/api/backups routes are retired.
Secrets and media
The ZIP includes the complete file-backed secrets.toml in plaintext. It may contain camera
passwords, access keys, MQTT credentials, webhook URLs, and notification-provider destinations.
Restrict the ZIP like the live secrets file and delete copies that are no longer needed. Environment
secret overrides are not copied into the bundle.
The catalog, MP4 recordings, thumbnail JPEGs, sessions, caches, audit activity, and in-memory notification/MQTT work are excluded. The target recorder retains its named volumes, placement, and metadata authority during restore. Private root references stay in configuration; the uploaded secrets must resolve them to the same target owners. Conflicting or missing references reject the apply before mutation. Finish or cancel a pending metadata handoff before importing configuration. Protect all owned volume roots with a separate archive policy when they need recovery.
What survives a restart
| State | Persistence and recovery boundary |
|---|---|
| Camera settings, access credentials and grants, dashboards and selections, templates, notification rules, MQTT settings | Durable configuration in config.toml; included in a configuration ZIP. |
| File-backed secrets | Durable sibling secrets.toml; included in plaintext. Environment overrides remain outside the ZIP. |
| Recording metadata, events, coverage and maintenance intent | Recording catalog data; recover with a consistent catalog and media archive. |
| MP4 recordings and event images | Files in configured storage locations; not part of a configuration ZIP. |
| Evidence-export history and artifacts | History belongs to the metadata volume; artifacts belong to export volumes, with separate expiry; not part of the ZIP. |
| Diagnostic log filter | The existing sibling log-filter file persists separately and is not in the two-TOML ZIP. |
| Notification delivery jobs, retries, inbox/history, cooldowns and delivery counters | Runtime memory; reset on restart. Saved rule drafts and active rules remain. |
| MQTT pending publications, retries and deduplication history | Runtime memory; reset on restart. Stored source events are not an automatic replay queue. |
| Access audit, credential last-use activity and active sessions | Runtime memory; reset on restart. Durable identities and grants remain. |
| Browser sign-in key, media handles and active wake lock | Page/browser runtime state; reload or reconnect as required. Saved dashboard wake-lock intent remains configuration. |
Restart is therefore a recording interruption and a runtime-state reset, even when every saved setting returns correctly. Collect required audit and delivery evidence before restart. A queue that was pending before shutdown is not proof of later provider delivery.
See Recording archive recovery for consistent offline copies and recovery rehearsals, and Upgrades and migrations for version changes.
Safe activation
Apply accepts a current format-3 ZIP, including its manifest comment, and validates paths, checksums,
both TOMLs, permissions, and capacity before staging. It returns 202 with
RESTORE_STATE_AWAITING_RESTART; this is not an immediate replacement of the live files.
Staging writes owner-only copies beside both target files. Recovery runs before configuration or databases open, and completion requires successful configuration, HTTP, and camera-worker startup. KeepPeek restores the prior pair automatically if startup health fails. A pending apply blocks a second upload from replacing it; a healthy completed restore can be superseded by the next apply. To return to an older configuration, apply a ZIP exported before the change.
Do not edit .backups/restore-journal.json or its staging files manually. The detailed HTTP, limit,
crash-recovery, and CLI contract is in the
backup and restore engineering guide.
Automation
The keeppeek config commands call the same Administrator-only HTTP endpoints. Commands print
machine-readable JSON to stdout. Supply remote authentication only through
KEEPPEEK_ACCESS_KEY; never put it in a URL or command argument.
CLI exports create a new private file before writing archive bytes. On Unix, the file mode is
0600. On Windows, a protected access list permits the current user, SYSTEM, and Administrators;
the CLI verifies that protection before writing. A destination that cannot provide this protection
is rejected. Existing files and Windows alternate data streams are not export destinations.
Browser downloads use the browser’s destination permissions; restrict those copies separately.
keeppeek config --server http://localhost:8081 export --output keeppeek-config.zip
keeppeek config --server http://localhost:8081 apply keeppeek-config.zip --confirm
Use your recorder’s configured port if it differs from the default 8081. Export does not overwrite
an existing file. Apply does not restart the recorder automatically; check its exit status and
RESTORE_STATE_AWAITING_RESTART result before restarting. Exit code 2 means invalid CLI usage,
3 means a stable server 4xx rejection, and 4 means a transport, protocol, or server failure.
Keep --server explicit in automation: the config CLI currently uses port 3000 if it is omitted,
which differs from a new recorder’s default listener port.
Upgrades and migrations
Upgrade the application separately from moving storage or importing configuration. Each operation has a different recovery boundary. KeepPeek is in Alpha qualification; keep a tested recovery copy and rehearse upgrades on an isolated installation before changing a recorder you depend on.
Prepare an upgrade
- Record the running version, installation method, service account, listener address, and effective configuration, catalog, recording, and thumbnail locations. A service account can use a different configuration directory from your interactive login.
- Read the target release notes and known limitations. Retain the exact previous package or container image so you can identify what was running.
- Export a configuration ZIP. Keep it private: it contains plaintext file-backed secrets. Record any environment-only secrets and external dependencies in your existing secure operations system; the ZIP does not include them.
- Arrange a consistent recording archive recovery copy, including the catalog and media. A ZIP alone cannot restore these after a failed disk or catalog migration.
- Complete any already-staged configuration apply or storage move before starting a separate upgrade. Finish or cancel active exports and maintenance jobs where possible. Preserve reports for any unresolved maintenance work. Record the expected recording interruption and notify the people who rely on the installation through your normal process.
- Stop the recorder through its service manager before replacing application files or taking an offline archive copy. Confirm it is stopped; do not run two versions against the same directories.
Do not upgrade by replacing config.toml with a sample. Server-managed credentials, camera grants,
layouts, templates, and notification rules live in that file alongside ordinary settings.
Replace the application
Docker on Linux
Keep the existing /config mount and any additional recording mounts. Pull the selected release
image, stop the old container, and create its replacement with the same mounts, ports, user access,
and environment. Record the image digest if you need to reproduce the installation; latest can
refer to a different build later.
Container replacement does not preserve data that was left only in the container’s writable layer.
Do not delete a named volume or host recording directory during an application upgrade. The image
runs as user 65532; the replacement must still be able to write its mounted directories.
See Get started for the basic mount contract.
macOS
Download the Apple Silicon DMG and its matching checksum file. Verify the checksum, replace
/Applications/KeepPeek.app, then open the new application once. The launcher reinstalls and starts
the per-user com.keeppeek launch agent. It starts after that user logs in; it is not a system-wide
boot daemon.
Check the loaded service and logs:
launchctl print gui/"$(id -u)"/com.keeppeek
tail -n 100 ~/Library/Logs/KeepPeek/keeppeek.log ~/Library/Logs/KeepPeek/keeppeek-error.log
Keep the existing configuration directory. The launcher writes its launch-agent definition again, so preserve and recheck any deliberate local service customizations. Package-signing and notarization details are in the macOS installation guide.
Windows
Run the installer for the host architecture. An existing KeepPeekService selects the service
component by default. The installer stops and removes that registration before replacing binaries,
then recreates and starts it when the service component is selected.
Record custom service identity, startup, recovery, and environment settings before upgrading; recreating the service is not a promise to preserve SCM customizations. Recheck them before relying on unattended operation. Configuration paths resolve in the process account’s environment.
Use an elevated prompt for service operations:
sc.exe query KeepPeekService
sc.exe qc KeepPeekService
For a planned manual stop and start, use sc.exe stop KeepPeekService and
sc.exe start KeepPeekService, checking sc.exe query KeepPeekService between operations. A stop
request can return before the service has finished stopping. The installer manages these steps
when performing its own service upgrade.
If the installer cannot stop the old service, resolve that failure before retrying the installer.
If startup fails, inspect the service account’s configuration and logs. The default destination is
keeppeek-service.log beside its configuration; [logging] service = "event_log" uses the Windows
Application log after the source has been registered. See the
Windows service guide.
Standalone binary
Stop the current process, replace the executable with the intended build, and start it with the same
configuration selection and environment. The --config <path> option selects the TOML file and its
sibling secrets.toml. A fresh configuration initializes named storage beside that file;
an existing metadata binding keeps its recorded owner.
What startup migrates
Current KeepPeek loads durable settings from config.toml. Older installations may still have
separate subsystem files. Startup imports their supported settings when the corresponding current
section is absent, writes the current configuration, and removes the retired file after successful
migration. Preserve an offline pre-upgrade copy if you need the original inputs.
| Legacy input | Current location or behavior |
|---|---|
access.toml | Credential identities, verifiers, roles, revisions, and grants move to [access_credentials]. |
peek-layouts.json | Layouts and per-user selection move to [peek_layouts]. Legacy private layouts become server-owned dashboards with restricted audiences. |
configuration-templates.json | Camera templates move to [configuration_templates]. |
notifications.db and its database sidecars | Rule drafts and active rules move to [notifications]; delivery history and pending runtime work are not a replay queue. |
mqtt-forwarder.db and its database sidecars | Retired and removed. Current MQTT pending work and deduplication are in memory and are not replayed from this file. |
When a current configuration section already exists, it is authoritative; do not expect an old file to merge additional settings into it. Do not recreate retired files to change settings.
The recording catalog also performs schema and index migrations as it opens. There is no supported general-purpose downgrade command or guarantee that an older binary can read a catalog after a newer binary has used it. Keep the pre-upgrade catalog and media together if a downgrade might be needed.
See the configuration reference for current fields and Backup and restore for durable versus runtime state.
Move storage deliberately
Use Settings → Named storage volumes. Create a private destination directory on a supported local filesystem, add its stable ID, roles, limits, and placement rules, and save the draft. Restart to apply configuration changes. New placement rules affect future objects; existing objects retain their catalog owner.
To drain media, choose Stop new writes for the source. Existing fragments finish there. Load its stored objects, select a compatible destination, and preview an individual move or a batch of up to 16 objects. Review the file count and bytes before confirming. Move jobs show durable progress and offer cancellation before authoritative publication. Stopping a batch stops new confirmations; already admitted jobs remain visible. An expired or changed preview requires another review.
Moves copy into a confined temporary file, verify length and SHA-256, publish the verified catalog location, then retire the old copy after readers finish. A lost confirmation reply is checked using the original job ID. Interrupted work resumes from its durable journal. Removal requires no owned objects, active allocations, unfinished jobs, or cleanup receipts; removing a definition does not delete unrelated files or its directory.
Metadata uses Catalog and export history controls. Preview a disabled metadata-only destination, review downtime and capacity, then confirm the restart handoff. The old catalog and history remain available for recovery, and the new authority is verified before media workers start. A missing bound root is offline and is never recreated. Preserve both copies and the pending configuration if activation fails; do not edit journals, overwrite a conflicting destination, or clear fencing records to force a retry. See the configuration reference.
Verify and recover
After startup, check the version, expected camera count, credentials and grants, dashboards, storage locations, and provider settings. Test live playback, a finalized new recording, and an older known recording. Check recording integrity and any pending maintenance or export history. Reconnect remote clients; sessions and their browser keys are not restored across a reload or restart.
An accepted configuration ZIP has its own automatic two-file startup rollback. That mechanism does not roll back an application binary, catalog schema, or storage move. If an application upgrade fails, stop the recorder and preserve its failed state before recovery. Restore an isolated copy of the previous complete installation and verify it before replacing the failed deployment. Do not run the older binary against the only copy of a possibly migrated catalog.
Record the result, interruption, recovered data, and any remaining gap in the installation’s qualification record.
Recording maintenance
Recording maintenance provides Administrator previews, confirmed deletion jobs, and bounded catalog reconciliation over the WebRTC control connection. Implementation issue #133 closed on 2026-09-08. The synthetic-media workflow and native Windows, macOS, and Linux checks have passed. Deployment-specific recovery and the wider release decision remain part of the Alpha gate #145.
Automatic storage retention remains separate from manual deletion. Manual claims exclude their recordings from retention and new playback/export resolution until the claim reaches a terminal outcome. Never delete recording files directly as a substitute for maintenance: that can leave stale catalog entries and unavailable evidence links.
Named-volume deletion follows each recording’s current catalog owner, including recordings moved to another named volume. It requires the original bound root to be available and writable. Active readers and unfinished moves block removal. Failed or interrupted jobs retain capacity ownership until the durable deletion checkpoint completes. Restore the same root or finish the conflicting work before retrying; do not clear ownership rows or private staging to force progress.
Startup preserves .active files and their catalog rows. A temporary-looking
filename does not establish ownership or authorize deletion, in either the
medium-term or long-term root. Interrupted media remains available for inspection.
Writers create new .active files exclusively. If the output path already exists,
creation fails without truncating or replacing the existing file.
Review and delete recordings
- Sign in as an Administrator and open Recording integrity, then Maintenance. Review deletion in Keep’s export-range view carries its camera, stream, and interval into maintenance. Closing Keep also releases its playback cursor.
- Select the camera, stream, UTC start and end, and operator or privacy reason. Choose Preview deletion. A day or hour is an ordinary UTC time range.
- Review every whole-recording boundary, byte count, gap, bookmark, related export, and stated consequence. An active, protected, retention-pending, or identity-unavailable selection cannot be confirmed.
- Type the exact confirmation text, such as
DELETE 1, and choose Delete permanently. Keep recordings closes the confirmation without starting deletion; Cancel job cancels the prepared intent. - Follow the per-object results. Use Refresh job after a connection failure and Download report to retain the result. Reports contain no confirmation nonce or host filesystem path.
Before deleting irreplaceable footage, retain the required evidence export or archive recovery copy and verify it separately. A configuration ZIP contains neither the media nor its catalog. A bookmark does not protect media from deletion; check the preview’s explicit protection and relationship evidence.
One deletion worker runs at a time. Close stored playback and wait for active exports before confirming. The server rechecks authorization during execution; revoking the session can leave a recoverable staged object rather than granting an old request unlimited permission to continue. A failed confirmation consumes its private nonce. Choose Refresh preview, review the new result, and type the confirmation again. The textbox is disabled while the refresh is pending. Losing the Administrator role or maintenance capability discards the review, including late preview responses. Already-removed bytes cannot be restored by cancellation.
Select a complete scope
Maintenance uses stable source, stream, and recording identifiers, never a display name or a caller-supplied filesystem path. Catalog inspection accepts one recording or a nonempty half-open time range on one source and logical stream. The start is included and the end is excluded.
A range selects whole recording objects that overlap it. It does not trim media at the requested timestamps. The snapshot retains each object’s full start and end bounds so an eventual destructive preview can show any expansion explicitly. The confirmation uses those whole-object bounds; it does not rewrite partial boundary fragments. Inspect the listed boundaries even when the requested interval is much shorter than the selected recording.
Selections contain at most 128 recording objects. Time ranges span at most 31 days, and indexed history inspection is bounded to 4,096 rows. If a complete selection cannot be established within those limits, the request fails instead of returning a partial result that could be mistaken for the full scope.
Counts and bytes in a catalog snapshot are recorded metadata. They do not prove that a file is intact, that its media is playable, or that the selected interval has no gaps. Active, protected, and cleanup-pending objects remain visible rather than disappearing from the selection.
Understand durable intent
Preparation reads an authoritative snapshot and checks its catalog revision. It rejects empty selections, active or unfinalized recordings, protected recordings, pending automatic cleanup, and recordings without a known end time. The stored intent records the requester, scope, operator or privacy reason, and timestamps.
The snapshot also retains an opaque fingerprint of each recording’s catalog device/file identifier. Confirmation and restart preserve this preparation-time binding; a later catalog refresh cannot substitute another file into the plan. Missing preparation evidence stays unavailable even if the current catalog later gains an identifier. The fingerprint does not verify media contents or prevent filesystem identifier reuse.
Confirmation requires the same requester, a server-generated nonce, and the unchanged preview revision. The catalog stores a hash of the nonce rather than the nonce itself. Prepared plans expire after ten minutes using both wall-clock and monotonic time; restarting the catalog requires a new preparation.
| State | Meaning |
|---|---|
| Prepared | A short-lived intent is awaiting confirmation; no recording is reserved. |
| Expired | Preparation must be repeated before confirmation. |
| Queued | Confirmation and a per-recording ledger are durable. Execution may reserve the selection. |
| Working | A live execution attempt owns the object. |
| Staged | The selected file has moved into private maintenance storage, with a durable staging checkpoint. |
| Deleted | File removal and catalog completion have been recorded. |
| Failed | Removal did not finish; retained evidence and a retry or inspection are required. |
| Cancelled | Admission of unstarted objects has stopped. Objects already deleted stay deleted. |
Confirmation creates exactly one ledger entry per selected recording in snapshot order, in the same transaction as the queued state. Retrying the same confirmation after a lost reply or restart returns that job rather than duplicating work. Queued and cancelled intents survive restart. Cancellation is idempotent and cannot assign a time before creation or confirmation.
The catalog admits at most 128 prepared plans and 256 confirmed or retained cancelled jobs. Expired preparation and terminal history older than 30 days can be pruned; unresolved execution is not evicted to make room. Cancelling before confirmation does not create work entries.
Reservations and per-object checkpoints survive restart. An authenticated retry can resume an abandoned attempt; it cannot run concurrently with a still-live worker. A file that disappeared before any staging evidence is not reported as deleted. The staging checkpoint binds recovery to the original token directory’s filesystem identity. A missing or replacement directory, renamed staged recording, or unexpected entry requires inspection instead of a successful result. Once unlink has completed, recovery can still finish if the empty original source directory has been removed.
Cancelling interrupted work does not release a claim merely because staging is absent. The original selected file must be positively identified; otherwise the object remains failed and reserved for inspection or recovery. Retry failed objects is available for those failures even when the job is cancelled. After restart, a previous worker’s pending objects report failed instead of indefinitely working; the checkpoint and reservations remain intact for authenticated retry. Before recording pipelines start, a bounded recovery pass settles catalog completion only when the original staging-directory checkpoint and absence checks prove that unlink has already completed. It does not move or delete present media. Other pending objects remain failed and reserved for an Administrator to inspect or retry. Each settled outcome is atomic, respects the original request deadline, and leaves any live executor untouched. If the pass cannot finish within its two-second budget, the server records a warning and retains unresolved claims.
Audit and coordination
Maintenance admission shares the configuration-update lock used by restore and access configuration. An export retry cannot remove its existing job while maintenance is admitted. The worker retains storage coordination during execution and rechecks the current session before filesystem mutations.
The access audit links each execution outcome to its durable job ID. Structured
recording_maintenance log events include requester, source and stream, requested
and expanded interval bounds, operator or privacy reason, preview revision,
object and byte counts, relationship counts, and the observed result. Hold
override is always false: this workflow cannot release a hold. Results distinguish
successful deletion, partial failure, incomplete work, completed cancellation,
and cancellation with pending work. Paths, confirmation nonces, and raw failure
details are excluded.
Reconcile the catalog
Open the Reconciliation view and choose Inspect catalog. The dry run reads at most 4,096 catalog rows and 4,096 archive entries and returns at most 128 findings. Scan limits are explicit: an incomplete report cannot authorize a remedy. Reports expire after ten minutes and belong to the requesting Administrator. At most sixteen reports are retained in server memory.
The inventory reports missing and unknown files, duplicate paths or catalog file identities, size or identity mismatch, rejected paths, temporary files, interrupted work, corrupt containers, and playback-index mismatch. Protected recordings are not mislabeled as interrupted jobs. Active recordings are not parsed while their writer can still change them.
Finalized cataloged files use bounded fragmented-MP4 validation: box boundaries, decode times, track identities, sample extents, and decoder metadata are checked. The parser accepts at most 4,096 fragments, 8 MiB of metadata and two tracks; each track fragment has at most 65,536 samples. This is not a full media decode or a stored-content checksum comparison. Unknown files are never automatically adopted, moved, or deleted.
- Acknowledge records an explicit ignore decision in the retained report and leaves the file and catalog untouched.
- Retain tombstone is available only for a missing, unprotected cataloged file. It rechecks absence and catalog revision, then removes the stale row and playback indexes while retaining existing coverage-derived deletion evidence. No filesystem bytes are deleted. A reappeared file or changed catalog rejects the operation; inspect again before making another change.
- Rebuild index is available only for an existing unprotected catalog recording whose container validates and whose playback index differs. After confirmation, it rechecks the catalog revision, exact index fingerprint and retained file observation. It replaces fragment/keyframe indexes and derived coverage in one transaction. The media bytes are unchanged. Unknown files cannot use this action.
Category filters and Download reconciliation report preserve inspection results. A new scan is required after a catalog-changing remedy. Mutating remedies share storage coordination with deletion and restore. Quarantine, content-checksum comparison, and previewed deletion of owned temporary files are not implemented yet.
Read a preflight report
Preflight accepts an actor-owned queued job. Its caller must authenticate and authorize the Administrator and supply the configured archive. These internal storage methods are not themselves a network authorization boundary.
The job, ledger, current recording rows, and observed catalog revision share one read transaction on the catalog search worker. Filesystem checks run only after that transaction closes. Reports preserve snapshot order and do not expose host paths or confirmation secrets.
| Status | Meaning |
|---|---|
Present | Planned and current device/file fingerprints match the observed file, and its size matches. Content is not verified. |
IdentityChanged | The preparation-time fingerprint, current catalog identifier, and observed file do not all agree. |
IdentityUnavailable | The preparation or current catalog lacks identity evidence; path and size cannot establish a match. |
MissingFile | The catalog-resolved file could not be found during inspection. |
MissingCatalog | The recording row is absent. Its file location and whether bytes remain are unknown. |
CatalogChanged | Source, logical stream, recording bounds, or recorded size no longer match the plan. |
ActiveRecording | The recording is no longer finalized. |
ProtectedRecording | Current catalog protection blocks maintenance. |
CleanupPending | Automatic retention already has pending work for the recording. |
SizeMismatch | The observed file size differs from the catalog byte count. |
PathRejected | The path is not eligible for inspection, including escapes, hidden paths, or links. |
InspectionFailed | Another filesystem error prevented a usable observation. |
Malformed identity evidence or an inconsistent ledger rejects the report rather than reconstructing work from current files. Known catalog blockers are reported before inspecting the affected path. Both the planned and observed catalog revisions are returned; a changed revision requires another preview.
Malformed persisted fingerprints also reject intent reads and confirmation retries with a redacted error. Rejected values and internal paths are not included in public diagnostic messages.
The entire request shares one two-second deadline across queueing, catalog reads, and at most 128 filesystem checks. Paths are limited to 4,096 bytes and sixteen components. The deadline is cooperative: it cannot interrupt an operating-system or database call already in progress. A timeout returns no partial report.
Know what file inspection proves
Inspection pins the configured archive directory and opens each descendant
directory without following links. It rejects hidden directories such as
.exports, traversal, alternate-stream syntax, leaf symlinks, nonregular files,
and files with multiple hard links. Checks before opening do not replace the
checks on the opened file.
Each observation retains a non-writable file handle until it is dropped. Unix uses nonblocking opens to avoid waiting for a writer if a regular file is swapped for a FIFO. Unix requires read permission; Windows requests file attributes. Inspection does not read media bytes or request write, create, or truncate access. Preflight releases each file observation as it advances to the next object.
A retained handle pins the observed object, not its pathname. Another handle can still change the file’s contents. Device/file identifiers can be reused outside the lifetime of an open handle, and the available 64-bit Windows file index is not a complete ReFS identity. These checks do not certify immutable recording ownership or qualify Windows reparse-point races without runtime verification.
Even an all-Present report with a matching revision is not permission to delete.
Filesystem observations are not atomic with the catalog read or with one another.
Revalidating a file and then unlinking its pathname still leaves a replacement
race. Missing catalog metadata is never evidence that an unknown file is safe to
remove.
The removal path uses separate private staging directories, revalidates the staged object, and synchronizes directory changes before recording completion. Unix directories must be owned by the process user and deny untrusted mutation; private staging denies group and other access. macOS and Linux use atomic no-replace rename, so a destination introduced before the move is not overwritten. Unix also checks the retained selected inode after unlink and rejects the attempt if that inode is still linked.
These checks do not isolate maintenance from another process running as the same operating-system account or root. Such a process can replace private staging names immediately before Unix pathname-based unlink. Post-unlink checks can detect some substitutions, but cannot prevent the replacement from being removed or establish where a relocated recording went. Do not treat retries after external staging manipulation as qualified recovery. A trusted service-account boundary or stronger process isolation must be resolved before this feature is qualified.
The native Windows implementation targets NTFS with persistent ACLs, checked
directory flushes, and handle-based rename/disposition. Other filesystems,
including ReFS, are rejected before source-file mutation. ReFS requires a full
128-bit file identity and a separately justified namespace-durability strategy;
a successful directory-flush call alone is not sufficient. The
PR CI run and
extended Main run
exercise the implementation, including Windows and synthetic deletion, on production
tree 4df9a94e (equivalent to 4bc3ab18). This establishes automated coverage of
the exercised fixtures, not every NTFS race, crash boundary, device, or deployment.
Qualification still required
For day-to-day operation, treat an unresolved reservation, missing staging identity, or incomplete reconciliation report as an inspection task. Preserve the report and the original files, close competing playback or exports when directed, and refresh the authoritative preview. Do not clear catalog rows or hidden staging entries manually to make a retry available. See Recording archive recovery for the difference between index repair, tombstones, and recovering missing bytes.
For a deployment and final release candidate, retain explicit evidence for:
- exact object ownership and adversarial replacement at every mutation boundary;
- Windows NTFS and Linux runtime tests, plus supported-filesystem failure behavior;
- holds, shares, operational investigations, moves, restores, and current access coordination, including audit completeness;
- partial failure, lost replies, cancellation, and crash recovery across all file/catalog/tombstone combinations;
- the remaining reconciliation categories and remedies;
- browser conflict regeneration, failed-job retry, access revocation, and truthful timeline results, beyond the existing desktop/mobile happy-path coverage;
- final-commit canonical checks and reproducible performance evidence.
Unknown files must never be adopted or deleted automatically. A ready export must not be removed as a side effect of deleting its source, and a bookmark is not an evidence hold. The implementation and acceptance criteria are tracked in issue #133.
Use Recording and evidence for recording policies, playable coverage, and evidence exports. See Backup and restore before changing storage, and Release readiness and known limitations for the qualification status of the wider system.
Recording archive recovery
A configuration ZIP restores settings and file-backed secrets. Recording recovery also needs the recording catalog and the files that it indexes. KeepPeek does not provide a public recording-archive importer, a button to merge another recorder’s database, or automatic adoption of unknown MP4 files.
Use this chapter to plan and rehearse an operator-managed filesystem recovery. It describes the current boundaries; it does not qualify every filesystem, backup product, or crash scenario.
Inventory the recovery set
Open Named storage volumes as an Administrator and record every configured root and its stable ID.
Fresh defaults live beside the selected config.toml; each role can have a separate destination.
Record the operating-system account that owns and runs the recorder.
| Item | Why it matters |
|---|---|
config.toml and sibling secrets.toml | Cameras, stable identities, policies, credentials, grants, dashboards, and integration settings. A configuration ZIP can preserve this pair. |
| The metadata-bound recording catalog and any accompanying database sidecars | Recording identities, owned locations, indexes, event data, coverage/deletion evidence, and maintenance intent. Copy the database family consistently. |
| The runtime state-store database file and any accompanying sidecars | Desired-state documents, leases, namespace and entry revisions. A separate consistent set; excluded from the configuration ZIP. Copy the family consistently. |
| Settings-backed state entries | Stored in the state_store section of config.toml; already covered by the configuration ZIP. Generic runtime entries are not. |
| Every active and archive volume root | The MP4 bytes referenced by the catalog. Preserve relative structure, file names, and interrupted work. |
| The optional fourth-tier local archive root | Verified replica bytes and private staging entries. Manifests and the target binding remain in the recording catalog; preserve both together. |
| Every thumbnail volume root | Event-image bytes and their catalog ownership. |
| Every export volume root and the metadata-bound export history file | Export artifacts and durable history. Artifacts still have independent expiry rules. |
| Service definitions, environment-only secrets, certificates, and external service configuration | Deployment dependencies outside the two TOML files. Preserve them through your existing protected operations system. |
The existing sibling log-filter and retained service logs | Diagnostic configuration and investigation evidence outside the configuration ZIP. |
Copy the entire selected recording roots, including hidden and interrupted-work entries, rather than
selecting only files ending in .mp4. Maintenance checkpoints can depend on private staging entries.
Copying them is preservation, not permission to retry destructive work after a move: filesystem
identities can change when data is copied.
Do not include only the default paths if Settings shows overrides. Do not treat an exported evidence clip as a full recording backup. Clips cover selected ranges and do not contain the recorder catalog.
Make a consistent offline copy
- Finish any pending configuration apply or storage move, then export the current configuration ZIP and save it privately. Finish or cancel active exports and maintenance jobs where possible; retain reports for unresolved work. If this is failure preservation instead, keep the unfinished journals with the copy and do not treat it as an ordinary clean recovery point.
- Stop KeepPeek through its normal service manager. Confirm the process has exited and no second recorder is using the same paths. Recording stops for this maintenance window.
- Copy the configuration directory and complete archive set while they remain stopped. Include any database sidecars that exist, for both the recording catalog and the runtime state store; do not manually delete them to make the copy simpler.
- Preserve ownership and access restrictions, record the source version and paths, and verify the copied files using your backup tool’s integrity checks. Keep the copy separately from the source disk.
- Start the source recorder again and verify that new recordings finalize normally.
A live copy of just the catalog database can disagree with its journal or recording files. KeepPeek’s configuration export lock does not create an atomic snapshot of the catalog, media, and export tree. If you use storage snapshots instead of a stopped copy, establish and test a consistent capture procedure for every involved volume and writer. This book does not claim that an arbitrary live filesystem copy is recoverable.
Restore the runtime state store before accepting watches
Restore the state-store database family together with its counters: namespace
revisions, entry revisions, and byte counts must return as one unit before the
server accepts new watches. Restoring entries without their counters would let
a fresh write reuse a revision that a previous writer already consumed, which
breaks compare-and-set ordering. After the restore, clients establish fresh
watches and reconcile the new snapshots against current capabilities; there is
no resume across a restore. A configuration ZIP alone does not restore generic
runtime state: it carries only the settings-backed entries stored in
config.toml.
Rehearse on an isolated host
- Keep the original backup unchanged. Restore a working copy into an isolated environment with enough space and compatible file permissions. Start with the same KeepPeek build that created it.
- Prevent the rehearsal from reaching real notification providers, MQTT consumers, and cameras until you deliberately test those connections. Imported credentials and rules are real settings; a duplicate recorder can otherwise act on the same devices or send duplicate notifications.
- Restore the catalog family and recording tree together at the original configured paths where practical. Restore the configuration pair and any required environment separately. Do not start with missing archive mounts and expect later attachment to be harmless.
- Confirm the service account can access every path, then start KeepPeek. Preserve startup errors and stop if a catalog, path, or migration failure occurs.
- Verify the expected cameras and access grants. Review a known older interval in Keep, inspect its coverage and event images, and play a finalized recording. Create a small evidence export and verify that its downloaded media decodes independently.
- Inspect recording integrity and reconciliation, including missing, unknown, interrupted, and protected recordings. Check old maintenance jobs before any retry. Record discrepancies rather than treating successful server startup as proof that every recording was recovered.
Catalog paths are meaningful. A same-path recovery avoids inventing a path rewrite. For a deliberate move on a working installation, use the validated storage migration. Do not manually rewrite catalog SQL or assume a configuration ZIP maps old recording paths: configuration apply keeps the target’s current recording, catalog, and thumbnail paths.
Once the isolated copy is verified, plan the production cutover and repeat the checks. Keep the old copy until the new recorder has completed the installation’s recovery acceptance test.
Understand startup recovery
Startup can reconcile specific interrupted states for recordings, automatic cleanup, export jobs, and maintenance. It is not an exhaustive media scan or a replacement for a consistent backup.
- An interrupted recording finalization can reconnect a catalog row to its finalized sibling.
Existing
.activefiles are preserved rather than truncated or deleted merely because of their extension. - Missing or unavailable named roots retain their catalog ownership. A replacement directory does not have the bound root identity. Restore the original root or use an explicit recovery procedure; creating an empty directory at its path does not make it the original volume.
- Export work that was running becomes failed and retryable. Missing ready artifacts also become failed. Partial export artifacts are cleaned within the owned export area.
- Event images retain their catalog references when the files are missing. Startup preserves unindexed images and temporary files; thumbnail quota cleanup only removes catalog-referenced images in the configured directory.
- Maintenance recovery only settles outcomes supported by its retained identity and checkpoint evidence. Other interrupted objects remain failed and reserved for Administrator inspection or retry. Copying files can change their identity, so a restored checkpoint may properly refuse work.
See Recording maintenance for exact mutation and retry boundaries, and Recording and evidence for coverage and export behavior.
Diagnose incomplete recovery
| Symptom | Next action |
|---|---|
| Settings returned, recordings did not | Confirm that a catalog and media archive were restored. The configuration ZIP excludes both. |
| Settings returned, runtime state did not | Confirm that the state-store database family was restored with its counters. Re-establish watches with a fresh snapshot afterwards. |
| Recordings appear missing after a path change | Check mount availability, service identity, effective paths, and the catalog’s original locations before changing files. |
| MP4 files exist but do not appear in Keep | Inspect reconciliation. Unknown files are not automatically adopted; Rebuild index only applies to eligible existing catalog recordings. |
| A cataloged recording has a valid container but a wrong index | Use the previewed Rebuild index remedy if offered. It changes indexes, not media bytes. |
| Reconciliation reports corruption | Preserve the original file and report. Structural validation is not a full decode or an end-to-end content checksum guarantee. |
| A copied maintenance job refuses retry | Preserve the job report and original checkpoint evidence. Do not delete private staging or manually clear reservations. |
| An old ready export is unavailable | Check expiry and artifact presence. Export metadata can outlive its bytes; retry requires source recordings still to exist. |
| Both configuration activation and rollback fail | Preserve the complete configuration directory, its restore journal and staged files, and service logs before manual recovery. |
Never delete unknown media, database sidecars, restore journals, or maintenance staging merely to clear an error. A missing file cannot be recovered by retaining a tombstone; that remedy records catalog cleanup, not restoration. Quarantine, arbitrary archive import, and automatic adoption of unknown files remain unsupported.
Record the recovery result
Keep the exact build, platform, filesystem, source and destination layout, backup time, interruption, commands, checksums, recovered interval, missing data, and observed errors with the recovery report. Include a known recording before and after the backup boundary and any interrupted work you tested. The Alpha qualification matrix still requires representative recovery and sustained-operation evidence. One successful rehearsal does not establish every deployment’s recovery behavior.
Native camera events
Native events are detections and status changes reported by the camera itself. KeepPeek consumes Reolink notifications, Hikvision/Annke ISAPI events, generic ONVIF PullPoint notifications and metadata carried by RTSP. These do not require an external inference service.
Supported observations enter the same event catalog as other events and can appear in Events, Keep’s history, notification rules and MQTT. Event processing is independent of media ingest: an event-service failure does not restart healthy video or audio. Compare its status with camera and stream health instead of treating them as one signal.
Enable and verify events
- Add the camera and verify its video and recording independently.
- In the camera’s own interface, enable the intended detection rule, schedule, region and event notification linkage. KeepPeek discovery does not configure these on the device.
- Use the default native event mode,
auto, unless there is a reason to select a specific path. - If the camera reports only unclassified motion, enable Store generic motion events in its KeepPeek settings. The default is off; explicit person or vehicle detections remain distinct.
- Trigger one known event, then open Events, choose that camera and the correct UTC date, and inspect its kind, source, timestamp and available image. Verify the corresponding recording in Keep before enabling an alert rule.
A detector-enabled switch is not an activity indicator. A camera reporting a motion rule with a human-only filter also does not prove that every notification contains a person classification. KeepPeek uses the explicit evidence in each notification.
Select the event transport
The optional [cameras.<name>.events] table belongs in the existing config.toml. These transport
fields are file-only; ordinary camera edits preserve them. Apply file changes through the existing
configuration/restart workflow. For example, to force generic ONVIF for an existing camera:
[cameras.front.events]
mode = "onvif-pullpoint"
metadata_stream = "auto"
snapshots = true
Replace front with the existing camera entry’s name. Do not add a second entry for the same
device merely to change its event path.
| Mode | Behavior |
|---|---|
auto | Prefer an eligible Reolink or ISAPI adapter; otherwise use generic ONVIF and permit RTSP metadata. Unsupported ISAPI HTTP 404/405 can hand over to generic ONVIF. |
vendor | Use the eligible vendor adapter without generic fallback. |
onvif-pullpoint | Use generic ONVIF independently of the video backend; metadata remains permitted unless disabled. |
rtsp-metadata | Consume metadata from the existing RTSP workers without contacting an ONVIF Event Service. |
disabled | Disable native events while leaving camera media independent. |
metadata_stream accepts auto, enabled, or disabled; it does not override vendor or
disabled mode. Authentication failure is reported rather than bypassed through another
authentication policy. The full field bounds and secret-reference rules are in the
Configuration reference.
Multi-channel devices need exact channel evidence. ISAPI main/sub URLs ending in 101/102 select
input 1; 201/202 select input 2. Both URLs must agree. Generic ONVIF source_tokens are opaque
camera-supplied identifiers, not profile indexes. Topic filters use expanded XML names, with
exclusions taking precedence. Use the detailed protocol guides when narrowing either field.
Understand event times and images
Repeated active observations update a logical event instead of creating one event for each notification. An explicit clear closes the matching observation. Generic ONVIF initialization establishes a baseline without notifying the user; an already-active baseline must clear before opening a new interval. Matching PullPoint and metadata evidence is deduplicated.
Timestamp rules depend on the source. Generic ONVIF uses valid camera UTC from the preceding five minutes; invalid, missing or future notification times use receipt time with an explanation in the event metadata. ISAPI’s recording timeline uses server receipt time and retains the original camera time as metadata. Correct the device clock when investigating inconsistent event timing.
ISAPI disconnects or a 30-second active-observation timeout close an observed span at its last evidence. That is not proof of the instant physical motion stopped. Unknown topics and heartbeats do not become invented motion events.
An event can be useful without an image. KeepPeek attaches explicitly associated snapshots and retains metadata when image capture is unavailable. Boxes are drawn only on their matching image; an unavailable canonical image is reported instead of replaced with unrelated footage.
Troubleshooting
| Symptom | What to check |
|---|---|
| Video works, events do not | Native event status, the device event service, credentials, rule schedule and channel selection. Video and event protocols are separate. |
| Only heartbeats arrive | Enable the desired camera rule and notification linkage. Check generic-motion retention for unclassified VMD. |
| Generic service is unsupported | Confirm the camera exposes ONVIF events or RTSP metadata; an ONVIF video profile alone is insufficient. |
| Repeated authentication failure | Correct the saved credentials and device permissions. Do not weaken authentication or repeatedly guess passwords. |
| Events have no thumbnail | Inspect the attachment state and snapshot support; metadata delivery can succeed independently. |
| Duplicate-looking cards | Compare source, event ID, rule, object and revision. Similar text can describe independent observations. |
For protocol details, see ONVIF events and Hikvision ISAPI. For model-generated detections, see External analysis. For delivery, see Notifications and integrations.
External analysis
An external analysis service consumes KeepPeek camera video, runs its own detector or model, and publishes events back through the public API. KeepPeek stores accepted event revisions and attachments and makes them available in Events, Keep, notifications and integrations.
KeepPeek does not install, host or select an inference product for you. Model execution, decoding, sampling, deployment and model licensing belong to the independent service. The repository’s Python object-detection example demonstrates the integration and supports CI; it is not a production detector product. Camera-native detections are covered separately in Native camera events.
Prepare the integration
- Verify that the camera is available in KeepPeek and that its configured stream can be decoded by the service. A low-resolution stream is usually sufficient for an initial interoperability check; choose it explicitly in the service.
- Obtain the camera’s stable source ID from its camera details. A display name, a current session ID or a Home Assistant entity ID is not a substitute.
- Provide a dedicated KeepPeek credential through the service’s supported secret mechanism. Event publication currently requires Administrator authority; a viewing-only User key cannot publish events. Apply the access policy appropriate to that service.
- Confirm the server advertises
keeppeek.event-publication.v1, the intended source, a supported media variant and the event types the service will publish. An unadvertised event type is not accepted merely because a model can produce it. - Start the service and verify one committed event before depending on its alerts or automation.
The browser has no general model-installation or external-service deployment wizard. Configuration for a particular service belongs to that client; do not invent extra KeepPeek TOML sections from the conceptual computer-vision scenario.
Try the reference example
The object-detection example
includes the complete platform-specific setup and commands. It requires Python 3.12 or newer,
FFmpeg on PATH, an active H.264 or H.265 source and a compatible KeepPeek server. Install its
requirements into the selected interpreter directly; the repository does not use Python virtual
environments. Generate its bindings from the checked-in protocol before running it.
Use KEEPPEEK_ACCESS_KEY_FILE with an owner-only key file or the supported environment variable
for authentication. Keep the key out of command arguments, endpoint URLs and committed files.
The example accepts non-secret settings including KEEPPEEK_URL, KEEPPEEK_SOURCE_ID, stream,
model, confidence, cooldown and inference-frame-rate choices.
The default example loads Ultralytics yolo11n.pt after authentication and media subscription
succeed. Missing weights may be downloaded by Ultralytics. Review that provider’s licensing and
deployment requirements independently. KeepPeek does not distribute the model weights.
Verify the result in KeepPeek
Open Events and select the source and UTC date. Inspect the event kind, confidence, text, revision and canonical image. A later model result may enrich or close the same event; it should not be interpreted as a new detection solely because its revision increased.
Use Open at this moment to compare the event with recorded video, and Back to event to return to the investigation. Review state and shared bookmarks work the same way for external and native events. A JPEG, story or description is an event attachment; it does not prove that continuous video was retained. Check Recording and evidence.
An attachment upload becomes visible only after the publication commits. A failed or unfinished upload must not appear as a complete event. Consumers use stable source/event IDs and revisions to recognize updates and retries. The service must use the API rather than writing KeepPeek’s catalog or attachment directories directly.
Failure and capacity checks
| Symptom | What to check |
|---|---|
| Subscription is rejected | Source visibility, advertised stream/codec and the service’s media capability checks. |
| Publication is denied | Administrator authority, enabled credential, current source identity and advertised event type. |
| Processing falls behind | Service decode/inference rate and bounded queues. Reduce sampling or use a suitable stream instead of accumulating old frames. |
| Events appear without images | Publication/attachment completion and image limits; inspect the event’s explicit availability state. |
| Recording continues while detections stop | Check the external process and its session. Inference failure does not imply recording failure. |
| Reconnection produces duplicates | Preserve logical event identity and increment revisions correctly; inspect commit/retry handling in the service. |
Source restarts change session identity. A service must rediscover the current source session and replace its media subscription, while preserving the stable source ID used for event history. The reference example’s checks demonstrate interoperability, not long-duration operation or certification of a third-party model deployment.
Developers should start with the public API and the computer-vision scenario. The latter explains the wider service architecture; check advertised capabilities and current examples before treating a described workload as an installed KeepPeek feature.
Notifications and integrations
KeepPeek evaluates notification policy on the server after an event or operational transition has been persisted. Camera workers never call a notification provider or MQTT broker directly. A slow or unavailable integration therefore cannot stop camera ingest, recording, event storage, health projection, or another delivery channel. See Camera and stream health for the evidence that creates operational transitions.
Notification controls are available only when the server advertises keeppeek.rules.v1.
Build and activate a rule
Open Settings > Notifications to create a rule. A rule can match:
- event creation, enrichment, and completion;
- camera offline, stale stream, unavailable decode, and interrupted recording transitions;
- storage or recording health changes;
- an explicit test send.
Filters can narrow a rule by camera or source, group, event kind, zone, confidence, image availability, duration, severity, review state, and bookmark state. Schedules use an IANA timezone and support active windows and quiet hours, including daylight-saving transitions.
Editing creates a draft. Activate validates and atomically promotes the complete draft; a conflict or invalid draft leaves the previous active revision unchanged. Use the test action before depending on a new destination. Test sends have separate rate accounting but still use the saved credentials and provider configuration.
Start with one camera, one event kind and one destination. Verify a real matching event in
Events after the test send succeeds: a provider test proves the delivery path, not that your
camera, schedule or rule filters will produce an alert. Rules and active revisions survive restart
in config.toml; delivery queues and inbox history do not.
Browser alerts, inbox and webhooks
Settings > Notifications has Rules, Inbox and History views. Use Inbox to mark a notification seen, acknowledge it or clear it. Clear all requires a second confirmation. These actions change notification receipts, not the event’s reviewed/dismissed state or the camera’s health. History shows delivery attempts and suppression reasons.
For desktop notifications, add a Browser action and select Enable browser alerts when offered. Browser permission is separate from KeepPeek access and server inbox delivery. The browser’s current permission and an active frontend determine whether it can display a system alert; this is not an offline push service for a closed application. Use Pushover when you need the supported external push channel.
Webhook actions send to the configured URL with a five-second timeout. They do not follow redirects, accept arbitrary request headers or allow credentials in the URL. Configure an endpoint that can accept the supported payload and inspect History after testing it. Enrichment can create a second webhook delivery only when the action enables that behavior.
The rule editor also exposes a Forwarder action, but its current provider returns
channel_unavailable. It is not the MQTT event forwarder. Configure MQTT independently under
Settings > Integrations, as described below.
Avoid duplicate and noisy alerts
KeepPeek gives one logical notification a stable identity derived from its rule, source, event or outage, and lifecycle. Event revisions, retries, enrichment, and recovery retain that identity while KeepPeek remains running. Equal message text never collapses unrelated cameras or events.
Rules can apply cooldowns per event family, camera and event kind, group, whole rule, or outage interval. The cooldown starts when the logical notification is created and resets on restart. Rate limits can apply to the rule, channel, principal, or whole server and also reset on restart. Critical bypass is explicit, bounded, and logged.
A preliminary alert may be replaced by an enriched revision when a canonical image or final duration becomes available within the configured deadline. Missing imagery does not suppress a metadata alert unless the action explicitly requires an attachment. Late enrichment stays in history without waking the user unless the rule permits it.
The process-local inbox keeps unread, seen, acknowledged, and cleared state per authenticated principal. Provider delivery and acknowledgement remain separate from KeepPeek review state. Delivery history explains whether work was created, replaced, suppressed, collapsed, rate-limited, retried, expired, delivered, or failed until the next restart.
Deliver through Pushover
Pushover is the supported push channel. To configure it:
- Register an application at https://pushover.net/apps/build.
- Obtain the application token and a user or delivery-group key.
- Add a Push action to a notification rule.
- Enter the token and key directly in Settings > Notifications.
- Save, activate, and run the rule’s test action.
The two credentials are write-only. KeepPeek stores owner-only values in secrets.toml and keeps
references with the rules in config.toml. It never returns them to the browser, health snapshots,
logs, or delivery reasons. Editing non-secret settings preserves the configured values; replacing
credentials requires entering both again.
Push actions support optional device names, sound, priorities -2 through 2, a public base URL
for deep links, and one bounded JPEG attachment. Emergency priority 2 also requires retry and
expiry values and records provider acknowledgement separately from KeepPeek acknowledgement.
Timeouts, connection failures, HTTP 408, 425, 429, and 5xx responses use the rule’s bounded in-memory retry policy. Invalid credentials and other permanent provider failures do not retry indefinitely. Restarting KeepPeek discards pending attempts. Disabling a rule or Push action stops new work and expires its pending attempts.
The notification outbox holds at most 10,000 pending/retrying actions in process memory. A full
queue records expired with reason outbox_full. Each action also has its own attempt, retry and
expiry bounds. Restart is not a recovery strategy for pending delivery: it discards that work and
can allow replayed events to notify again after deduplication state is cleared.
Forward events through MQTT
Open Settings > Integrations to configure, test, observe, or disable the MQTT event forwarder. It requires an MQTT 5 broker; MQTT 3.1 and 3.1.1 are not fallback protocols.
Configure a stable client ID, KeepPeek instance ID, topic prefix, optional username and write-only
password, QoS, retention policy, and optional CA certificate for mqtts://. Use TLS outside an
explicitly trusted local development network. Credentials embedded in the broker URL are rejected.
The default topics are:
keeppeek/{instance_id}/sources/{source_id}/events/{event_type}
keeppeek/{instance_id}/forwarders/{forwarder_id}/status
Every event publication is a complete JSON snapshot with schema_version = 1. Consumers should
deduplicate with (instance_id, event_id, revision). Camera outage and recovery records use the
same envelope and carry bounded cause, affected streams, recording consequence, duration, and
recovery evidence. This profile publishes attachment metadata, not image bytes.
QoS 1 is the default and provides at-least-once transport while KeepPeek remains running. A broker reconnect around acknowledgement can redeliver the same stable identity, so consumers must still deduplicate. QoS 0 has no broker acknowledgement; QoS 2 does not make queued work durable across a KeepPeek restart.
The bounded in-memory MQTT outbox defaults to 64 MiB and reconnect delay grows from 250 ms to a 30-second ceiling. A broker outage or configuration replacement does not reconnect camera streams. Restarting KeepPeek discards pending publications and deduplication history. When the outbox is full, forwarding fails visibly while event persistence and recording continue.
Settings and Prometheus expose connection state, bounded error detail, pending items and bytes, retry and duplicate counts, last received and delivered times, and oldest unacknowledged work.
Troubleshoot a missing alert
Check the chain in order: the source event exists, the active rule matches it, its schedule and cooldown permit delivery, the action is available, and the provider or broker accepted the work. A missing image, an acknowledged inbox item and a failed provider request are separate outcomes. Changing one does not repair the others.
| Evidence | Next step |
|---|---|
| No event in Events | Check native events or the external analysis service, camera scope and UTC date. |
| Rule is saved but not active | Activate the intended draft and resolve any validation or revision conflict. |
suppressed, collapsed or rate_limited | Inspect the active filter, schedule, cooldown and next eligible time. |
| Push fails permanently | Verify both configured credentials and destination in a controlled test; do not repeatedly retry invalid credentials. |
| MQTT pending bytes keep growing | Check MQTT 5 broker availability, TLS trust, authentication and acknowledgements before the bounded outbox fills. |
| Inbox or queued work disappears after restart | This is process-local delivery state. The stored event and configured rule remain separate. |
For Home Assistant, the direct camera card displays live video. It does not create entities or subscribe to these events for you. MQTT consumers and Home Assistant automations need their own topic, payload and deduplication configuration.
See the detailed notification rule, Pushover, and MQTT 5 forwarder references for every field, payload, metric, and failure state.
Home Assistant card
Use custom:keeppeek-card to put live KeepPeek cameras in a Home Assistant Lovelace dashboard.
Home Assistant serves the card module and dashboard configuration, but the browser connects
directly to KeepPeek for session creation and WebRTC video. No Home Assistant backend integration,
media proxy, or embedded iframe is required.
flowchart LR
H[Home Assistant] -->|Card module and configuration| B[Browser]
B -->|Authenticated session requests| K[KeepPeek]
K -->|Direct WebRTC video| B
The card supports live video, a visual editor, camera selection, grid and single-camera layouts, focus, and shared connections. It inherits Home Assistant theme colors and adapts to dashboard width. Audio, PTZ, event ribbons, timeline playback, and Home Assistant entity discovery are not implemented in this card. Use the KeepPeek application for the corresponding supported features.
The direct card and the MQTT event forwarder solve separate tasks. The card displays selected live cameras; MQTT lets an independently configured consumer react to events. Installing the card does not create Home Assistant camera entities, motion sensors or automation triggers. See Notifications and integrations for the event envelope and delivery limits, and Recording and evidence for recorded playback.
Prepare KeepPeek
- Confirm that the selected cameras play in KeepPeek.
- Create a dedicated User credential with the intended camera access in KeepPeek’s access settings. See Authentication and access control.
- Make the KeepPeek endpoint and its advertised WebRTC addresses reachable from the browser that displays Home Assistant. Home Assistant Cloud does not relay the card’s media.
- Add the exact Home Assistant origin to the existing KeepPeek
config.toml:
[direct_card]
allowed_origins = [
"https://home.example.net",
"https://homeassistant.local:8123"
]
An origin consists of the scheme, host, and port, without a path or trailing slash. Include each origin used to open the dashboard. Restart KeepPeek after editing this configuration. See the Configuration reference for the server’s configuration contract.
Use browser-trusted HTTPS for normal deployments. The card permits HTTP only for local development
on localhost, 127.0.0.1, or [::1]. Keep credentials out of the endpoint URL. A private VPN is
preferable to exposing the recorder publicly.
KeepPeek allows credentialless CORS on /create and /delete for configured origins. The card
sends its Bearer key in the Authorization header and never uses browser cookies. Browser
preflights must allow POST, OPTIONS, Authorization, Content-Type, and Content-Encoding;
do not substitute a wildcard origin or a Home Assistant media proxy.
Install the card
The distribution contains one self-contained JavaScript module, keeppeek.js, and a metadata
file, keeppeek-card.json, with its version, SHA-256 checksum, and byte counts. The module bundles
its runtime, icons, and CSS; it does not fetch third-party scripts or fonts.
HACS installation
- Choose a KeepPeek release containing the card
artifacts. Earlier releases without
keeppeek.jscannot install the card. - In HACS, add
https://github.com/xnorpx/keeppeekas a custom Dashboard repository. - Download the selected release. If HACS does not register the resource automatically, add
/hacsfiles/keeppeek/keeppeek.jsas a JavaScript module dashboard resource. - Reload the dashboard and select KeepPeek in the card picker.
The HACS manifest selects the release artifact and hides default-branch downloads. A locally successful build does not make that artifact available through HACS. Custom-repository installation is separate from inclusion in the HACS default store.
Manual installation
- Obtain
keeppeek.jsandkeeppeek-card.jsonfrom the intended release, or build them locally. - Verify the module’s SHA-256 against the metadata file.
- Put
keeppeek.jsin thewww/directory inside Home Assistant’s configuration directory. Restart Home Assistant if this is the first time you createdwww/. - Register
/local/keeppeek.js?v=VERSIONas a JavaScript module resource, replacingVERSIONwith the artifact’s version. - Reload the dashboard and add the card.
From the KeepPeek repository root, build the artifacts with:
bun run --cwd ui build:home-assistant
The output directory is target/home-assistant-card/dist/. Building from the checkout allows
testing in real Home Assistant before publishing a release.
Configure a dashboard
For a YAML-mode dashboard, put the dedicated key under keeppeek_lovelace_token in Home
Assistant’s secrets.yaml, then use this configuration:
type: custom:keeppeek-card
endpoint: https://keeppeek.example.net
token: !secret keeppeek_lovelace_token
title: Entrances
sources:
- source_id: "192.168.1.20"
title: Front door
quality: auto
- source_id: "192.168.1.21"
title: Driveway
quality: low
layout: grid
columns: 2
aspect_ratio: "16:9"
show_name: true
Replace the example IDs with stable source IDs advertised by KeepPeek. Camera display names and
Home Assistant entity IDs are not substitutes. Home Assistant resolves !secret; the card does
not parse YAML or resolve secret references itself. Home Assistant’s secrets.yaml is distinct
from KeepPeek’s companion secrets.toml.
In the visual editor, enter the endpoint and access key, then select Load sources to discover available cameras. For storage-mode dashboards, enter the key through this editor instead of using a YAML secret tag. The existing key is never read back into the form: an empty password field with a configured placeholder preserves it. Enter a replacement or use the clear action to change it explicitly.
After saving, test from the actual dashboard browser and network used by the intended viewer. Confirm every selected source is visible to that credential and that an omitted or ungranted camera is not accessible. Start with one camera before increasing the grid size. A configuration that is valid for 16 sources does not guarantee the device can decode 16 streams smoothly.
| Option | Default | Behavior |
|---|---|---|
sources | Required | Between 1 and 16 unique source objects. |
sources[].quality | auto | Select auto, low, or high from the server’s available variants. |
sources[].title | Camera name or ID | Optional title below the video. |
title | KeepPeek | Optional card heading. |
layout | grid | grid displays the selected sources; single provides a camera selector. |
columns | 2 | Integer from 1 to 4, limited to the visible camera count; the grid stacks below 440 pixels. |
aspect_ratio | 16:9 | 16:9, 4:3, or 1:1; video is contained rather than cropped. |
show_name | true | Show camera names below the video. |
view | live | Only live video is supported. |
Source IDs and titles must be nonblank and at most 160 characters. The editor preserves source titles, quality selections, and unrelated Home Assistant layout metadata across edits. The detailed card reference lists all validation limits and transport details.
Understand the security boundary
The key necessarily reaches the browser. A YAML !secret reference keeps configuration organized,
but does not conceal the resolved key from a person who can inspect Home Assistant’s dashboard
configuration or runtime. Dashboard editors and installed third-party frontend modules are part
of this trust boundary. Use a dedicated User key, not an Administrator key, unless every viewer
should have that authority.
The card keeps credentials in memory and does not put them in URLs, browser storage, rendered diagnostics, or console output. That does not prevent Home Assistant from storing its dashboard configuration or browser developer tools from showing the authenticated request header.
The sources list is a display filter, not authorization. KeepPeek’s trusted-local policy still
applies: a browser classified as local is an Administrator even when it sends a User key. Narrow
access.local_networks when dashboard viewers must be authenticated and restricted. CORS neither
changes that policy nor prevents a copied key from being used by another client.
Rotating, disabling, revoking, or expiring the credential invalidates its sessions according to server policy. Authentication failure stops automatic card retries; update the key and reconnect.
When changing camera grants, review the credential’s access in KeepPeek as well as the card’s display list. Sharing a saved KeepPeek dashboard or adding a camera to a group does not make the card an authorization boundary. Test restrictive grants from a client classified as remote; trusted-local Administrator access intentionally follows a different policy.
Connections and troubleshooting
Cards in the same browser document share a connection when their normalized endpoint and key match. Identical source and quality selections also share one subscription. Different credentials never share media. Limits are eight connection identities per dashboard, 64 consumers per identity, and 16 distinct source/quality subscriptions per connection.
Hidden tabs, offscreen cards, removed cards, and changed selections release obsolete demand.
Removing the final consumer closes the peer and calls /delete. Reconnects have bounded waits
and at most eight automatic attempts, and restore only current subscriptions. Theme changes do
not reconnect. Lovelace can temporarily detach cards while rebuilding its responsive columns;
those moves may require a clean reconnect before video resumes.
| Symptom | Check |
|---|---|
| The card is missing from the picker | Confirm the installed module and JavaScript module resource URL, then reload. |
| Connection fails | Check the endpoint, certificate, exact allowed origin, and browser Local Network Access permission. |
| Access denied | Check the dedicated key and its enabled/expiry state in KeepPeek. |
| Unknown source ID | Use Load sources and select an ID visible to this credential. |
| One camera is offline | Check that camera in KeepPeek; other cards and sources should continue. |
| Video does not play | Check browser codec support and reachability of KeepPeek’s advertised WebRTC addresses. |
Browsers can report CORS denial, an unreachable server, certificate failure, and denied Local Network Access as the same fetch error. The card lists the relevant checks without claiming to identify the exact cause. Do not disable browser security checks to work around it.
Upgrade or roll back
Select the new version in HACS, or replace the manual module and update the resource’s ?v=
suffix. Reload all open dashboard tabs to release old module instances. Keep the card and server
on compatible releases because the protocol is pre-1.0. For rollback, restore the previous HACS
version or manual artifact and reload. Do not register multiple versions of the resource at once.
Test locally and in CI
The lightweight preview uses the real KeepPeek server and synthetic cameras, but emulates the Home Assistant dashboard shell. From the repository root:
bun run --cwd ui test:e2e:prepare
bun run --cwd ui demo:home-assistant
The demo prints its local URL. It does not use the operator’s configuration or cameras.
Test inside real Home Assistant
With Docker running and the repository prerequisites, including Playwright Chromium, installed:
bun run --cwd ui test:home-assistant-container
This prepares the binaries and module, pulls the official Home Assistant 2026.9.1 image pinned
by an immutable multi-architecture digest, and runs the tests. To reuse prepared artifacts:
bun run --cwd ui test:home-assistant-container:run
Each runtime scenario onboards a temporary user in actual Home Assistant. The container serves the built module and resolves YAML secret references. KeepPeek, the two synthetic RTSP cameras, and Chromium run on the host, so WebRTC media avoids Docker NAT. Tests cover fresh decoded frames on desktop and mobile, three cards sharing one session/subscription, reconnect, navigation cleanup, themes, source discovery, credential redaction, and saved changes in the real visual editor.
The container uses two CPUs, 2 GiB of memory, a loopback-only HTTP port, and a Docker-managed configuration volume without privileged or device access. Tests remove their own containers, volumes, temporary files, and fixture processes. They never seed internal authentication storage.
On macOS, this suite has been exercised with ARM64 Docker Desktop as a development test. That is not a supported Home Assistant production deployment recommendation. The official macOS installation guide describes Home Assistant OS in a VM; the container installation guide targets Docker Engine on Linux.
CI and release verification
The Home Assistant Container Ubuntu job is required by the UI gate. It reuses the Linux KeepPeek and card build artifacts and the pinned Home Assistant image. Its evidence includes screenshots, sanitized server logs, onboarding notices, and JUnit results, but excludes raw configuration, authentication storage, and credential-bearing browser traces.
The normal ./check.sh remains Docker-free and typechecks the container suite. Run both it and
the container command when changing the integration. The container tests record Home Assistant’s
exact setup-only WebSocket close notice separately; other runtime errors fail the tests.
Local real-Lovelace tests do not prove HACS delivery. A release check must still install the published module through HACS, exercise it in a disposable Home Assistant instance, and verify upgrade and rollback. A configured CI job is not evidence of a successful hosted run; check the workflow result for the commit being released.
Groups and two-way audio: current status
Live participant groups are not operational in the current server and browser runtime. Settings includes Groups & two-way audio, but its directory reports unavailable. The generated protocol defines list, join, and leave messages; the running implementation does not handle those commands. There are no real group names, participant counts, join sessions, or participant recording states to display. Creation and administration controls explain this limitation.
Do not add an invented [groups] section to config.toml or infer support from generated types.
The configuration reference lists the supported settings.
Three different uses of grouping
| Concept | Current purpose | What it does not grant |
|---|---|---|
| Camera group labels and access grants | Organize cameras and define which cameras an identity may access. | Membership in a voice or conference session. |
| Saved dashboards and their audience | Share a camera layout and its wall presentation settings with selected viewers. | Permission to see cameras outside the viewer’s grants. |
| Live participant groups | A protocol and design model for named camera collections with optional participant audio/video. The runtime is unavailable. | A working walkie-talkie, conference, or group administration feature today. |
Use Authentication and access control for camera grants and Live wall for saved layouts. These implemented features work independently of live participant groups.
What the group design describes
The group client scenarios describe server-owned definitions with static camera stream members, optional passwords, and optional live participants. Participant media would have a server-assigned identity and its own stream and recording policy. Camera membership would not change a camera’s ordinary recording policy.
The design is full duplex: it has no floor control, moderator, or server-enforced speaking turn. Push-to-talk means a client gates its own microphone while keeping its publication ready. It is not a server command or a promise that the current browser includes a working radio console.
Those scenarios explain the intended contract, including reconnect and per-participant recording behavior. They are not evidence that the handler, client capture, directory, or recording path has been implemented and qualified. A future implementation needs runtime and device evidence before this chapter can provide operational setup instructions.
Camera talkback is separate
Talkback sends audio to a supported physical camera. A live participant group would exchange media between clients through KeepPeek. Support for one does not imply support for the other.
The current browser Talk control is unavailable. Consult Camera controls for supported physical camera operations and the availability reason shown for the selected device.
How KeepPeek works
KeepPeek puts recording, live media, event storage, and investigation in one local service. The browser is a client of that service. Closing a tab ends its viewing work; it does not stop the server’s configured recording policy.
This chapter explains the current boundaries. Users and design choices contains the product rationale, while the feature chapters provide operational instructions.
Media and event paths
flowchart TB
Camera[IP camera] -->|Encoded media| Core[KeepPeek]
Core -->|Record| MP4[MP4 and catalog]
Core -->|Live WebRTC| Browser[Client]
MP4 -->|Playback via KeepPeek| Browser
The camera supplies encoded media. KeepPeek understands codecs and timing well enough to route, package, and record those streams without decoding and re-encoding their video in the core. The browser or an external service performs the decoding it needs. Camera control, native events, and media can use different protocol connections, so success on one path does not prove the others are healthy.
Supported native adapters and optional analysis services publish events through a separate path:
flowchart TB
Source[Camera or analysis service] --> Events[Validate and store event]
Events --> Review[Review in KeepPeek]
Events --> Delivery[Rules and MQTT]
The server separates camera/media work, storage work, WebRTC sessions, and integration delivery. Health and queue evidence make stalls and pressure visible. Optional analysis and notification delivery do not become a prerequisite for ordinary recording. Resource exhaustion or failure of the recorder’s own storage and network still needs operational attention.
Persistent data and live state
| Data | Owner and purpose | Operational consequence |
|---|---|---|
| Application configuration | The existing config.toml holds supported settings, policies, layouts, and access configuration. | Use validated saves or configuration import and preserve the file across upgrades. |
| Reusable private strings | The companion secrets.toml supplies supported secret references. | Preserve references and protect configuration archives and private restore material. |
| Encoded recordings | MP4 files in the configured storage paths. | Retention, recording policy, storage pressure, and file integrity determine what remains. |
| Recording and event catalog | Indexed recording identity, coverage, events, and related durable investigation state. | Recover it consistently with the media and the documented archive procedures. |
| Images and exports | Feature-owned data under configured paths and retention policies. | A playable recording does not imply that an event thumbnail or export is retained. |
| Notification and MQTT runtime state | Bounded process memory for pending work and related runtime history or deduplication. | A restart does not replay this work. Persisted rules and provider configuration are separate from delivery state. |
| Active connections and subscriptions | The running server and each client connection. | Reconnect and resolve current capabilities; a stored preference does not prove a stream is live. |
The configuration reference is the authority for serialized fields, defaults, and paths. Do not create separate settings files for layouts or permissions. For a consistent recovery plan, use Backup and restore; configuration export and a full recording archive serve different purposes.
Control, media, and access
HTTP creates and deletes API sessions and provides dedicated administration and diagnostic operations. WebRTC carries typed control messages, data messages, and media. Clients must use the negotiated capabilities and source identities instead of assuming that every camera has the same streams, codecs, events, or controls.
Authentication identifies the caller; authorization determines the operations and cameras that caller may use. The same restrictions apply through a dashboard, deep link, or integration. A hidden UI control is not the security boundary. See Authentication for local trust, named credentials, roles, camera grants, reverse proxies, and revocation.
The server exposes configuration export/apply, backups, recording coverage, logs, and metrics on dedicated HTTP routes. Their methods and authentication requirements are defined by the HTTP contract and implementation; they are not a general REST replacement for the WebRTC control protocol. Custom clients should start with the API overview and verify runtime support for each command they need. The API is pre-1.0 and does not yet promise stable compatibility across versions.
Recording truth and investigation
Live frames, stored coverage, event timestamps, and exported evidence answer different questions. A connected camera can fail to record; a retained event can outlive its image or video; a successful seek does not prove that the surrounding day has no gaps.
Keep combines recorded coverage, selected time, camera identity, and event context. Health screens explain stream progress and recorder evidence. Maintenance inspects persisted files and catalog state. Keep these layers separate when diagnosing an incident so that a browser playback problem does not become an unnecessary archive repair.
Start with Recording and evidence and Camera and stream health. Use Recording maintenance when stored-data evidence warrants it.
Extension boundaries
External services consume media and publish supported events without becoming part of the core recorder. Camera-native adapters provide another event source. Notifications and MQTT consume committed events and maintain their own delivery evidence. Home Assistant’s card connects the browser directly to KeepPeek rather than proxying video through Home Assistant.
Some broader protocol scenarios are implemented only in part. Live participant groups currently have declarations and a UI explanation without a runtime handler. Consult the feature’s availability statement before designing a deployment around it.
Implementation entry points include
src/app.rs for startup,
src/server.rs for request handling,
src/webrtc.rs for sessions and media,
and src/storage for recording and
catalog behavior. Development and verification instructions are in Contributing.
Users and design choices
KeepPeek is a local-first network video recorder and media gateway. Its core job is to ingest camera media, record it reliably, make live and stored media available, and provide a stable place for events and integrations to meet. It can use analytics events produced directly by a camera, events from an optional external service, or no analytics at all.
That narrow center is deliberate. Inference frameworks, models, accelerators, and licensing change much faster than a recorder should. KeepPeek therefore accepts camera-native events and makes media available through an open protocol instead of deciding which models every installation must run.
This chapter records the product’s users and rationale, including intended directions. Use the feature guide for current screen availability and release readiness for qualification limits. The architecture overview connects these choices to the running service.
At a glance
| User | Starting point | What they value | KeepPeek’s role |
|---|---|---|---|
| Software Consumer | Common cameras and a problem to solve, not an integration project | Sensible defaults and dependable results | A complete first-party recording and viewing experience |
| Small Business Security | A shop, workshop, office, or yard with needs generic analytics do not fully cover | Relevant alerts, dependable evidence, and a solution fitted to the business | A reliable recorder plus a boundary for tailored inference and workflows |
| Open Source Enthusiast | Existing tools and a willingness to connect them | Reuse, inspectability, and replaceable components | A composable open core rather than another closed ecosystem |
| Home Automation Enthusiast | Home Assistant, MQTT, HAP, automations, and devices that should cooperate | Local orchestration, useful events, and stable identities | A camera and event source for the automation system already in place |
| Residential Sysadmin | A home server, NAS, container host, or automation stack | Reliability, diagnostics, and repeatable operation | A native service with stable interfaces and health evidence |
| Residential Power Optimization Engineer | Cameras, networks, and compute that can be arranged deliberately | Control, measurement, and efficient placement of work | An observable media plane that can feed the right hardware |
Software Consumer
I just want it to work.
Profile
- Starting point: A set of common cameras and an expectation that recording, live view, and review form one product.
- Primary goal: Install it, add cameras, and trust that footage will be available when needed.
- Constraints: Little interest in protocols, model runtimes, or maintaining an integration stack.
How they use KeepPeek
- Set up: Follow a guided camera workflow with safe defaults.
- Operate: Use the Svelte web application for live viewing and recording review.
- Investigate: Receive a clear diagnosis and a next action instead of raw protocol state.
- Extend: Choose an optional free or paid service only when it adds something useful.
What success looks like
Core recording works without a cloud account, an AI subscription, or decisions about model and accelerator compatibility.
Product implications
The first-party experience must hide optional architecture until it matters, without removing the open interfaces that let other users go further.
Follow Get started from installation to a first camera, then verify both live stream health and recorded coverage.
Small Business Security
Custom detection for the way the business actually works.
Profile
- Starting point: A shop, workshop, office, warehouse, or yard with existing cameras and a specific operational risk.
- Primary goal: Combine dependable recording with alerts and search events designed around the way the business operates.
- Constraints: Privacy, false alarms, existing equipment, available compute, response time, and a solution that staff can operate without becoming AI specialists.
How they use KeepPeek
- Establish the baseline: Deploy KeepPeek for local recording, live viewing, health, and portable MP4 evidence.
- Define the need: Identify the events, objects, zones, schedules, and responses that matter to the business rather than starting with a generic model feature list.
- Add tailored inference: Run a separate service that subscribes to selected WebRTC streams and publishes normalized events back to KeepPeek.
- Connect the workflow: Route useful events into the KeepPeek timeline, notifications, Home Assistant, MQTT, webhooks, or another business system.
What success looks like
The business receives relevant, reviewable events without making recording dependent on the custom model. The inference service can evolve with the business while KeepPeek remains the stable media and evidence layer.
Product implications
Custom business logic belongs in an external protocol plugin, not a private fork of the recorder. That keeps camera credentials and storage ownership inside KeepPeek while allowing a tailored free or paid service to use the hardware, models, and support arrangement appropriate for the customer.
Need a tailored solution? Contact KeepPeek to discuss custom AI, inference, integrations, and deployment for your specific business needs.
Open Source Enthusiast
Because someone else already wrote it.
Profile
- Starting point: A collection of proven open-source tools, libraries, models, and devices.
- Primary goal: Reuse the best available component for each job and replace it when something better appears.
- Constraints: Avoid proprietary lock-in and duplicated implementations.
How they use KeepPeek
- Set up: Combine KeepPeek with existing cameras and familiar self-hosted infrastructure.
- Operate: Use the included viewer or another client that implements the public protocol.
- Investigate: Inspect the server, wire contract, and generated bindings.
- Extend: Build or adopt an independent detector, transcoder, event forwarder, or viewer.
What success looks like
The recorder remains a useful shared foundation while every fast-moving or specialized component can come from somewhere else.
Product implications
The licensing model protects improvements to the server while allowing independent clients and services to use their own licensing and business model.
Home Automation Enthusiast
Because the porch light should have opinions.
Profile
- Starting point: A local automation system built around Home Assistant, MQTT, HAP/HomeKit, notification services, and a growing collection of devices.
- Primary goal: Turn camera events and recording health into useful household actions without making the automation controller proxy video.
- Constraints: Stable device identities, local control, understandable event semantics, and automations that recover cleanly after either system restarts.
How they use KeepPeek
- View: Put selected live streams, events, and timeline review in a Home Assistant dashboard.
- React: Forward normalized camera-native or external analytics events through MQTT or webhooks.
- Present: Use an independent HAP bridge when selected cameras should appear in Apple Home.
- Observe: Scrape Prometheus metrics and include recording or camera health in automations.
What success looks like
A meaningful camera event can turn on a light, send a notification, or update a dashboard while KeepPeek continues recording independently. Reconnects do not silently retarget an automation to a different camera, and media does not take an unnecessary trip through the automation hub.
Product implications
KeepPeek needs stable source IDs, normalized event types, backfill after disconnects, direct browser media, and clear integration boundaries. It should participate in a home automation system without trying to replace one.
Residential Sysadmin
Unnecessarily professional.
Profile
- Starting point: A self-hosted environment with monitoring, backups, and perhaps Home Assistant, MQTT, or a reverse proxy.
- Primary goal: Operate cameras like a small but well-run production service.
- Constraints: Limited maintenance time and no separate operations team.
How they use KeepPeek
- Set up: Install one native service or container and validate each camera before trusting it.
- Operate: Use the web interface for normal work and inspect health evidence, reason codes, and bounded logs when something changes.
- Investigate: Distinguish camera, transport, recording, and external-service failures.
- Extend: Attach integrations without patching the recorder or sharing its storage internals.
What success looks like
The system can be monitored and recovered using explicit server, storage, camera, stream, ingress, recording, and WebRTC evidence instead of guesses based on whether a video tile happens to be playing.
Product implications
KeepPeek exposes stable health reason codes, bounded logs, and documented interfaces. Rust supports a predictable native service, while the open API and external-service boundary keep upgrades and failures isolated.
Residential Power Optimization Engineer
Profile
- Starting point: A mixed set of cameras and compute devices, each with different power and performance characteristics.
- Primary goal: Put capture, storage, decoding, and inference where each task runs best.
- Constraints: Power, heat, bandwidth, accelerator availability, privacy, and equipment already owned.
How they use KeepPeek
- Set up: Run KeepPeek close to the cameras and storage.
- Operate: Record camera-native streams without requiring an inference workload on the server.
- Investigate: Use explicit health and media evidence to find transport, recording, or downstream-service problems.
- Extend: Subscribe to WebRTC media from whichever device should decode or analyze it.
What success looks like
Recording stays dependable while optional work can move between a server, GPU host, desktop, or repurposed edge device without redesigning the recorder.
Product implications
The server must remain efficient and observable, while its media boundary must not assume that inference runs in the same process or even on the same machine.
Cameras, brands, and protocols
KeepPeek assumes that a home or small business may contain cameras from several generations and manufacturers. Compatibility is therefore organized around working protocols and verified media, not a logo on the enclosure. A discovered camera is a candidate, not proof that authentication, streaming, controls, and events all work.
Brand integration paths
| Camera family | Integration path | Current meaning |
|---|---|---|
| Reolink | Dedicated reo-proto backend for Baichuan over TCP or UDP, with ONVIF and RTSP fallback paths | First-class discovery, authentication, main/sub media, supported controls, and native camera events |
| Dahua and compatible OEM/rebrand models | ONVIF discovery and profile queries with RTSP media through Retina or explicit stream URLs | A supported standards-based family with coverage for common H.264/AAC and H.265/G.711 stream patterns; each OEM model and firmware still requires validation |
| Tapo (supported models) | ONVIF discovery and profile queries where available, with RTSP media through Retina or explicit stream URLs | A supported mainstream vendor through the standards path; exact capabilities still depend on model and firmware |
| Hikvision / Annke | SADP discovery, ONVIF/RTSP media, and a plain-Rust ISAPI event worker | Native event support depends on model, firmware, channel mapping, and enabled camera rules; see the ISAPI guide |
| Other ONVIF cameras | ONVIF discovery and device/profile queries, then RTSP media | Support depends on the device’s implementation of the standards and must be validated per model |
| Generic or unknown cameras | Explicit main and/or sub RTSP URLs | Recording can work without a recognized brand; metadata, controls, and native events may be limited |
Support a pragmatic subset, not every camera
KeepPeek does not aim to support every camera ever sold. The camera market contains too many firmware variants, incomplete protocol implementations, cloud-only products, and models that reuse a brand name while behaving differently. Claiming universal compatibility would create a large, fragile surface and make failures harder to diagnose.
The practical goal is excellent support for a well-known subset: dedicated integration where that creates real value, as with Reolink; tested standards paths for selected Dahua-family and Tapo models; and a clear ONVIF or RTSP route for other cameras that implement those protocols correctly. A brand row means KeepPeek deliberately tests and maintains a useful path for that vendor family. It does not mean every model, firmware version, codec, control, cloud feature, or native analytics event is supported.
Compatibility is described at three levels:
- Supported vendor and model: The documented firmware and configuration pass discovery or manual setup, authentication, main/sub streaming, keyframe detection, MP4 recording, and independent playback validation.
- Protocol-compatible camera: The camera exposes usable ONVIF and RTSP behavior, but its exact model has not yet earned a maintained compatibility claim.
- Manual RTSP camera: KeepPeek can record a validated stream URL even when discovery, controls, metadata, and native camera events are unavailable.
New camera support should be added from measured evidence and a maintainable protocol path, not by collecting one-off exceptions for every device. This keeps setup predictable for common cameras while leaving an honest manual path for experienced users.
Protocol responsibilities
| Responsibility | Protocol or component | What KeepPeek uses it for |
|---|---|---|
| Discovery | ONVIF WS-Discovery, Reolink discovery, Hikvision SADP, and bounded local port probes | Find candidate devices and collect safe identity hints |
| Device queries and control | ONVIF SOAP plus supported Reolink HTTP or Baichuan commands | Read device information, profiles, capabilities, imaging state, and supported PTZ controls |
| Generic media ingest | RTSP/RTP through Retina over TCP or UDP | Receive standards-based camera media, including explicit main and sub URLs |
| Reolink media ingest | Baichuan through reo-proto over TCP or UDP | Receive complete Reolink video and audio frames without requiring RTSP |
| Native camera events | Reolink Baichuan and compatible Hikvision/Annke ISAPI | Normalize supported alarms into stored/live events; classifications require event evidence, and image association is protocol-specific |
| Viewer and service delivery | WebRTC with RTP and reliable/unreliable data channels | Forward live media, control, events, and publications without exposing camera credentials |
WebRTC is the client-facing media gateway protocol, not a requirement imposed on cameras. Cameras can continue speaking their native protocol to KeepPeek while browsers and independent services use one consistent downstream API.
Media and transport policy
KeepPeek accepts H.264 and H.265 video and supported AAC, G.711 A-law, G.711 mu-law, and ADPCM
audio. It can retain main and sub profiles and records camera-native streams without re-encoding.
When backend selection is automatic, Reolink uses reo-proto and other cameras use Retina. TCP is
the conservative default; UDP remains available when a camera requires it or validation shows it
is the better path. Explicit RTSP URLs take precedence over URLs discovered through ONVIF.
Discovery alone is never the final compatibility test. The camera setup workflow verifies credentials, requested profiles, incoming frames and keyframes, finalized MP4 output, and independent FFmpeg decoding before recommending a backend or adding a camera to the live configuration.
How this fits the users
- Software Consumer: Gets discovery and safe automatic defaults where possible, plus a validated manual RTSP path when the camera is not recognized.
- Small Business Security: Can retain an existing mixed camera fleet and add custom inference only to the streams and locations that need it.
- Open Source Enthusiast: Can reuse ONVIF, RTSP, Retina, and vendor-specific protocol work and add another adapter without changing the WebRTC client contract.
- Home Automation Enthusiast: Gets stable camera and event identities across supported vendors without making the automation hub understand each camera protocol.
- Residential Sysadmin: Gets one inventory and one observable recording path across a mixed fleet, with the selected backend visible in diagnostics.
- Residential Power Optimization Engineer: Can choose main or sub media, compare transports, and keep camera-native inference on the camera when that is the most efficient location.
Key design choices
Keep inference outside the server
KeepPeek does not build object detection, face recognition, license-plate recognition, image description, or model management into the server. That does not mean inference must happen in a separate assisted service. A camera with built-in motion, person, vehicle, or other analytics can send those events directly to KeepPeek, which validates and stores them and presents them beside recorded media.
KeepPeek supports three independent modes:
- Recording only: Record and view media without enabling analytics anywhere.
- External analytics: Forward media to an optional service and accept the events it publishes.
- Camera-native analytics: Use events produced by the camera without another inference service.
The repository includes a
Python object-detection service demo
for the external analytics path. It uses aiortc to subscribe to encoded KeepPeek video over
WebRTC, decodes selected frames with the user-installed FFmpeg executable, runs inference, and
publishes normalized events back to KeepPeek. It is a reference implementation and CI example,
not a supported detector product.
Forward encoded streams, not inference JPEGs
KeepPeek does not provide a one-image-at-a-time inference API as its detector boundary. A common integration with an image-oriented service such as CodeProject.AI uploads one JPEG for each inference request. That is convenient for a caller that already owns an image, but it is inefficient when the source is a continuous H.264 or H.265 camera stream.
For an ordinary encoded camera stream, a server-generated snapshot pipeline would be:
camera stream -> KeepPeek video decode -> JPEG encode -> HTTP upload -> detector JPEG decode -> model
That path requires KeepPeek to bundle video decoders, convert full-resolution frames into pixels, encode and allocate a new JPEG for every sample, and transfer it only for the detector to decode it again. Repeating that work across several cameras and inference frames per second adds CPU use, memory traffic, latency, and another codec and licensing surface to the core service.
KeepPeek instead uses the media gateway it already has:
camera stream -> KeepPeek encoded forwarding -> detector video decode and sampling -> model
Forwarding encoded access units is much cheaper for the core than decoding pixels and producing JPEGs. The inference service keeps one decoder running near the model, chooses its own sampling rate, drops stale frames, and can batch work across cameras. This preserves timing and stream continuity while letting the service select an H.264 or H.265 decoder suited to its hardware.
JPEG remains useful after inference. A service or camera can attach a deliberately selected snapshot to a durable event, but KeepPeek does not turn every candidate inference frame into a JPEG. Event evidence is bounded output, not the transport used to feed the model.
User fit: Consumers avoid hidden image work; small businesses can run efficient continuous inference; open-source developers keep decoding beside model scheduling; home automation enthusiasts receive normalized events without decoding video; sysadmins keep image codecs out of the recorder; power optimizers avoid repeated decode, JPEG encode, and JPEG decode work.
Inference changes quickly and brings hardware drivers, large model files, model-specific licenses, and different privacy decisions. Keeping it out of the recorder lets camera firmware or an external model change without coupling its lifecycle to recording and live viewing. It also avoids making extra AI compute and a cloud account mandatory costs of running an NVR.
User fit: The consumer can use intelligence already present in a camera; a small business can commission inference for its specific risks; the open-source enthusiast can swap model stacks; the home automation enthusiast consumes the resulting normalized events; the sysadmin gets a smaller failure domain; the power optimizer chooses the best compute target.
Keep recording and media routing at the center
KeepPeek owns the durable and slow-changing responsibilities: camera ingest, recording, stable media identity, timeline access, health evidence, authorization, and validation of events received from cameras or published back by services. Specialized inference can happen inside a camera or in a replaceable service around that center; transcoding likewise remains outside the core.
This boundary keeps the most important promise narrow: footage should still be recorded when an optional service is overloaded, incompatible, or offline. External services receive media through the gateway instead of opening camera sessions or writing KeepPeek’s database and recording directories directly.
User fit: Consumers get one accountable recorder; small businesses keep custom models independent from evidence; open-source enthusiasts compose services; home automation enthusiasts get a stable event source; sysadmins retain one recording authority; power optimizers can place each workload independently.
Why WebRTC fits KeepPeek
flowchart LR C1[Supported camera] -->|Encoded stream| K[KeepPeek<br/>Record, route, validate] C2[Supported camera] -->|Encoded stream| K K -->|MP4| S[(Recording storage)] K -->|WebRTC media| B[Browser or viewer] K -->|WebRTC media| I[Inference service] I -->|Events| K K -->|Normalized events| A[Home automation]
WebRTC is a mature, widely implemented protocol suite rather than one vendor’s media stack. It is built into browsers, and interoperable implementations are available for most common service languages. Examples include:
An independent viewer, inference service, or protocol plugin can therefore choose an implementation that fits its language and deployment instead of adopting KeepPeek’s Rust stack. The author of KeepPeek is also well versed in WebRTC protocols and browser behavior. That experience reduces risk in the difficult parts of a media gateway: SDP negotiation, ICE, RTP packetization, encryption, congestion handling, SCTP, and interoperability between native and browser clients.
WebRTC also gives KeepPeek two complementary delivery paths inside one session:
- RTP over UDP carries latency-sensitive audio and video without forcing every frame through an ordered reliable stream.
- Data channels over SCTP carry ordered reliable protobuf control plus reliable or discardable binary payloads. They can also deliver encoded media when RTP is not the right binding for a client.
This combination provides flexibility without inventing separate protocols for viewers,
publishers, transcoders, and inference services. It also gives browsers clean integration through
RTCPeerConnection, negotiated media tracks, and data channels rather than a custom framing and
playback stack.
The protocol boundary separates camera access from compute placement. An inference client can run beside KeepPeek, on a GPU machine elsewhere on the private network, or on a repurposed device. An older iOS device, for example, could run a small client application that embeds a compatible WebRTC implementation, subscribes to an encoded stream, and runs a suitable local model without receiving camera credentials or direct access to the recording store. Results return through the same API as events.
User fit: Consumers receive browser-native live playback; small businesses can attach custom inference without exposing camera credentials; open-source developers can choose a language-native implementation; home automation enthusiasts get direct media and event paths; sysadmins manage one observable boundary; power optimizers can move workloads without moving storage.
Fit into home automation instead of replacing it
KeepPeek should be a focused recorder and media gateway inside a larger home or small-business system, not another general-purpose automation hub. Its open API, stable source identities, normalized events, and observability endpoints let established tools do the jobs they already do well.
| Integration | Connection boundary | Intended role |
|---|---|---|
| Home Assistant | A Lovelace card connects directly to KeepPeek over WebRTC; an optional integration can expose entities | Put live cameras, events, timelines, and recording health in an existing dashboard without proxying media through Home Assistant |
| MQTT | A data-only event forwarder subscribes to normalized KeepPeek events and publishes them to the user’s broker | Feed automations with camera-native or external analytics events, optional snapshots, and disconnect backfill without decoding video |
| HomeKit Accessory Protocol (HAP) | An independent bridge translates selected KeepPeek capabilities into HomeKit accessories | Present compatible camera streams, events, doorbells, and controls to Apple Home while keeping HAP-specific behavior outside the recorder core |
| Prometheus | The monitoring system scrapes GET /metrics | Build dashboards and alerts for server, storage, camera, stream, recording, and WebRTC health |
Home Assistant and MQTT designs build on the public API and normalized event model. HAP is an intended bridge boundary rather than a currently implemented server feature. A HAP bridge can be free, community-maintained, or commercial without requiring HomeKit code or credentials inside the KeepPeek process.
User fit: The consumer can choose integrations by name; a small business can connect security events to operational workflows; the open-source enthusiast can reuse mature ecosystem projects; the home automation enthusiast gets stable events and direct media; the sysadmin can connect familiar dashboards, brokers, and monitoring; the power optimizer can place each bridge on suitable hardware.
Use Rust for the server
A recorder is a long-running, concurrent parser of untrusted network and media data. Rust provides native performance and control over allocation without a garbage collector, while its ownership and type systems prevent broad classes of memory and concurrency errors. It also supports native services on Linux, macOS, and Windows and has reusable libraries for the protocols KeepPeek needs.
The trade-off is a steeper contribution path and longer compile times than some managed languages. That cost is accepted in the core because predictable resource use and memory safety matter more than optimizing for the fastest first prototype.
User fit: Consumers benefit from reliability without needing to know the language; small businesses get a dependable evidence service; open-source contributors get compiler-checked interfaces; home automation enthusiasts get a dependable event source; sysadmins get a predictable native process; power users get low overhead.
Keep the core small and focused
KeepPeek is intended to be a recorder and media gateway, not a bundled appliance stack. The core service should not need a gigabyte or more of memory merely to start and perform ordinary recording and forwarding. That baseline is different from the configurable in-memory cache used for pre-event recording. KeepPeek retains recent encoded frames so an event-triggered recording can include what happened before the event began.
The cache grows with the number of buffered streams, their bitrates, the enabled main and sub profiles, and the configured pre-event duration. A longer window across many high-bitrate cameras can therefore use substantial memory by design. Keeping those frames encoded makes the cache much cheaper than retaining decoded images, but it does not make the cache free. A hard total-RAM guarantee must wait for representative measurements and must always state the camera, bitrate, profile, and pre-event-cache assumptions.
Keeping inference runtimes, model weights, video encoders and decoders, and a packaged desktop browser out of the server protects its binary size, memory footprint, startup time, and attack surface. An optional detector or transcoder pays for its own dependencies and resources without making every KeepPeek installation pay the same cost.
User fit: The consumer avoids unused runtime costs; a small business can scale custom inference separately; the open-source enthusiast can compose specialized tools; the home automation enthusiast keeps orchestration in the existing hub; the sysadmin gets a service suited to modest hardware; the power optimizer can account for each workload.
Run on Linux, macOS, and Windows
The recording, media gateway, health, and public API behavior should remain consistent across Linux, macOS, and Windows. Distribution should follow the convention that makes each platform feel native rather than forcing one packaging model everywhere.
| Platform | First-class distribution |
|---|---|
| Linux | A minimal multi-architecture Docker image for linux/amd64 and linux/arm64, published to GHCR with a persistent configuration volume. Native archives remain useful, but Docker is the primary polished Linux deployment. |
| macOS | A signed and notarized KeepPeek.app in a drag-to-install DMG, with a per-user launchd service. The current release target is Apple Silicon. |
| Windows | Authenticode-signed x86-64 and ARM64 binaries plus a signed NSIS installer that can register, upgrade, and remove the Windows service. |
Public macOS and Windows release installers should be signed, timestamped where supported, and verified by the release workflow before publication. Checksums remain useful for every platform, including Linux images and native archives.
An operations-friendly combination: A focused Rust service is straightforward to build across platforms, first-class Docker images make Linux deployment repeatable, and the optional Prometheus
GET /metricsendpoint plugs into standard dashboards and alerts. Together these make KeepPeek a strong choice where portability, container deployment, and conventional observability matter.
The Svelte interface runs in the browser and provides the same administration, live view, and recording workflow regardless of which operating system hosts the server. Platform-specific code should stay behind narrow boundaries so a feature does not quietly become available on only one host.
User fit: Consumers avoid operating-system lock-in; small businesses can deploy on existing hardware; open-source contributors can port and test explicit platform boundaries; home automation enthusiasts can place bridges beside their existing controllers; sysadmins can use the machine already running the home; power optimizers can choose hardware and operating system independently.
Store recordings as MP4
KeepPeek stores encoded video and supported audio in ordinary .mp4 files rather than a
proprietary media container or database blob. Finalized segments can be copied, inspected, and
played with established tools such as VLC, FFmpeg, and ffprobe, provided that the tool supports
the codecs used by the camera. Basic access to a recording should not require KeepPeek to remain
installed or an export into a private intermediate format.
The files use fragmented ISO MP4. Each file starts with its initialization metadata and continues
with keyframe-aligned media fragments. This structure lets KeepPeek flush recording progress,
index exact byte ranges for timeline seeks, and serve only the required fragments while the file
itself remains a standard media object. Active segments use a temporary .mp4.active suffix and
become .mp4 files when finalized without re-encoding or remuxing them.
The recording catalog stores identity, time, paths, events, and fragment indexes; encoded media stays in the MP4 files on disk. The catalog makes review faster, but it does not hold footage hostage inside a KeepPeek-specific database format.
MP4 is a container, not a codec. An H.265 recording still requires an H.265-capable VLC build, browser, or other player. Choosing MP4 improves interoperability without forcing KeepPeek to bundle the video decoders and encoders deliberately kept outside the core service.
User fit: The consumer can open a finalized segment in VLC; a small business can hand over portable evidence; the open-source enthusiast can reuse the MP4 ecosystem; the home automation enthusiast can attach standard evidence files to downstream workflows; the sysadmin gets files that fit normal backup workflows; the power optimizer can inspect recordings with standard tools.
Keep video codecs out of the core service
KeepPeek is codec-aware but does not bundle a video decoder or encoder. It understands enough H.264 and H.265 structure to receive encoded camera frames, identify configuration and keyframes, write MP4, and forward or repacketize media. It does not decode those frames into pixels or re-encode them inside the core service.
Bundling codec implementations would introduce patent and distribution questions that vary by codec, build, use, and jurisdiction. It would also bring substantial native dependencies, security surface, CPU or accelerator policy, and binary and memory cost into every installation. Keeping that machinery outside the core avoids silently inheriting the licensing and packaging choices of a particular FFmpeg or codec-library build.
On desktop, the first-party experience relies on Chrome or Chromium and the operating system’s media stack for video decoding and rendering. The UI asks the browser which RTP and MP4 codecs it actually supports and selects a compatible camera stream. Svelte provides the application UI; the browser provides the desktop runtime, media pipeline, accessibility primitives, and hardware decode path, so KeepPeek does not need to ship an Electron-style browser or codec pack.
Browser support is still a real boundary. H.264 is the broad compatibility path, while H.265 availability depends on the browser build, operating system, and hardware. When a camera-native stream cannot be decoded by the viewer, the user can select an H.264 substream or run an independent transcoding service that publishes a compatible variant back through the media gateway. The transcoder owns its codec dependencies, resource cost, and licensing decisions.
User fit: The consumer opens the interface in Chrome or Chromium without a KeepPeek codec bundle; a small business can use standard workstations or commission a specific transcoder; the open-source enthusiast can choose codec tooling deliberately; the home automation enthusiast can consume event metadata without decoding video; the sysadmin keeps codec packages out of the recorder; the power optimizer can use viewer-side hardware decoding.
Use Svelte for the first-party interface
KeepPeek needs a responsive browser interface for live state, timelines, health, and configuration, but it does not need a second application server. Svelte 5 and SvelteKit can produce a static client, use compiled reactivity for frequently changing media state, and consume generated TypeScript protobuf bindings without introducing a separate client state framework.
The browser becomes the cross-platform user interface while Rust remains responsible for server truth. The trade-off is a separate frontend toolchain, but that separation also keeps presentation changes out of the media process and makes the interface approachable to web contributors.
User fit: Consumers get the complete graphical product; small businesses can adapt focused operational views; open-source contributors can work in a familiar web stack; sysadmins get the same UI on every host; home automation enthusiasts can use direct dashboard clients; advanced users remain free to implement another client against the API.
Make setup AI-assistable without making AI a dependency
KeepPeek should be easy for an AI assistant to configure, but the running recorder should not need an assistant or model. The repository’s KeepPeek Camera Setup Skill turns camera discovery, authentication checks, stream tests, MP4 validation, transport selection, and final verification into an explicit, reusable workflow.
The Skill is constrained rather than magical: credentials stay outside the conversation, generated configuration is staged, media is independently validated, and camera-side changes require human approval. Structured configuration, stable capability messages, reason codes, metrics, and focused command-line tools make those steps understandable to both people and assistants.
User fit: Consumers can receive guided setup; small businesses get a repeatable custom deployment process; open-source contributors can improve the workflow; home automation enthusiasts get repeatable integration setup; sysadmins get a documented runbook; power users can automate repeatable experiments without turning the recorder into an AI appliance.
Connecting users to choices
| User | Most relevant choices | What those choices enable |
|---|---|---|
| Software Consumer | First-party Svelte UI, common cameras, MP4, browser decoding, setup Skill | Install, record, review, and diagnose without assembling an integration stack |
| Small Business Security | Stable recording core, encoded-stream inference, open API, protocol plugins | Commission tailored analytics and workflows without making evidence depend on the model |
| Open Source Enthusiast | AGPL server, MIT API, documented WebRTC, replaceable external services | Reuse, inspect, replace, and improve each component |
| Home Automation Enthusiast | Stable identities, normalized events, Home Assistant, MQTT, HAP, Prometheus | Connect cameras to local dashboards, alerts, and automations without proxying media |
| Residential Sysadmin | Rust, small core, native packaging, Docker, health evidence, standard MP4 | Operate and recover a predictable service using familiar infrastructure |
| Residential Power Optimization Engineer | Encoded forwarding, external inference, browser codecs, flexible placement | Put storage, decoding, inference, and integrations on the most suitable hardware |
Boundaries and non-goals
- KeepPeek is not a model host, training environment, detector marketplace, or GPU manager.
- KeepPeek does not expose periodic server-generated JPEGs as its native inference API.
- Camera-native analytics events must work without an additional assisted AI service.
- Recording and ordinary viewing must not require camera AI, an external AI service, or a cloud service.
- A protocol plugin does not receive direct access to internal databases, recording paths, or camera credentials.
- The core service does not bundle a video decoder, encoder, codec pack, or desktop browser runtime.
- MP4 storage improves file portability but does not guarantee that every player can decode every camera codec.
- A numeric memory limit is not a published guarantee until representative workloads are measured. Baseline service overhead should remain modest, while total memory also includes the configured encoded pre-event cache and scales with stream count, bitrate, enabled profiles, and retention duration.
- The MIT API permits independent commercial clients and services; it does not exempt private modifications to the AGPL server.
- KeepPeek does not try to replace Home Assistant, an MQTT broker, HomeKit, or a monitoring system.
- AI-assisted setup must remain reproducible by a person and must not expose secrets or silently change camera settings.
Release readiness and known limitations
KeepPeek is in Alpha qualification. POC and MVP gates are complete; Alpha implementation issue closure does not establish the early-adopter matrix or a feature freeze. Passing automated tests is necessary, but it is not enough to call a camera recorder production-ready. The promotion decision belongs to the tested release build and representative deployment recorded in the active Alpha gate #145. The roadmap describes the subsequent release phases.
What automated validation proves
The repository gate builds the Rust workspace and browser application, runs Rust, TypeScript, Svelte, browser, real-media, and Playwright tests, checks formatting and dependencies, and validates the versioned Paper scenario and visual-harness manifests.
Focused benchmarks cover latency and memory budgets for recording coverage, operational events, notifications, MQTT enqueue, storage, and event lookup. Some are ignored tests or standalone Cargo benchmarks and require separate explicit runs; a green ordinary check or CI run does not establish their results. Record the exact benchmark command, build, workload, host, and measurements with the qualification evidence.
Those checks prove deterministic contracts and regression fixtures. They do not prove that a particular camera firmware, network, disk, browser, reverse proxy, notification account, or broker will behave correctly under sustained load.
Paper reference integrity tests check the exported design bundle, scenario identities, assets, dimensions, hashes, and shared tokens. They do not by themselves compare every production page with every Paper artboard. Story-based visual checks and route-level browser checks cover their declared fixtures. Treat the accepted mobile Keep proposal as a design reference and verify the implemented workflow on the target device; a reference file is not deployment evidence.
Release and dependency integrity
Cargo.lock is a tracked release input. Direct Cargo requirements name a compatible version, and
repository build, test, benchmark, container, and release commands use --locked. CI runs
cargo audit --deny warnings without advisory exceptions.
The UI and visual harness intentionally do not track JavaScript lockfiles. Every direct Bun
dependency uses an exact version, while transitive dependencies resolve from the public npm
registry on each clean install. CI creates a separate temporary lockfile for each manifest with
dependency scripts disabled, runs bun audit, and rejects high or critical advisories. This policy
detects current ecosystem breakage but does not make JavaScript transitive resolution reproducible
between runs.
The object-detection example intentionally tests the newest compatible Python packages. CI uses
Python 3.12 and resolves requirements.txt through pip-audit without installing the full model
runtime in the audit job.
The embedded CCTV Camera Database comes from the v2.8.0 release archive. The build verifies its
SHA-256 digest before opening the ZIP. Container builds use Bun 1.4.0 and immutable base-image
digests, then CI starts the production image and probes its HTTP listener. A version tag can create
release artifacts only when it points to a commit on main and that exact commit has a successful
push CI run.
Representative deployment matrix
Before promotion, record exact evidence for the deployment that will rely on KeepPeek:
- at least one direct RTSP or ONVIF camera and the established native camera path;
- main and sub streams, including browser-compatible and incompatible codec profiles;
- desktop and mobile browser workflows;
- local access and remote access through the intended VPN or reverse proxy;
- healthy, stale, reconnecting, recording-failed, storage-pressure, and recovered states;
- events with and without images, revised events, and a dense event day;
- Pushover success, failure, and retry plus MQTT success, outage, and recovery;
- normal, partial, failed, cancelled, retried, and independently decoded exports.
This matrix documents tested configurations. It is not a universal camera, browser, broker, or provider support claim.
Continuous recording soak
Run the final release build long enough to cross normal segment finalization, catalog maintenance, camera reconnect, provider retry, and storage cleanup cycles. During the soak:
- account for every unexpected recording gap with camera, writer, storage, or catalog evidence;
- verify primary recorded playback remains at source rate while timelines refresh and exports run;
- monitor memory, recording databases, thumbnails, export jobs, the in-memory notification outbox, MQTT outbox, logs, threads or tasks, file descriptors, sessions, and browser object URLs for bounded growth;
- restart and verify that notification runtime state, the MQTT outbox and deduplication history, access audit/activity, and active sessions reset; credentials and grants, durable operational events, export jobs, and recording catalog state recover according to their documented contracts;
- rerun the complete workflow without manual database or recording-file edits.
Stop qualification on any silent recording-loss path, remote authentication bypass, secret leak, indefinitely running job, or open release-blocking defect.
MQTT restart rehearsal
Use a disposable broker and synthetic event on an isolated installation. Interrupt broker access,
publish the event, and observe the pending count before restarting KeepPeek. After restart,
verify that the pending outbox is empty. Restore broker access and confirm that the old pending
publication is not replayed. Emit a new event and verify delivery and consumer deduplication by
(instance_id, event_id, revision). Confirm that the original durable event is still available
in KeepPeek and that recording continued independently. QoS does not make this outbox durable;
downstream recovery must not assume replay across a KeepPeek restart.
Current limitations
Camera support is evidence-based
Discovery does not prove authentication, stable media, keyframes, or valid recordings. Validate
each requested stream through a finalized MP4 and independent decode before relying on it. Prefer
reo-proto over TCP for a Reolink camera only after that path passes; use RTSP over TCP for a
generic ONVIF camera unless measured evidence supports another transport.
Browser codec support varies
KeepPeek stores camera media without re-encoding it. A browser may therefore record or index a main stream that it cannot decode. Keep a broadly compatible H.264 substream for live view and review, and treat a truthful incompatibility message as different from missing footage. Automatic transcoding remains separate work.
Configuration bundles do not archive recording media
KeepPeek provides direct ZIP export and validated application of config.toml and plaintext
secrets.toml, with restart activation and automatic startup recovery. recordings.db, recording MP4s, and thumbnail
JPEGs remain a separate archive responsibility. A recovery rehearsal must test both the sensitive
KeepPeek configuration bundle and the recording archive. See
Backup and restore and the
recording archive recovery procedure. There is no public importer
for an unrelated recording archive, no general catalog downgrade command, and no automatic adoption
of unknown MP4s. A validated index rebuild applies only to eligible existing catalog recordings.
Restart resets runtime work
Notification inbox/history, retry work and delivery counters; MQTT pending publications and deduplication; and access sessions, audit history and last-use activity are not durable. Restart does not replay those queues. Capture required operational evidence before restarting and verify the intended behavior afterward. Durable recording events and settings have separate recovery contracts; see What survives a restart.
Maintenance has filesystem and recovery boundaries
Manual maintenance is destructive and uses confirmed whole-recording selections. Native Windows mutation targets NTFS with persistent ACLs; ReFS is rejected. Structural container inspection does not certify complete media decodability or immutable content. Unknown files are not adopted or deleted automatically. Review Recording maintenance for cooperative deadlines, same-account interference limits, interrupted-work handling, and unqualified scenarios.
Access roles are intentionally fixed
Remote access supports Administrator and User; custom roles are not available. Administrators can restrict a User credential to selected camera groups and individual cameras in Settings → Access & roles → User access. The server enforces these grants for media, events, coverage, and camera controls. Dashboard audiences remain separate and do not grant camera access. Saving a grant change closes the affected User’s existing sessions; stale saves retain the draft and report a conflict. The default grant includes all current and future cameras.
Trusted-local clients remain Administrators. Exclude an address from access.local_networks and
require a User credential when that client needs camera restrictions. Hidden navigation is not
an authorization boundary. See Camera and dashboard access.
An access change during dashboard initialization can leave an expired-session error visible until the page is reloaded. The invalid session remains denied by the server. Follow the reconnect steps and verify the new grants after sign-in; this does not establish that the UI race has been resolved.
Detection remains external
KeepPeek records and presents camera-native or externally published events. It does not own object, face, or license-plate detection, model management, zones, masks, or training. Recording and health must remain useful when an external analysis service is absent.
Promotion record
The owner records the exact release commit, platform and browser versions, camera and codec matrix, network topology, storage configuration, test commands, soak duration, observed limits, known workarounds, and a promote or reject decision. Unverified items stay explicit; a green build never silently checks a deployment-specific criterion.
Use a record with the following concrete results:
| Evidence | Record |
|---|---|
| Build and deployment | Exact version/commit or image digest, host architecture, filesystem, service identity, browser versions, and proxy/VPN topology. |
| Camera matrix | Camera model and firmware, transport, main/sub codecs, finalized recording and independent decode results. |
| Access | Allowed and denied camera checks, dashboard audiences, expired/revoked sessions, and local versus remote classification. |
| Sustained operation | Start/end times, workload, recording gaps and explanations, memory/storage growth, cleanup and reconnect results. |
| Recovery | Configuration ZIP apply, stopped catalog/media copy, restart, upgrade rehearsal, and recovered/missing intervals. |
| Integrations and evidence | Provider/broker outage and restart behavior, completed/failed/cancelled exports, and verified downloaded bytes. |
| Decision | Remaining issues and workarounds, evidence location, and an explicit promote or reject decision. |
Use Upgrades and migrations to plan a version change, and Reporting bugs for reproducible failures. This guide does not close the Alpha gate, its feature-freeze decision, or the installation’s long-running qualification work.
Reporting bugs
GitHub Issues are for reproducible KeepPeek defects. The
bug report form is the canonical
submission path, and
.github/ISSUE_TEMPLATE/bug.yml
is the source of truth for its required fields.
Choose the right channel
| What you need | Where to go |
|---|---|
| Existing KeepPeek behavior reproducibly fails | Bug report |
| Setup help, troubleshooting, or usage guidance | Q&A |
| New behavior or a feature proposal | Suggestions |
| A security vulnerability | Private security report |
A support request is not yet a bug until the evidence shows existing KeepPeek behavior violating its contract. Feature requests belong in Suggestions even when they would solve a real problem. Search open and closed Issues before filing so evidence and discussion stay on one report.
Before submitting
Confirm all of the following:
- The same defect is not already present in an open or closed Issue.
- The report describes a reproducible KeepPeek defect, not setup help or a feature proposal.
- The defect reproduces on the current release, or the exact affected build is known.
- Relevant Health, camera diagnosis, server log, and browser console evidence has been checked.
- Credentials, private addresses, tokens, cookies, private media, and sensitive data are removed.
- A KeepPeek diagnostics package is attached, or the report explains why it is unavailable.
Reports that are duplicates, support requests, feature proposals, or missing the required
reproduction and environment details may be closed or marked needs more info.
Describe one observable defect
Keep one report focused on one failure. State what fails, when it fails, and the effect on recording, review, evidence, security, or operation. Include the affected camera, stream, time range, or workflow only in sanitized form.
Select the primary affected area in the form:
- Recording and ingest
- Peek and live video
- Events, search, and previews
- Keep, timeline, and recorded playback
- Export
- Cameras, discovery, and configuration
- Camera controls and vendor integration
- Storage, retention, backup, and restore
- Authentication, authorization, and privacy
- Installation, upgrade, and service lifecycle
- API, WebRTC, and external integrations
- UI, mobile, and accessibility
- Other
If several areas are involved, select the user-visible workflow that fails first and describe the secondary effects in the report.
Provide a deterministic reproduction
Start from a known state and give the smallest sequence that reproduces the defect:
Starting state: <configuration, page, camera state, or recording state>
1. Configure or open ...
2. Select ...
3. Perform ...
4. Observe ...
Frequency: <always, intermittent, or N of M attempts>
Separate expected and actual behavior. Make the difference observable through a state, response, error, file, timing, resource measurement, or visible result. “It does not work” is not enough to reproduce or verify a fix.
Record regression and impact
State whether the behavior previously worked. Include the last known good and first known bad versions when known, how frequently the defect occurs, its operational or data-loss risk, and any verified workaround.
Use unknown when a relevant fact cannot be established. Do not guess a regression version or
claim data loss without evidence.
Describe the environment
Include every field relevant to reproduction, using N/A only when it genuinely does not apply:
- KeepPeek version, commit, and build profile
- operating system and architecture
- installation method: native package, Docker, service, or other
- browser or client and version
- camera make, model, and anonymized firmware version
- camera source: Reolink, ONVIF, RTSP,
test-camera, or other - stream codec, resolution, frame rate, and audio
- camera count and relevant catalog or date-range scale
- storage type and free space
Hardware-specific and intermittent bugs are valid, but the report must distinguish direct evidence from a suspected cause and provide a bounded way to collect the next observation.
Attach sanitized evidence
After reproducing the defect, open Settings > Logs & diagnostics > Download diagnostics. Attach
the generated keeppeek-diagnostics-*.json.gz file under Diagnostics package and sanitized
evidence so the package contains the relevant retained server and browser logs.
If the package cannot be generated or attached, state the exact reason. Missing diagnostics do not
block submission, but a report without enough evidence may be closed as needs more info.
Add focused timestamps, screenshots, stack traces, protocol fixtures, or small log excerpts when they make the defect easier to reproduce. Never publish:
- credentials, access keys, tokens, cookies, or authorization headers;
- camera IP addresses, hostnames, private filesystem prefixes, or stream and snapshot URLs;
- faces, license plates, private recordings, notification text, or location details;
- unsanitized logs, screenshots, packet captures, or diagnostics bundles.
Redact at the source. Do not attach evidence that cannot be confidently sanitized.
Define fix acceptance criteria
Describe the observable behavior that will make the bug fixed. Keep each criterion unchecked and name the test or procedure that will verify it:
- [ ] AC-1: <the reported reproduction produces the correct observable outcome>
- Expected outcome: <exact state or result>
- Verification: <regression test, command, or reproducible procedure>
- [ ] AC-2: <the relevant failure or boundary case behaves correctly>
- Expected outcome: <exact state or result>
- Verification: <test name, command, or reproducible procedure>
Acceptance criteria should describe corrected behavior, not prescribe an unverified implementation.
Include performance evidence
When the defect affects runtime, build time, memory, storage, network traffic, or interaction latency, include:
- metric and unit;
- current or before value;
- expected budget or target;
- workload or dataset;
- hardware, operating system, browser, and build profile where relevant;
- exact command or harness;
- run count and statistic, such as median or p95.
For a correctness-only bug, write Performance: N/A and explain which performance paths the change
cannot affect. A statement such as “slow” or “no regression” is not performance evidence.
Submit and follow up
Review the complete report once more for secrets and unsupported claims, then submit it through the bug report form. Respond to requests for focused evidence or clarification on the same Issue rather than opening duplicates.
A strong report lets another person answer what failed, how to reproduce it, what should have happened, which build and environment are affected, what evidence proves it, and how the eventual fix will be verified.
Open source and licensing
KeepPeek is built in an ecosystem shaped by other open-source camera projects and protocol libraries. This chapter gives those projects credit, explains where KeepPeek made different product choices, and documents the license boundary between the server and its public API.
Different products, different strengths
Scrypted and Frigate are great products with strong, distinct designs. KeepPeek is not an argument that those designs are wrong. Their choices may fit another user, home, or business better.
| Project | Distinct design center | A strong fit when |
|---|---|---|
| Scrypted | A high-performance video integration platform and NVR with a rich plugin model, smart detections, and low-latency connections to HomeKit, Google Home, and Alexa | Broad camera and smart-home integration, an extensible plugin ecosystem, and first-class consumer-platform bridges should live in one system |
| Frigate | A local NVR built around real-time AI object detection, accelerator-aware processing, zones, focused event review, Home Assistant, and MQTT | Detection is a central NVR responsibility and the user wants an integrated, highly configurable local AI and home-automation experience |
| KeepPeek | A focused recorder and WebRTC media gateway that accepts camera events and keeps optional inference, transcoding, and ecosystem bridges outside the core | Recording should remain independent from analytics, and viewers or services should compose through an open media and event boundary |
Thank you to the Scrypted and Frigate maintainers and communities. Their work has advanced local camera software, demonstrated what polished self-hosted systems can do, and made the trade-offs in this space much easier to understand.
Code foundations
KeepPeek depends on substantial protocol work from other Rust projects. The local forks retain their upstream authorship and licenses; carrying a fork is a maintenance decision, not a claim that KeepPeek created the original work.
- Retina by Scott Lamb and its contributors provides the high-level RTSP client, RTP depacketization, codec framing, and much of the interoperability groundwork used by KeepPeek. KeepPeek carries a local Retina fork for its camera, recording, and compatibility needs.
- ONVIF-rs by Chris Bruce, Lumeo, and its contributors did the difficult work of making ONVIF discovery, schemas, authentication, and camera operations usable from Rust. KeepPeek carries a local ONVIF-rs fork and continues that work for its supported camera surface.
- str0m by Martin Algesten and its contributors is the upstream WebRTC implementation used by KeepPeek. It is the best Rust WebRTC implementation: its Sans I/O design, explicit time and network inputs, RTP and frame APIs, data channels, and lack of a hidden async runtime fit a focused media service especially well.
KeepPeek would be significantly harder to build without these projects. Thank you to every upstream author, maintainer, reviewer, and contributor whose work remains in the code.
Licensing model
KeepPeek uses two licenses at a deliberate protocol boundary:
| Area | License | Purpose |
|---|---|---|
| Server, first-party viewer, and most repository code | AGPL-3.0-only | Keep the shared NVR and media-gateway core open, including modified versions operated over a network |
Public definitions and documentation under api/, plus bindings generated solely from them | MIT | Let independent clients and services implement the protocol without adopting the server’s license |
The repository root license contains the
complete KeepPeek licensing notice and AGPL terms. The api/ directory has its own
MIT license.
Why the server uses AGPL
The server is a network service, so the AGPL is the appropriate copyleft boundary for the shared core. The intent is that users can inspect, modify, self-host, and retain access to the source of the KeepPeek server they use, including modified versions offered over a network.
KeepPeek’s NVR and first-party viewer remain open source and free to use without a camera-count limit. The license protects that common foundation from becoming a private server fork whose users cannot inspect the code serving them.
Why the API uses MIT
An open server is most useful when other software can talk to it without inheriting the server’s
implementation choices. The public HTTP, WebRTC, protobuf, and SDP contracts under api/ therefore
use MIT. Bindings generated solely from those definitions remain covered by that directory’s MIT
license.
Implementing or using the public protocol does not, by itself, impose KeepPeek’s repository-wide AGPL license on an independent client or service. A service can choose the language, WebRTC implementation, deployment model, and license appropriate to its users.
Free and paid protocol services
This split creates a protocol-plugin model rather than an in-process plugin ABI. A free, community, or paid service can run as a separate process and subscribe or publish through the documented API. That service may be open or closed source without receiving a private extension point inside the KeepPeek server.
The boundary allows commercial integrations to fund specialized inference, transcoding, business workflows, viewers, or support while the recorder and media gateway remain shared open-source infrastructure. Camera credentials, recording paths, and internal databases remain owned by KeepPeek rather than exposed to an in-process plugin.
What the split means for users
- Software consumers retain a free recorder and first-party viewer, with optional services only when they add value.
- Small businesses can buy tailored services at the API boundary without depending on a private recorder fork.
- Open-source enthusiasts can audit the server, share core improvements, and build permissively licensed clients.
- Home automation enthusiasts can choose community or commercial bridges.
- Self-hosters retain control of the server running on their hardware.
- Service authors can build sustainable offerings without closing the shared core.
See Contributing for how licenses apply to submitted changes.
Contributing
KeepPeek welcomes focused contributions that strengthen recording, camera compatibility, media routing, observability, the public API, and the first-party experience. A contribution should make the supported product more dependable without silently turning the core into a different product.
Read Users and design choices before proposing a substantial change. It describes the users, boundaries, and trade-offs that guide review.
Use Reporting bugs for a reproducible defect. Use GitHub Discussions for setup help, troubleshooting, usage questions, and feature proposals.
Guard the product boundary
The feature set is deliberately and heavily guarded. Any proposal that expands product scope must be discussed and reviewed before implementation begins. A complete implementation is not, by itself, a reason to add a feature to the core.
Feature review asks:
- Does this belong in a focused recorder and media gateway?
- Can an independent service or protocol plugin provide it through the public API instead?
- Which documented users need it, and does it make another user’s core workflow worse?
- What permanent maintenance, security, storage, resource, compatibility, and licensing costs does it add?
- Can the behavior be tested without depending on one private device, account, or cloud service?
Open a GitHub Discussion before writing a feature that changes the product boundary, public protocol, storage model, supported platform contract, or long-term maintenance surface. Expect alternatives and non-goals to be reviewed as seriously as the proposed implementation. This review is how KeepPeek avoids accidental scope creep.
Contributions that fit well
Focused bug fixes, tests, documentation corrections, diagnostics, and interoperability improvements usually have a clear path when they preserve the existing product boundary.
Camera protocols and RTSP interoperability
Camera protocol fixes and bounded RTSP workarounds are especially welcome. They are often easier to submit and review than feature proposals because the expected behavior can be captured in a fixture and regression test.
A useful camera contribution includes:
- manufacturer, model, firmware version, backend, and TCP or UDP transport;
- the smallest reproducible failure with credentials, addresses, and private media removed;
- sanitized protocol, SDP, or packet evidence when it can be shared legally and safely;
- a regression fixture or deterministic fake-camera case;
- a narrow parser, transport, or adapter fix instead of a product-wide brand check;
- successful main/sub frame, keyframe, MP4 finalization, and independent decode validation when the change affects recording.
Never submit camera passwords, access keys, private stream URLs, identifiable recordings, or packet captures containing secrets. If private evidence cannot be published, file the bug with only the safe evidence and explain what is unavailable rather than attempting a speculative workaround.
One camera quirk should not weaken validation for every camera. Prefer a protocol-level correction when the device is standards-compliant, and a bounded, evidence-backed compatibility rule when it is not.
Public API and external services
API contributions should preserve typed messages, stable identity, explicit capabilities, bounded queues, and clear failure behavior. An example client or service should consume the canonical API definitions rather than copy handwritten wire types or depend on private server internals.
New detector, transcoder, automation, and commercial-service ideas generally belong outside the core first. A small reference implementation may demonstrate interoperability without becoming a promise that KeepPeek will own that service as a product.
AI-assisted contributions
AI is one of the tools used to develop KeepPeek. This section does not take a position on AI in general; it defines only how AI-assisted contributions to this repository are reviewed.
AI-assisted contributions are acceptable. The standard is the same as for code written without an assistant, and responsibility never transfers to the model.
The person submitting the pull request is its first reviewer. Before requesting another review, they must:
- inspect every changed line, including generated tests and configuration;
- understand and be able to explain every line in the pull request;
- verify API, security, resource, failure, and compatibility assumptions against the repository;
- remove invented behavior, unnecessary abstractions, unrelated cleanup, and generated commentary;
- run the required validation and investigate failures rather than asking reviewers to debug raw model output.
“The AI wrote it” is not an explanation for a design decision and is not a reason to merge code the submitter cannot defend. Reviewers may ask the submitter to explain control flow, ownership, protocol behavior, tests, or individual lines. If they cannot, the contribution is not ready.
Pull request shape
Use the repository’s pull request template as the canonical shape for every pull request. Complete its summary, linked Discussion, acceptance criteria verification, performance evidence, validation, and review evidence before requesting review. A bug-fix pull request may link its reproducible bug issue instead of a feature Discussion.
Keep pull requests small enough to review as one coherent behavioral change. State the supported user and boundary affected, include focused regression coverage, update public documentation when a contract changes, and leave unrelated formatting, dependency updates, and refactors out.
Use draft pull requests for early feedback when the boundary is clear but the implementation is not ready. Do not use a large completed pull request as the first request for product-scope review.
Review cadence
Pull request reviews are done on Sundays. If a Sunday review window passes and a pull request is missed, leave a short, polite ping on that pull request to remind the maintainer. A reminder on the existing pull request is enough; do not open a second Discussion or duplicate pull request.
Development environment
Repository development uses stable Rust, Bun 1.4.0, and Python 3.12. The pinned Bun and Python
versions live in ui/.bun-version and examples/object_detection_service/.python-version.
Formatting and validation require the Python packages in
examples/object_detection_service/requirements.txt, including Black. The root fix and check
scripts use an absolute interpreter path from KEEPPEEK_PYTHON when set. Otherwise, macOS/Linux
scripts try python3.12 and then python3; Windows scripts resolve python from PATH. The
selected interpreter must be Python 3.12. Set KEEPPEEK_PYTHON explicitly if another Python
installation wins that lookup.
Do not use a Python virtual environment. fix.sh and fix.bat install the requirements into the
resolved interpreter, retrying with --break-system-packages when that interpreter is externally
managed.
The canonical checks also require Bun, cargo-nextest, and cargo-machete. Install UI dependencies
with Bun from the public npm registry and do not create or commit an npm, pnpm, or Yarn lockfile.
Validation
Run the canonical repository check from the repository root before requesting review:
- macOS or Linux:
./check.sh - Windows:
.\check.bat
Run the same gate with opt-in slow Rust test bodies before submitting a pull request:
- macOS or Linux:
KEEPPEEK_RUN_SLOW_TESTS=1 ./check.sh - Windows PowerShell:
$env:KEEPPEEK_RUN_SLOW_TESTS = '1'; .\check.bat
Pull-request CI keeps feedback fast by splitting static UI checks, unit tests, coverage, and Linux
Playwright tests into non-overlapping jobs. The extended Main workflow runs after every merge and
once per hour on main; it owns the opt-in slow Rust tests, ARM builds and tests, Windows installer
lifecycles, Rust coverage and CodeQL, and full Playwright suites on macOS and Windows.
Windows pull-request core tests run only the runtime-profile regressions and native volume-root
validation. Windows pull-request durable-state tests retain ordinary regressions and defer
scale/quota/churn qualification. The full Windows core and durable-state suites run on every push to
main in the CI workflow. Linux and macOS still run the full core suite before merge, and local
canonical checks still run the complete suite. This split changes scheduling, not test assertions or
the required post-merge Windows coverage.
During development, run the narrowest relevant test first. The final check covers repository formatting, Rust tests and lints, frontend checks, and the supported test suites. Document any test that cannot run in the pull request and explain why.
Camera changes should also be validated against the affected device and independent MP4 tooling. Passing a unit test does not prove that a real camera authenticates, advances frames and keyframes, finalizes recordings, and produces decodable output.
Update the book and design evidence
The book lives in book/src; SUMMARY.md controls its navigation. Document the current behavior
and the practical task it enables, including authorization, persistence, limits, and failure
recovery. Update the feature guide when adding a screen or changing where a
task starts. Fill operational chapters with instructions rather than leaving empty placeholders.
Repository Markdown is the only editing source for the
Wiki and the
mdBook site. The Wiki also includes selected engineering pages
from docs/; Pages remains the generated mdBook mirror. Submit documentation changes through a
repository pull request. Do not edit generated Wiki pages directly.
See Wiki publishing
for page mapping, preview, credentials,
publication status, and recovery.
Keep serialized fields, defaults, and examples synchronized with the configuration reference. Protocol declarations and engineering scenarios can describe capabilities that do not yet have a runtime handler. Check the server and client implementation before presenting those capabilities as available.
From the repository root, validate documentation with:
bun run --cwd ui format:markdown:check
mdbook build book
bun run --cwd ui wiki:test
bun run --cwd ui wiki:check
bun run --cwd ui wiki:build
Install mdbook and mdbook-mermaid and make both available on PATH; the book’s configuration
uses the Mermaid preprocessor. After building, open the generated book, follow the changed
navigation and cross-links, and check diagrams and long tables at desktop and narrow widths.
These focused documentation checks complement the repository validation above.
The first-party interface has versioned Paper references and scenario-based browser evidence under
ui/design/paper. Use the active reference’s
README and manifests to identify its scope. The active NVR design is keeppeek-nvr-alpha;
keeppeek-nvr-v34 remains the historical input to the existing visual harness and Linux Loki
baselines. A new Paper export does not approve a replacement baseline. A reference image records design intent; browser
interaction tests prove working behavior. Keep unavailable, loading, denied, empty, and error
states explicit, and preserve the actual evidence when updating a reference. Do not infer current
feature availability from a mockup or a demonstration video.
Licensing
See Open source and licensing for the project-wide license model. Contributions are accepted under the license of the area they modify. Submit only work you have the right to provide under that license, including fixtures, generated files, and AI-assisted output.