Skip to content

nbsp1221/mediavault

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

206 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Mediavault

Self-hosted encrypted media streaming vault for browsing, uploading, and protecting personal media through a web interface. Built with React Router v7 and pure Bun runtime.

Features

  • 🎬 Stream local media files through a web browser
  • πŸ” SQLite-backed account login with httpOnly site sessions
  • πŸ“ Browser-first upload with staged commit into the library
  • πŸ”’ Protected DASH playback with JWT tokens, ClearKey, and encrypted media packaging
  • 🎨 YouTube-inspired UI for video browsing
  • ⚑ Pure Bun runtime for maximum performance

Getting Started

Development

# Install dependencies
bun install

# Create local environment config
cp .env.example .env

# Start development server
bun run dev

Access at http://localhost:5173

Create the first local login account after the server starts by enabling the operator-only admin API for bootstrap and calling it with a bearer token:

MEDIAVAULT_ADMIN_API_MODE=bootstrap \
MEDIAVAULT_ADMIN_API_TOKEN=dev-admin-token \
MEDIAVAULT_DATABASE_ENCRYPTION_KEY=dev-database-key \
bun run dev

curl -fsS \
  -H "Authorization: Bearer dev-admin-token" \
  -H "Content-Type: application/json" \
  -d '{"username":"owner","password":"vault-password"}' \
  http://localhost:5173/api/admin/users

bun run dev is for trusted local development only. Do not expose the Vite development server through a public tunnel, reverse proxy, or untrusted LAN. Use bun run build and bun run start, or the Docker production image, for deployment. When MEDIAVAULT_STORAGE_DIR is not set, development runtime storage defaults to a checkout-specific directory outside this repository so Vite cannot serve runtime media, keys, or SQLite data from the project root.

Production

# Build application
bun run build

# Start production server
bun start

Create login accounts through the operator-only admin API. For first-run production bootstrap, set MEDIAVAULT_ADMIN_API_MODE=bootstrap and MEDIAVAULT_ADMIN_API_TOKEN to a deployment-specific random value, start the server, and call POST /api/admin/users with Authorization: Bearer <token>. Set MEDIAVAULT_DATABASE_ENCRYPTION_KEY before starting the app so the primary SQLite database can be opened. For protected playback routes, set MEDIAVAULT_PLAYBACK_JWT_SECRET. For ingest and encrypted playback packaging, also set MEDIAVAULT_MEDIA_KEY_DERIVATION_SECRET. For signed client identity cookies, set MEDIAVAULT_AUTH_CLIENT_COOKIE_SECRET. When NODE_ENV=production, Mediavault runs a startup preflight and refuses to start without the full vault requirements: at least one auth account in the primary SQLite database or a bootstrap admin API token, MEDIAVAULT_PLAYBACK_JWT_SECRET, MEDIAVAULT_MEDIA_KEY_DERIVATION_SECRET, MEDIAVAULT_AUTH_CLIENT_COOKIE_SECRET, MEDIAVAULT_DATABASE_ENCRYPTION_KEY, and usable configured storage.

Docker Deployment

Published GHCR Image

Mediavault publishes production images to GitHub Container Registry after verified main pushes. Pulling and restarting the service on a host is an operator-owned deployment step.

docker pull ghcr.io/nbsp1221/mediavault:latest

The current checked-in docker-compose.yaml remains a local source-build path. Do not use docker compose pull as the GHCR image update path until a future Compose migration changes that file to consume the published image.

Local Source-Build Compose

# Start the application
docker-compose up -d

# Access at http://localhost:3000

For remote browser use, run the container behind an HTTPS reverse proxy. Production auth cookies are secure cookies, so plain remote HTTP is not a complete production deployment. Reverse proxy routing, TLS certificates, firewall rules, and public port exposure are operator-owned infrastructure responsibilities.

Features

