Backend
The Go server (nvrd).
Backend
This document describes the Go backend. The backend is the Go binary called nvrd. It is in the server/ directory.
Overview
nvrd is a Go module at github.com/nvr/nvr/server. It is a modular monolith. It provides the REST API, the WebSocket, camera management, the recording index, the event system, and the background jobs.
The backend is the center of the system. It does not process media bytes. It configures MediaMTX and runs FFmpeg as a tool.
The entry point
The main entry point is server/cmd/nvrd/main.go. The main function does this sequence:
- Creates a root context for the interrupt and termination signals.
- Loads the configuration.
- Sets up the JSON logger.
- Creates the data and recordings directories.
- Opens the SQLite database and applies the migrations.
- Creates the domain services.
- Reads the settings that override the runtime configuration.
- Creates the API server and the HTTP router.
- Registers the MediaMTX hooks and the WebSocket.
- Registers the generated OpenAPI routes.
- Serves the embedded dashboard.
- Starts the HTTP server.
- On shutdown, stops the scheduler and the HTTP server.
The configuration
The configuration comes from environment variables. The package is server/internal/config. There is no configuration file. The Load function reads the variables and returns a Config value.
The configuration variables are:
| Variable | Purpose | Default |
|---|---|---|
NVR_HTTP_ADDR |
The HTTP listen address. | :8080 |
NVR_DATA_DIR |
The data directory for the database. | ./data |
NVR_RECORDINGS_DIR |
The recordings directory. | ./recordings |
NVR_SECRETS_KEY |
The encryption key for secrets. | empty |
NVR_LOG_LEVEL |
The log level. | info |
NVR_MEDIAMTX_API_URL |
The MediaMTX control API URL. | http://127.0.0.1:9997 |
NVR_MEDIAMTX_WEBRTC_URL |
The MediaMTX WebRTC URL. | http://127.0.0.1:8889 |
NVR_MEDIAMTX_HLS_URL |
The MediaMTX HLS URL. | http://127.0.0.1:8888 |
NVR_MEDIAMTX_PLAYBACK_URL |
The MediaMTX playback URL. | empty |
NVR_RETENTION_DAYS |
The recording retention period. | 7 |
NVR_GOOGLE_CLIENT_ID |
The Google OAuth client ID. | empty |
NVR_GOOGLE_CLIENT_SECRET |
The Google OAuth client secret. | empty |
NVR_GOOGLE_REDIRECT_URL |
The Google OAuth redirect URL. | empty |
The backend stores some settings in the database. These settings override the environment variables. The settings are the recordings directory, the recording enabled flag, and the retention period.
The service packages
The packages are in server/internal/. Each package has a narrow public surface.
internal/api
The API package is the HTTP adapter layer. It provides the REST handlers, the middleware, and the WebSocket.
The main file is server/internal/api/server.go. It defines the Server type. The Server holds all the domain services.
The handler files are:
handlers.go: health, version, and authentication.cameras.go: camera CRUD, live, snapshot, and probe.recordings.go: recording list and playback.events.go: event list and acknowledge.settings.go: settings get and update.system.go: disk and status.users.go: user list, create, and delete.storage_gdrive.go: Google Drive administration.
The middleware is in middleware.go. The SessionMiddleware resolves the session token and attaches the user to the request context. The CORSMiddleware allows local development origins.
The authorization helpers are in authz.go. They are requireUser, requireOperator, and requireAdmin.
The WebSocket is in ws.go. Refer to api for the WebSocket details.
internal/auth
The auth package manages authentication and roles. It provides password hashing, session tokens, and role constants.
The password functions use Argon2id. Refer to password.go.
The session functions are in session.go. A session token is 32 random bytes. The backend stores only the SHA-256 hash of the token. The session TTL is 30 days.
The cookie functions are in cookie.go. The cookie name is nvr_session. The cookie is HttpOnly, Path=/, SameSite=Lax, and Secure=false. The backend also accepts the Authorization: Bearer header and the X-Session-Token header.
The service is in service.go. It creates sessions, resolves users from tokens, and deletes sessions.
The roles are admin, operator, and viewer. On first boot, the setup endpoint creates the first user as admin.
internal/camera
The camera package manages cameras and stream profiles. It provides camera CRUD, encrypted credentials, and stream probing.
The service is in service.go. It lists, gets, creates, updates, and deletes cameras. It manages the stream profiles. It encrypts the camera passwords.
The driver is in driver.go. The Driver type probes a camera. The GenericRTSPDriver in generic_rtsp.go uses ffprobe to get the camera stream metadata.
The probing result includes the codec and resolution. It flags H.265/HEVC streams.
internal/media
The media package is the MediaMTX client. It is the only package that knows MediaMTX exists.
The service is in service.go. It issues live and playback streams. It mints short-lived tokens. It manages the MediaMTX paths.
The MediaMTX client is in mediamtx.go. It uses the MediaMTX v3 API to upsert and delete paths. A path is named cam_<uuid-without-dashes>.
The token store is in tokens.go. Tokens are path-bound and held in memory. They are reusable until expiry.
The auth handler is in auth.go. It accepts the MediaMTX external-auth callback and validates the token.
The snapshot function is in snapshot.go. It runs FFmpeg to capture one JPEG frame.
internal/recording
The recording package owns the segment index. It indexes completed segments, answers timeline queries, and enforces retention.
The service is in service.go. It lists recordings, indexes segments, and prunes old rows. The retention period defaults to 7 days.
The segment-complete handler is in hook.go. It accepts the MediaMTX callback and indexes the segment. It tolerates JSON or form data with varied field names.
The path helper is in path.go. It converts a MediaMTX path name to a camera ID.
There is no clip-export function. Playback is a short-lived MediaMTX URL.
internal/event
The event package persists events and publishes them to subscribers.
The bus is in bus.go. Subscribers receive buffered channels. A slow subscriber can lose events.
The service is in service.go. It emits, gets, lists, and acknowledges events. The event types are camera online, camera offline, disk low, and archive complete.
internal/jobs
The jobs package is the cron scheduler. It is not a durable queue.
The scheduler is in scheduler.go. It uses robfig/cron. The jobs are:
- Camera health probes every minute.
- Retention prune every hour.
- Disk check every five minutes.
- Google Drive archive at midnight UTC.
The archive job is in archive_gdrive.go.
There is no thumbnail generator.
internal/notify
The notify package is a placeholder. It has only a package comment. There is no notifier implementation.
internal/secrets
The secrets package provides encryption at rest. It uses AES-256-GCM. The key is the SHA-256 of the secrets key. Without a key, values are stored as plaintext for development.
internal/storage
The storage package is the archive seam. It defines the Provider interface for object storage. It has Put, Get, Delete, List, and PresignURL.
There is no local or S3 implementation. Google Drive is the concrete archive tier in internal/storage/gdrive.
The recording index adapter is in recording_index.go. It connects the recording service to the Drive archive.
internal/storage/gdrive
The gdrive package is the Google Drive archive. It manages the OAuth lifecycle and the archive uploads.
The service is in service.go. It validates configuration, begins OAuth, handles the callback, and disconnects.
The archive functions are in archive.go. The ArchivePending function is single-flight. It uploads older unarchived rows in batches. It marks missing files as skipped.
The upload function is in upload.go. It creates files in the NVR Archives folder. It uses the nvr_recording_id property for idempotent retries.
internal/store
The store package is the SQLite data layer. It opens the database, applies migrations, and provides the generated queries.
The database is opened in open.go. It uses modernc.org/sqlite. It enables WAL, foreign keys, and a busy timeout. It sets one maximum open connection.
The migrations are applied in migrate.go. Refer to database for the schema.
The queries are generated by sqlc. The query files are in server/internal/store/queries/.
internal/ui
The ui package embeds the dashboard. The Handler function serves the built dashboard files. The /api/ paths return 404. Unknown routes serve index.html.
internal/deps
The deps package pins libraries in go.mod with blank imports. It has no runtime API. Some pinned libraries have no implementation yet.
The routers and hooks
The backend registers three kinds of routes:
- The generated OpenAPI routes under
/api/v1. - The WebSocket at
/api/v1/ws. - The MediaMTX hooks at
/internal/mediamtx/authand/internal/mediamtx/segment-complete.
The MediaMTX hooks are internal. They are not part of the public API.
Build and code generation
The backend uses two code generators:
oapi-codegengenerates the server stubs from the OpenAPI contract.sqlcgenerates the database queries.
The commands are in server/Makefile. The make generate command runs both generators.
Refer to api for the API contract and to development for the build commands.