pnpmConfigHook only sets up config, doesn't include the pnpm binary 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Jellarr
Jellarr is an open-source tool designed to simplify declarative configuration management for Jellyfin media servers. Inspired by Configarr's approach to *arr stack automation, Jellarr uses Jellyfin's official REST API to safely apply configuration changes through version-controlled YAML files.
By streamlining server configuration, Jellarr saves time, enhances consistency across environments, and reduces manual intervention.
Why Jellarr?
Managing Jellyfin configuration becomes painful at scale:
- Configuration drift across dev/staging/prod environments
- No version control for settings changes
- Manual clicking through web UI doesn't scale to multiple servers
- No automation for configuration-as-code workflows
Existing solutions have limitations. While declarative-jellyfin pioneered declarative Jellyfin configuration, it takes a risky approach:
- Directly manipulates XML files and SQLite databases
- Stops/starts Jellyfin service multiple times during configuration
- Reimplements Jellyfin's internal UUID generation in bash
- NixOS-only with hardcoded systemd and path dependencies
- Breaks when Jellyfin changes internals (example issue)
Jellarr takes a different approach:
- ✅ API-based — Uses Jellyfin's official REST API, never touches internal files or databases
- ✅ Zero service interruption — Jellyfin keeps running, no restarts required
- ✅ Cross-platform — Works on Docker, bare metal, any OS (not just NixOS)
- ✅ Type-safe — OpenAPI-generated types catch errors at build time
- ✅ Future-proof — Relies on stable API contracts, not reverse-engineered internals
- ✅ Production-ready — Idempotent, safe to run anytime via cron/systemd timers
Quick Start
# With Nix
nix run github:venkyr77/jellarr/v0.0.1
# With Docker
docker pull ghcr.io/venkyr77/jellarr:v0.0.1
# Download binary (requires Node.js 24+)
./jellarr-v0.0.1
Example config (config/config.yml):
version: 1
base_url: "http://localhost:8096"
system:
enableMetrics: true
pluginRepositories:
- name: "Jellyfin Official"
url: "https://repo.jellyfin.org/releases/plugin/manifest.json"
enabled: true
encoding:
enableHardwareEncoding: true
hardwareAccelerationType: "vaapi"
vaapiDevice: "/dev/dri/renderD128"
Installation
Nix Flake (Recommended)
Add to your flake.nix:
{
inputs.jellarr.url = "github:venkyr77/jellarr";
outputs = { self, nixpkgs, jellarr, ... }: {
nixosConfigurations.myhost = nixpkgs.lib.nixosSystem {
modules = [
jellarr.nixosModules.default
({ config, ... }: {
services.jellarr = {
enable = true;
user = "jellyfin";
group = "jellyfin";
environmentFile = config.sops.templates.jellarr-env.path;
config = {
base_url = "http://localhost:8096";
system.enableMetrics = true;
};
};
})
];
};
};
}
Or run directly:
JELLARR_API_KEY=your_api_key nix run github:venkyr77/jellarr/v0.0.1
Docker (Recommended)
docker pull ghcr.io/venkyr77/jellarr:v0.0.1
With docker-compose:
services:
jellarr:
image: ghcr.io/venkyr77/jellarr:v0.0.1
container_name: jellarr
environment:
- JELLARR_API_KEY=${JELLARR_API_KEY}
- TZ=Etc/UTC
volumes:
- ./config:/config
restart: "no"
Bundle Download
Download from releases (requires Node.js 24+):
curl -LO https://github.com/venkyr77/jellarr/releases/download/v0.0.2/jellarr-v0.0.2.cjs
JELLARR_API_KEY=your_api_key node jellarr-v0.0.2.cjs --configFile path/to/config.yml
Note: Requires Node.js 24+ installed on your system.
Configuration
Jellarr uses a YAML configuration file (default: config/config.yml).
System Configuration
version: 1
base_url: "http://localhost:8096"
system:
enableMetrics: true # Enable Prometheus metrics endpoint
pluginRepositories:
- name: "Jellyfin Official"
url: "https://repo.jellyfin.org/releases/plugin/manifest.json"
enabled: true
trickplayOptions:
enableHwAcceleration: true
enableHwEncoding: true
Encoding Configuration
version: 1
base_url: "http://localhost:8096"
encoding:
enableHardwareEncoding: true
hardwareAccelerationType: "vaapi" # none, amf, qsv, nvenc, v4l2m2m, vaapi, videotoolbox, rkmpp
vaapiDevice: "/dev/dri/renderD128"
# or qsv device
qsvDevice: "/dev/dri/renderD128"
hardwareDecodingCodecs:
- h264
- hevc
- mpeg2video
- vc1
- vp8
- vp9
- av1
enableDecodingColorDepth10Hevc: true
enableDecodingColorDepth10HevcRext: true
enableDecodingColorDepth12HevcRext: true
enableDecodingColorDepth10Vp9: true
allowHevcEncoding: false
allowAv1Encoding: false
Library Configuration
version: 1
base_url: "http://localhost:8096"
library:
virtualFolders:
- name: "Movies"
collectionType: "movies"
libraryOptions:
pathInfos:
- path: "/data/movies"
- name: "TV Shows"
collectionType: "tvshows"
libraryOptions:
pathInfos:
- path: "/data/tv"
Branding Configuration
version: 1
base_url: "http://localhost:8096"
branding:
loginDisclaimer: |
Configured by <a href="https://github.com/venkyr77/jellarr">Jellarr</a>
customCss: |
@import url("https://cdn.jsdelivr.net/npm/jellyskin@latest/dist/main.css");
splashscreenEnabled: false
User Management
version: 1
base_url: "http://localhost:8096"
users:
# Regular user with plaintext password (development only)
- name: "regular-user"
password: "secure-password"
# Regular user with password file (production recommended)
- name: "viewer-user"
passwordFile: "/run/secrets/viewer-password"
# Admin user with policy configuration
- name: "admin-user"
passwordFile: "/run/secrets/admin-password"
policy:
isAdministrator: true
loginAttemptsBeforeLockout: 3
Password Security:
- plaintext password: Use
passwordfield for development/testing only - password file: Use
passwordFilefor production - file contains only the plaintext password (whitespace is trimmed) - Exactly one required: Each user must specify either
passwordorpasswordFile(not both)
sops-nix Integration:
{
sops.secrets = {
jellarr-api-key.sopsFile = ../../../../secrets/jellarr-api-key;
viewer-user-password.sopsFile = ../../../../secrets/viewer-user-password;
admin-user-password.sopsFile = ../../../../secrets/admin-user-password;
};
services.jellarr = {
enable = true;
environmentFile = config.sops.templates.jellarr-env.path;
config = {
base_url = "http://localhost:8096";
users = [
{
name = "viewer-user";
passwordFile = config.sops.secrets.viewer-user-password.path;
}
{
name = "admin-user";
passwordFile = config.sops.secrets.admin-user-password.path;
}
];
};
};
}
Startup Configuration
version: 1
base_url: "http://localhost:8096"
system: {}
startup:
completeStartupWizard: true # Mark startup wizard as complete
Useful for automated deployments where you want to skip the interactive startup wizard.
Secret Management
With sops-nix (nix only)
{
sops = {
secrets.jellarr-api-key.sopsFile = ./secrets/jellarr.env;
templates.jellarr-env = {
content = ''
JELLARR_API_KEY=${config.sops.placeholder.jellarr-api-key}
'';
owner = config.services.jellarr.user;
group = config.services.jellarr.group;
};
};
services.jellarr = {
enable = true;
environmentFile = config.sops.templates.jellarr-env.path;
config = { /* ... */ };
};
}
With Environment Variable
export JELLARR_API_KEY=your_api_key
jellarr
With Docker
docker run -e JELLARR_API_KEY=your_api_key \
-v ./config:/config:ro \
ghcr.io/venkyr77/jellarr:v0.0.1
API Key Bootstrap (NixOS, Same Host Only)
For NixOS deployments where Jellarr runs on the same host as Jellyfin, you can use the bootstrap feature to automatically provision the API key into Jellyfin's database:
{
sops.secrets.jellarr-api-key.sopsFile = ./secrets/jellarr-api-key;
services.jellarr = {
enable = true;
config = {
base_url = "http://localhost:8096";
# ... your config ...
};
# Bootstrap: automatically inserts API key into Jellyfin's database
bootstrap = {
enable = true;
apiKeyFile = config.sops.secrets.jellarr-api-key.path;
# Optional settings (showing defaults):
# apiKeyName = "jellarr";
# jellyfinDataDir = "/var/lib/jellyfin";
# jellyfinService = "jellyfin.service";
};
};
}
How it works:
- The
jellarr-api-key-bootstrapsystemd service runs after Jellyfin starts - It waits for Jellyfin's database to exist
- If the API key doesn't already exist, it stops Jellyfin, inserts the key into the SQLite database, and restarts Jellyfin
- The
jellarrservice hasAfter=jellarr-api-key-bootstrap.service, ensuring proper ordering
Important notes:
- This only works when Jellarr and Jellyfin are on the same host - it requires direct access to Jellyfin's database file
- The bootstrap service runs as
root(required for stopping/starting Jellyfin and writing to the database) - The insertion is idempotent - if a key with the same name exists, it skips
- For deployments where Jellarr runs on a different host than Jellyfin, you
must provision the API key manually (via Jellyfin's web UI or a separate
script) and provide it via
environmentFile
Full Configuration Example
Full configuration example with VAAPI hardware acceleration:
version: 1
base_url: "http://localhost:8096"
system:
enableMetrics: true
pluginRepositories:
- name: "Jellyfin Official"
url: "https://repo.jellyfin.org/releases/plugin/manifest.json"
enabled: true
trickplayOptions:
enableHwAcceleration: true
enableHwEncoding: true
encoding:
enableHardwareEncoding: true
hardwareAccelerationType: "vaapi"
vaapiDevice: "/dev/dri/renderD128"
hardwareDecodingCodecs:
["h264", "hevc", "mpeg2video", "vc1", "vp8", "vp9", "av1"]
enableDecodingColorDepth10Hevc: true
enableDecodingColorDepth10Vp9: true
enableDecodingColorDepth10HevcRext: true
enableDecodingColorDepth12HevcRext: true
allowHevcEncoding: false
allowAv1Encoding: false
library:
virtualFolders:
- name: "Movies"
collectionType: "movies"
libraryOptions:
pathInfos:
- path: "/mnt/movies/English"
branding:
loginDisclaimer: |
Configured by <a href="https://github.com/venkyr77/jellarr">Jellarr</a>
customCss: |
@import url("https://cdn.jsdelivr.net/npm/jellyskin@latest/dist/main.css");
splashscreenEnabled: false
users:
- name: "regular-user"
password: "secure-password"
- name: "viewer-user"
passwordFile: "/run/secrets/viewer-password"
- name: "admin-user"
passwordFile: "/run/secrets/admin-password"
policy:
isAdministrator: true
loginAttemptsBeforeLockout: 3
startup:
completeStartupWizard: true
Architecture
Jellarr is built in TypeScript with a strict pipeline pattern:
- CLI (
src/cli/index.ts) - Commander.js entry point - Pipeline (
src/pipeline/index.ts) - Main orchestration:- Reads YAML config file
- Validates with strict Zod schemas
- Creates authenticated Jellyfin API client
- Fetches current server configuration
- Applies ONLY specified changes idempotently
- Apply Modules (
src/apply/) - Handle configuration updates per feature:calculateDiff()- Pure calculation, returns schema or undefinedapply()- Side effects, calls Jellyfin API
Key Design Principles:
- Selective updates: Only modifies explicitly configured fields
- Idempotent: Safe to run multiple times
- Type-safe: OpenAPI-generated types + Zod validation
- Calculate/Apply pattern: Separation of pure logic from side effects
- Comprehensive test coverage
Development
pnpm test # Tests with Vitest
pnpm typecheck # TypeScript validation
pnpm eslint # Code linting
pnpm build # Build with esbuild
# Full validation pipeline
npm run build && tsc --noEmit && pnpm eslint && pnpm test && nix fmt
Nix Development
nix build .#default # Build CLI package
nix build .#docker-image # Build Docker image
nix flake check # Run checks
nix fmt # Format project files
Comparison: Jellarr vs declarative-jellyfin
| Feature | Jellarr | declarative-jellyfin |
|---|---|---|
| Method | Official REST API | Direct XML/DB manipulation |
| Service Impact | Zero (never stops Jellyfin) | Stops/starts multiple times |
| Platform | Cross-platform (Docker, any OS) | NixOS-only |
| Dependencies | Node.js 24+ | systemd, sqlite, jellyfin running on same host |
| Safety | API validates all changes | Direct file/DB writes |
| Future-proof | API contract stability | Breaks on internal changes |
| Type Safety | TypeScript + Zod + OpenAPI | Bash scripts |
| Testing | Comprehensive unit tests | Complex NixOS VM tests |
declarative-jellyfin's approach:
- Writes
~/.config/jellyfin/encoding.xmldirectly - Manipulates SQLite database with hardcoded schema
- Reimplements Jellyfin's UUID algorithm in bash
- Requires stopping Jellyfin service during configuration
- Known issues with timing/conflicts
Jellarr's approach:
- Uses
/System/Configurationand similar API endpoints - Never touches internal files or databases
- Jellyfin validates all changes
- Zero service interruption
- Works on any platform where Jellyfin runs
Contributing
Contributions welcome! See CONTRIBUTING.md for guidelines.
License
© 2025 Jellarr contributors