βœ… Pure Bun runtime - Fast, modern JavaScript runtime
βœ… Security hardened - Non-root user, minimal capabilities
βœ… Health monitoring - Auto-restart on failure
βœ… Persistent storage - Data and videos preserved

Docker healthchecks use GET /health/ready. The endpoint returns 204 No Content only when the production full-vault readiness checks pass, and 503 Service Unavailable with no diagnostic body when they fail. Detailed causes are written to container logs.

Volumes

The app writes to /app/storage inside the container.

By default, Docker Compose backs that path with the named volume mediavault-storage. If you want a host-visible bind mount, edit docker-compose.yaml and replace the named volume with a path such as ./storage:/app/storage.

  • named volume: Docker-managed storage with fewer host permission issues
  • bind mount: host-visible files, but host ownership and write permissions matter

Storage layout:

storage/db.sqlite
storage/videos/
storage/staging/

The production image provisions FFmpeg, ffprobe, and Shaka Packager for browser upload commit and encrypted DASH packaging.

Commands

# Start
docker-compose up -d

# View logs
docker-compose logs -f

# Stop
docker-compose down

Environment

Create .env file before starting the app:

cp .env.example .env

Required for the full vault feature set:

  • MEDIAVAULT_DATABASE_ENCRYPTION_KEY: encryption key for the primary SQLite database at rest
  • MEDIAVAULT_PLAYBACK_JWT_SECRET: signing secret for protected playback token issuance
  • MEDIAVAULT_MEDIA_KEY_DERIVATION_SECRET: secret used to derive per-video encryption keys
  • MEDIAVAULT_AUTH_CLIENT_COOKIE_SECRET: signing secret for client identity cookies
  • at least one SQLite auth account, or first-run bootstrap through MEDIAVAULT_ADMIN_API_MODE=bootstrap and MEDIAVAULT_ADMIN_API_TOKEN

Generate deployment-specific secret values before starting the full vault path. The database encryption key, media key derivation secret, playback JWT secret, auth client cookie secret, and admin API token are free-form strings, but they should be cryptographically random. They do not need to be hex-encoded.

The app requires the database encryption key before opening the primary SQLite database. In production, all secret values must be present and non-blank before the app starts. Runtime preflight does not score secret strength, so weak values are an operator mistake, not something the app can reliably fix at startup.

openssl rand -base64 32
bun -e "const { randomBytes } = await import('node:crypto'); console.log(randomBytes(32).toString('base64url'))"

Optional:

  • MEDIAVAULT_MEDIA_KEY_DERIVATION_SALT: salt value used during playback key derivation
  • MEDIAVAULT_STORAGE_DIR: override the unified storage root for db.sqlite, committed media, and staged uploads
  • MEDIAVAULT_ADMIN_API_MODE: account management API mode, one of disabled, bootstrap, or always
  • MEDIAVAULT_ADMIN_API_TOKEN: bearer token for the operator-only account management API
  • MEDIAVAULT_AUTH_CLIENT_COOKIE_NAME: override the client identity cookie name
  • MEDIAVAULT_AUTH_SESSION_COOKIE_NAME: override the auth session cookie name
  • MEDIAVAULT_AUTH_SESSION_TTL_MS: session lifetime in milliseconds
  • MEDIAVAULT_AUTH_TRUST_PROXY_HEADERS: trust forwarded client IP headers for rate limiting
  • MEDIAVAULT_AUTH_FAILED_LOGIN_BLOCK_DURATION_MS: failed-login block duration
  • MEDIAVAULT_AUTH_FAILED_LOGIN_DELAY_MS: invalid-login response delay
  • MEDIAVAULT_AUTH_FAILED_LOGIN_WINDOW_MS: failed-login tracking window
  • MEDIAVAULT_AUTH_MAX_FAILED_LOGIN_ATTEMPTS: failed-login threshold before blocking
  • FFMPEG_PATH: override the FFmpeg binary path
  • FFPROBE_PATH: override the ffprobe binary path
  • SHAKA_PACKAGER_PATH: override the Shaka Packager binary path

Notes:

  • Use /login for the site auth flow.
  • Production Docker readiness requires the full vault configuration, writable storage, at least one auth account or bootstrap admin API token, and runnable FFmpeg, ffprobe, and Shaka Packager.
  • In bootstrap mode, POST /api/admin/users is available only until the first account exists. Use always only when you intentionally want ongoing account automation. Rotate the token by updating the environment and restarting the container.
  • Back up MEDIAVAULT_MEDIA_KEY_DERIVATION_SECRET with the storage volume and primary SQLite database. Existing encrypted media depends on preserving that value.
  • Back up MEDIAVAULT_DATABASE_ENCRYPTION_KEY with the primary SQLite database. Existing database contents cannot be opened if that value is lost or changed.
  • MEDIAVAULT_MEDIA_KEY_DERIVATION_SALT is optional. If you customize it, preserve it with the media key derivation secret and storage backup.
  • Video segments use DASH/CENC/ClearKey with a per-video key.bin. Thumbnails use the same per-video key and are stored at thumbnail.jpg as a Mediavault AES-128-GCM envelope; authenticated HTTP responses expose only normal image/jpeg bytes.
  • The default Compose port binding 3000:3000 is for simple reachability. For a hardened deployment, restrict direct HTTP access with firewall rules, bind the port to loopback, or place the app behind a private proxy network.
  • The protected playback path issues /videos/:videoId/token, resolves /videos/:videoId/manifest.mpd, and serves /videos/:videoId/clearkey for the browser license flow.

Architecture And Refactor Context

For the current architecture and repo state, start here:

  • docs/roadmap/current-refactor-status.md
  • docs/roadmap/personal-video-vault-rearchitecture-phases.md
  • docs/architecture/personal-video-vault-target-architecture.md

Technology Stack

  • Frontend: React Router v7 with SSR
  • Runtime: Bun (pure Bun, no Node.js)
  • Styling: TailwindCSS v4
  • Persistence: Primary SQLite database for auth sessions, canonical video metadata, playlists, and ingest state
  • Video: FFmpeg for thumbnails and streaming
  • Streaming: DASH with JWT token issuance, ClearKey license delivery, and encrypted media packaging
  • Thumbnail storage: AES-128-GCM envelope at thumbnail.jpg, decrypted server-side after authentication

Testing

The test suite is split by scope:

  • bun run test:modules: colocated module and policy tests
  • bun run test:integration: route and auth integration tests
  • bun run test:ui-dom: jsdom + React Testing Library component tests
  • bun run vitest:ui: interactive Vitest UI for local debugging only
  • bun run test: all Vitest projects
  • bun run test:runtime:smoke: runtime smoke against the dev server and built Bun server
  • bun run check:fast: quick local check for iteration, not a completion gate
  • bun run check: hermetic lint + typecheck + calibrated coverage + changed-file mutation + runtime smoke
  • bun run check:runtime: check plus required browser smoke and Docker Compose smoke
  • bun run check:docker-compose-smoke: Docker Compose production readiness contract
  • bun run test:e2e:smoke: required browser smoke command

Why the smoke layers exist:

  • Vitest runs in Node for this repo
  • bun run dev and bun run start do not execute route code the same way
  • runtime-only regressions, such as unsupported module schemes, must be caught separately

Runtime smoke verifies that the dev server and built Bun server preserve the critical startup, auth, protected route, token, thumbnail, and owner playlist contracts that ordinary Vitest coverage does not prove.

Browser verification remains a separate step for UI and playback flows. See docs/E2E_TESTING_GUIDE.md.

If Playwright playback fixtures need to be rebuilt for the browser-safe H.264 policy, refresh them with:

bun run backfill:browser-playback-fixtures

The script rebuilds only the known Playwright fixture video IDs and leaves already-compatible manifests untouched.


Built with ❀️ using React Router and Bun.

Releases

No releases published

Packages

 
 
 

Contributors

Languages