Compare commits

..
Author SHA1 Message Date
ajnart bbe13f5d39 fix(ci): use pinned pnpm for releases 2026-09-06 21:02:59 +02:00
Thomas 32a51c1e60 feat: announce Homarr v2 beta (#6793)
* feat: announce Homarr v2 beta

* chore: refine v2 beta announcement

* feat: expand v2 beta guidance

* chore: refine v2 beta resources

* chore: simplify v2 beta modal

* chore: improve v2 beta feedback links

* fix: keep v2 modal above header

* chore: sharpen v2 beta message

* fix: clear header for v2 modal
2026-09-06 20:36:04 +02:00
homarr-crowdin[bot] da32acb4f0 chore(lang): updated translations from crowdin
* chore(lang): update translations from crowdin

* chore(lang): update translations from crowdin

* chore(lang): update translations from crowdin

---------

Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2026-09-06 01:52:04 +00:00
homarr-update-contributors[bot] 1da13d9bb3 chore: update contributors (#6767)
Co-authored-by: Homarr Update Contributors <210161987+homarr-update-contributors[bot]@users.noreply.github.com>
2026-09-04 15:58:09 +00:00
homarr-crowdin[bot] cb0fec088f chore(lang): updated translations from crowdin
* chore(lang): update translations from crowdin

* chore(lang): update translations from crowdin

---------

Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2026-09-03 02:05:21 +00:00
homarr-crowdin[bot] a2fe8dd461 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2026-09-01 02:36:36 +00:00
homarr-update-contributors[bot] b6dfcd69ba chore: update contributors (#6735)
Co-authored-by: Homarr Update Contributors <210161987+homarr-update-contributors[bot]@users.noreply.github.com>
2026-08-28 21:54:00 +00:00
homarr-crowdin[bot]andThomas a38b0ef71d chore(lang): updated translations from crowdin (#6732)
Co-authored-by: Thomas <thomas@ajnart.fr>
2026-08-28 14:07:42 +02:00
Thomas 2cf3027eeb Merge pull request #6720 from homarr-labs/docs/update-helm-docs
docs(helm): update chart installation page
2026-08-28 08:32:40 +02:00
homarr-releases[bot] fa18f6fc71 chore: update bug report template (#6724) 2026-08-28 08:32:03 +02:00
Releases Homarr 331925c197 chore(release): 1.76.2 [skip ci] 2026-08-27 20:09:22 +00:00
Thomas 0e51f745d3 Merge pull request #6723 from homarr-labs/dev
chore(release): automatic release v1.76.2
2026-08-27 22:08:05 +02:00
Thomas d6f720d0e6 Revert "feat(http): set custom User-Agent on outbound integration requests (#6606)" (#6722)
This reverts commit ddbf05524b.
2026-08-27 22:00:23 +02:00
Thomas 5324bf12d4 fix(startup): use stable internal service addresses (#6721) 2026-08-27 21:52:54 +02:00
github-actions[bot] 2922a02ecf docs(helm): update chart installation page 2026-08-27 17:09:27 +00:00
Thomas 87cdcf28e0 fix(mcp): return valid string for void tool results (#6703) 2026-08-27 11:10:37 +02:00
homarr-releases[bot] 441b181e79 chore: update bug report template (#6716)
Co-authored-by: Releases Homarr <175486441+homarr-releases[bot]@users.noreply.github.com>
2026-08-27 11:08:43 +02:00
Releases Homarr eba639b59c chore(release): 1.76.1 [skip ci] 2026-08-26 16:43:52 +00:00
Thomas Camlong da36283ad1 chore(release): retrigger release pipeline 2026-08-26 18:30:12 +02:00
Thomas Camlong c050368782 chore(release): trigger release pipeline 2026-08-26 18:16:26 +02:00
homarr-releases[bot] 82b50f0fd7 chore(release): automatic release v1.76.1 2026-08-26 15:50:55 +00:00
Thomas a0149d5894 Merge pull request #6673 from homarr-labs/investigate-6672-immich-401 2026-08-22 19:44:03 +02:00
ajnart 8579fcbfc2 test(http,immich): address CodeRabbit review
- guarantee server teardown via try/finally in both wire-level tests
- add tuple-array HeadersInit coverage to the helper suite
2026-08-22 17:21:38 +02:00
ajnart b6f3aba23e test(http,immich): prove User-Agent fix at the wire level
Add two regression tests that fail on v1.76.0 and pass with the fix:
- wire-level test: a Headers instance (the shape @immich/sdk hands over)
  must reach the remote server with all headers intact
- real-SDK e2e: getServerStatistics through fetchWithTrustedCertificatesAsync
  against a live local server must deliver x-api-key
2026-08-22 17:16:31 +02:00
ajnart 31e218295d fix(http): preserve SDK headers when injecting User-Agent
The v1.76.0 User-Agent injection object-spread `fetchOptions.headers`, which
silently drops all headers when the value is a Headers instance. The Immich
SDK passes a Headers object with the merged x-api-key header, so requests
lost their API key and returned 401 (issue #6672). Build a real Headers
instance instead, keeping case-insensitive user-agent handling.
2026-08-22 17:12:43 +02:00
Thomas aca3b5d861 fix: correct Unraid CPU, link context menus, and board resizing (#6510)
Fix Unraid logical CPU averaging, preserve native context menus for app and bookmark widgets in view mode, and prevent scrollbar-width GridStack resize oscillation.
2026-08-22 10:53:26 +02:00
homarr-crowdin[bot] a7a2f0223a chore(lang): updated translations from crowdin (#6596)
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2026-08-22 08:49:51 +02:00
homarr-releases[bot] 7773fdc10b chore: update bug report template (#6655) 2026-08-21 22:14:45 +02:00
Thomas 7019a1d0a4 Merge pull request #6656 from homarr-labs/docs/update-helm-docs 2026-08-21 22:14:26 +02:00
github-actions[bot] 2d535caeac docs(helm): update chart installation page 2026-08-21 17:41:09 +00:00
Releases Homarr 2013fbe80d chore(release): 1.76.0 [skip ci] 2026-08-21 17:20:50 +00:00
homarr-releases[bot] 219ef58926 chore(release): automatic release v1.76.0 2026-08-21 17:19:40 +00:00
homarr-update-contributors[bot] 97195bc463 chore: update contributors (#6649)
Co-authored-by: Homarr Update Contributors <210161987+homarr-update-contributors[bot]@users.noreply.github.com>
2026-08-21 12:53:40 +00:00
Danura30082 73a742154e docs: Proxmox certificate obtention instructions (#6613) 2026-08-20 18:51:58 +02:00
Afonso Jorge Ramos 993a267e0c fix(truenas): keep reporting pools without usable dataset space (#6635) 2026-08-20 15:37:10 +02:00
Thomas f1c49d1fbf fix(startup): only chown runtime paths for custom PUID/PGID (#6598) 2026-08-19 08:29:17 +02:00
Thomas 95c66c61eb fix(widgets): stop Health Monitoring system tab firing NOT_FOUND when Proxmox is mixed in (#6623) 2026-08-18 19:56:47 +02:00
Thomas ec2f8c6c2c fix(widgets): match OMV disk temperature via normalized partition names (#6615) (#6619)
OMV reports filesystem devices (sda1) without the /dev/ prefix, so the
partition-suffix normalization never applied and filesystem-to-SMART
matching returned no temperature. Strip partition suffixes from
unprefixed sd/vd, nvme and mmcblk names, and use the shared normalized
matcher in both the health monitoring and system disks widgets.
2026-08-18 19:39:27 +02:00
Thomas 05ba57c2fa chore: migrate cursor rules to portable agent skills + mantine MCP guidance
* chore: migrate cursor rules to portable agent skills and add mantine MCP guidance

Replace Cursor-only .cursor/rules/*.mdc with Agent Skills open-standard
SKILL.md files in .agents/skills/ (canonical), mirrored to .cursor/skills/
and .claude/skills/ so they work across Claude Code, Cursor, Codex, and
OpenCode. Add mantine MCP server guidance to AGENTS.md and un-ignore
.agents/skills/ in .gitignore.

* refactor: single-source agent skills at .agents/skills

Cursor, Codex, and OpenCode all read .agents/skills/ natively, so the
.cursor/skills/ mirror was redundant. Claude Code reads .claude/skills/,
which is now a symlink back to .agents/skills/ so the content lives once.
Deletes .cursor/skills/ and converts .claude/skills/ to a symlink.

* chore: ship mantine MCP server via project-scoped .mcp.json

Declares the keyless @mantine/mcp-server in .mcp.json (Claude Code
project-scope standard, committable) and documents MCP server setup and
the mantine entry in AGENTS.md.

* chore: address coderabbit security-review and markdown lint findings

Add explicit security-review guidance before exposing MCP tools and label
plain-text code fences to satisfy markdownlint MD040.
2026-08-16 16:27:51 +02:00
Thomas ddbf05524b feat(http): set custom User-Agent on outbound integration requests (#6606)
* feat(http): set custom User-Agent on outbound integration requests

Set a default 'User-Agent: Homarr/<version> (+https://homarr.dev)' header
on all outbound HTTP requests made via fetchWithTrustedCertificatesAsync,
so Homarr's integration traffic is distinguishable in reverse-proxy,
WAF, and security-monitoring logs instead of appearing as generic undici.

Closes #6604

* refactor(version): centralize Homarr version in @homarr/version package

Replace fragile deep-relative 'package.json' imports across the monorepo
with a single source of truth: the new @homarr/version leaf package reads
the version once from the root package.json and exports it as VERSION.

Consumers now import by package name incl. the new User-Agent header in
fetchWithTrustedCertificatesAsync (packages/core):
  - core, api (info router), analytics, request-handler (update checker)
This also removes all manually-maintained relative version imports.

* fix(http): normalize User-Agent injection with Headers

Object-spreading fetchOptions.headers breaks when callers pass a Headers
instance (spread yields no entries) or a tuple array (numeric keys), and is
case-sensitive. Build a Headers instance and set the default User-Agent only
when no case-insensitive User-Agent entry exists, via a shared helper in both
fetch paths.

Adds unit tests covering record, Headers, tuple-array, and casing variants.

* revert(version): undo version refactor and tests, keep single-file change

Keep the User-Agent change as a single edit to
packages/core/src/infrastructure/http/request.ts. Remove the @homarr/version
package, the consumer migrations, and the unit tests.
2026-08-16 16:09:15 +02:00
Thomas df438b4cd2 Merge pull request #6571 from homarr-labs/feat/fix-home-assistant-actions 2026-08-16 00:23:14 +02:00
Thomas a1f91eb385 Merge pull request #6567 from homarr-labs/feat/fix-truenas-free-space 2026-08-15 20:20:35 +02:00
Thomas 8d5ffb2dd9 feat(paseo): add worktree install script (#6599)
* feat(paseo): add worktree install script

* feat(paseo): inline worktree setup in paseo.json

* feat(paseo): add scripts and teardown to paseo.json

* feat(paseo): expand workspace scripts catalog

* feat(paseo): reset encryption key in worktree env
2026-08-15 12:29:52 +02:00
Thomas Camlong 56e6b23af6 test(e2e): remove flaky credentials onboarding test 2026-08-15 11:05:06 +02:00
Thomas Camlong 5488bf4d18 fix(truenas): use lowercase in operator for dataset query 2026-08-15 01:33:20 +02:00
homarr-crowdin[bot] 621ea07549 chore(lang): updated translations from crowdin (#6553) 2026-08-14 20:39:02 +02:00
homarr-update-contributors[bot] f64c01456b chore: update contributors (#6588) 2026-08-14 20:38:42 +02:00
homarr-releases[bot] 56198c3f02 chore: update bug report template (#6591) 2026-08-14 20:38:26 +02:00
Thomas ca1bb6f341 Merge pull request #6592 from homarr-labs/docs/update-helm-docs
docs(helm): update chart installation page
2026-08-14 20:38:10 +02:00
github-actions[bot] bf6b6663cd docs(helm): update chart installation page 2026-08-14 18:25:32 +00:00
Releases Homarr 5c2d86767f chore(release): 1.75.0 [skip ci] 2026-08-14 17:45:24 +00:00
homarr-releases[bot] 7c75642d4d chore(release): automatic release v1.75.0 2026-08-14 17:43:56 +00:00
Thomas Camlong 7652eb8c1c preserve smart home toggle response 2026-08-13 17:04:00 +02:00
Thomas Camlong e761c10f32 fix Home Assistant widget actions 2026-08-13 16:21:48 +02:00
ajnart f6137b2dcc fix(truenas): guard empty dataset percentage 2026-08-12 18:24:33 +02:00
ajnart 425dd90871 fix(truenas): report usable dataset space 2026-08-12 18:19:07 +02:00
2751 changed files with 86501 additions and 303099 deletions

No files matched your search

+182 -27
View File
@@ -1,40 +1,195 @@
---
name: codebase-context
description: Navigate Homarr's monorepo architecture and reuse shared packages. Use when locating code, choosing a package boundary, understanding runtime services, tRPC, databases, widgets, integrations, cron jobs, Custom Widgets, Workshop, or checking @homarr/common before creating a utility.
description: Comprehensive Homarr codebase architecture and conventions reference. Use when orienting in the monorepo, understanding package dependency layers, the three runtime services and ports, tRPC router structure, multi-database support, widget/integration/cron system architecture, routing, auth providers, env vars, or key patterns before writing code.
---
# Homarr Codebase Context
Orient from the current checkout before editing. Treat package manifests, exports, and source as authoritative when they differ from this snapshot.
Homarr is an open-source, self-hosted dashboard for managing homelab services. It integrates with 50+ self-hosted apps (media servers, download clients, DNS, NAS, etc.) and provides a drag-and-drop widget-based UI.
## Workflow
## Tech Stack
1. Confirm the branch and inspect the nearest `package.json`, existing sibling modules, and package exports.
2. Search `@homarr/common` before creating a general-purpose helper. Read [references/common.md](references/common.md) for every public entrypoint and export.
3. Read [references/architecture.md](references/architecture.md) when the task crosses packages, runtimes, routing, data, widgets, integrations, cron jobs, Custom Widgets, or Workshop.
4. Keep the change in the deepest package that owns the behavior. Reuse public entrypoints rather than deep-importing implementation files.
5. Re-run targeted searches before documenting counts, consumers, or dependency edges; these change faster than the architectural seams.
- **Monorepo**: pnpm workspaces + Turborepo
- **Framework**: Next.js (App Router, `output: "standalone"`) — T3 Stack
- **Language**: TypeScript throughout
- **ORM**: Drizzle (supports SQLite via better-sqlite3, MySQL, PostgreSQL)
- **API**: tRPC (HTTP + WebSocket subscriptions) with superjson + OpenAPI bridge via `trpc-to-openapi`
- **Auth**: NextAuth v5 (database sessions, Credentials/LDAP/OIDC providers)
- **UI**: Mantine v9 (not Tailwind) + Tabler icons
- **State**: Jotai (atoms), TanStack Query (server state via tRPC)
- **Realtime**: WebSocket server (`ws`) for tRPC subscriptions, backed by Redis pub/sub
- **Cron**: `node-cron` in a standalone Fastify service (`apps/tasks`)
- **i18n**: next-intl with `[locale]` dynamic segment (prefix mode: `"never"`, locale from cookie)
- **Testing**: Vitest + jsdom, Playwright for E2E
- **Lint/Format**: oxlint + oxfmt (not ESLint/Prettier)
- **Docs**: Docusaurus 3 in `apps/docs/` (`@homarr/docs`)
- **Package manager**: pnpm 10.34.1, Node >= 24.16.0
## Current toolchain
## Repository Structure
- Use Node `24.18.0` from `mise.toml` and pnpm `11.15.1` from `package.json`.
- Use pnpm workspaces with Turborepo. Workspace packages use the `@homarr/` scope and catalog-managed dependencies.
- Use Next.js App Router, TypeScript, tRPC, Drizzle, Mantine v9, Tabler icons, Jotai, TanStack Query, next-intl, Vitest, and Playwright.
- Use oxlint and oxfmt. Mantine is the application UI system; Tailwind is limited to the docs app.
- `pnpm dev` starts only `@homarr/nextjs`. Start a standalone runtime explicitly when the task needs it.
```text
homarr/
├── apps/
│ ├── nextjs/ # Main Next.js application (port 3000)
│ ├── docs/ # Docusaurus 3 documentation site (@homarr/docs)
│ ├── tasks/ # Cron job runner + Fastify tRPC API (port 3002)
│ └── websocket/ # Standalone tRPC WebSocket server (port 3001)
├── packages/
│ ├── api/ # tRPC appRouter, procedures, OpenAPI
│ ├── auth/ # NextAuth config, providers, session, API keys
│ ├── db/ # Drizzle schema (3 DB drivers), migrations, queries
│ ├── core/ # Env validation, DB/Redis driver factories, logging
│ ├── definitions/ # Domain enums: WidgetKind, IntegrationKind, permissions
│ ├── widgets/ # All 39 dashboard widgets (definitions + components)
│ ├── integrations/ # Integration classes (HTTP clients to external apps)
│ ├── redis/ # Redis pub/sub channels, caching abstractions
│ ├── translation/ # next-intl setup, locale configs, lang JSON files
│ ├── ui/ # Shared Mantine components, theme, hooks
│ ├── validation/ # Shared zod schemas for API/forms
│ ├── common/ # Shared utilities, IDs, errors
│ ├── cron-jobs/ # Cron job implementations (25+ jobs)
│ ├── cron-jobs-core/ # Cron scheduling primitives
│ ├── cron-job-api/ # tRPC router for job management (start/stop/trigger)
│ ├── cron-job-status/ # Cron status via Redis
│ ├── boards/ # Board context, edit mode, cache updater
│ ├── modals/ # Modal primitives on Mantine
│ ├── modals-collection/ # Feature modals (apps, boards, docker, etc.)
│ ├── form/ # useZodForm (Mantine + zod resolver)
│ ├── forms-collection/# Reusable form UIs (new app, icon picker, upload)
│ ├── spotlight/ # Command palette / search with multiple modes
│ ├── request-handler/ # Server request handlers (feeds, integrations)
│ ├── notifications/ # Mantine notifications wrapper
│ ├── docker/ # Dockerode-based Docker access
│ ├── icons/ # Icon DB/repo integration
│ ├── image-proxy/ # Image proxy + caching
│ ├── ping/ # Reachability / ping utilities
│ ├── analytics/ # Server-side analytics (Umami)
│ ├── server-settings/ # Server setting keys/types
│ ├── settings/ # User-facing settings UI context
│ └── cli/ # Node CLI for ops (brocli)
├── tooling/
│ ├── typescript/ # Base tsconfig
│ └── github/ # CI setup action
├── development/ # Dev docker-compose (Redis, MySQL, PostgreSQL)
├── e2e/ # E2E test specs
└── Dockerfile # Multi-stage production build
```
## High-value seams
## Three Runtime Services
- Server/RSC tRPC: `@homarr/api/server`
- Client tRPC hooks: `@homarr/api/client`
- Main router: `packages/api/src/root.ts`
- MCP eager router: `packages/api/src/mcp.ts`
- Database schemas: `packages/db/schema/{sqlite,mysql,postgresql}.ts`
- Widget registry: `packages/widgets/src/registry.ts`
- Widget loading manifest: `packages/widgets/src/manifest.ts`
- Integration definitions: `packages/definitions/src/integration.ts`
- Integration factory: `packages/integrations/src/base/creator.ts`
- Cron jobs: `packages/cron-jobs/src/jobs/`
- App routes: `apps/nextjs/src/app/[locale]/`
| Service | Port | Role |
| --------- | ---- | -------------------------------------------- |
| Next.js | 3000 | Main web app (SSR + API routes) |
| WebSocket | 3001 | tRPC subscriptions via `ws` |
| Tasks | 3002 | Cron job runner + management API via Fastify |
When a code change affects users, invoke `documentation-sync`. When exposing tRPC through MCP, invoke `mcp-integration`.
In Docker, nginx listens on **7575** and proxies to all three. The tasks service authenticates via `CRON_JOB_API_KEY` header.
## Package Dependency Layers
1. **Foundation**: `core` (env, DB drivers, Redis client, logging) → no `@homarr` deps
2. **Domain**: `definitions` → `common` → `core`
3. **Data**: `db` → `core`, `definitions`, `common`
4. **Auth**: `auth` → `db`, `definitions`, `core`, `validation`
5. **Cross-cutting**: `translation`, `validation`, `redis`, `server-settings`
6. **Feature backends**: `integrations`, `request-handler`, `cron-jobs`, `docker`, `ping`
7. **API surface**: `api` → pulls in auth, db, redis, integrations, etc.
8. **UI stack**: `ui`, `modals`, `form`, `notifications`, `boards`, `spotlight`
9. **Composed features**: `widgets`, `modals-collection`, `forms-collection`
## Key Patterns
### tRPC
- **Server caller** (RSC): `import { api } from "@homarr/api/server"` → cached caller with auth context
- **Client hooks**: `import { clientApi } from "@homarr/api/client"` → React Query hooks
- **Router structure**: `packages/api/src/root.ts` defines top-level namespaces (`user`, `board`, `widget`, `integration`, `docker`, `kubernetes`, etc.)
- **Procedures**: `publicProcedure`, `protectedProcedure`, `permissionRequiredProcedure`, `onboardingProcedure`
- **WebSocket subscriptions**: Client uses `wsLink` when `type === "subscription"`, connects to port 3001
### Multi-Database Support
Drizzle schemas exist in three parallel implementations:
- `packages/db/schema/sqlite.ts`
- `packages/db/schema/mysql.ts`
- `packages/db/schema/postgresql.ts`
Selected at runtime via `DB_DRIVER` env (`better-sqlite3` | `mysql2` | `node-postgres`). Migrations are per-engine in `packages/db/migrations/{sqlite,mysql,postgresql}/`.
### Widget System
Widgets are registered in `packages/widgets/src/index.tsx` as `widgetImports` (must satisfy `Record<WidgetKind, ...>`). Each widget:
- Has a `definition` created via `createWidgetDefinition(kind, { icon, createOptions, supportedIntegrations? })`
- Uses `optionsBuilder.from(factory => ({ ... }))` for typed options
- Loads dynamically via `next/dynamic`
- Fetches data via tRPC client hooks (backed by cron job caches)
### Integration System
Integrations are defined in `packages/definitions/src/integration.ts` (`integrationDefs`) and instantiated via `createIntegrationAsync` in `packages/integrations/src/base/creator.ts`. Each extends abstract `Integration` class with `testConnectionAsync` and typed methods.
### Cron Job System
Jobs defined in `packages/cron-jobs/src/jobs/`. Each job:
- Uses `createCronJob` with a cron expression
- Publishes results to Redis channels
- Widget subscriptions consume those channels via tRPC subscriptions
- Job management (start/stop/trigger) via `cronJobApi` HTTP client to port 3002
### Modals
Created with `createModal(component).withOptions({ defaultTitle })` from `@homarr/modals`. Opened via `useModalAction(SomeModal)`. Feature modals live in `packages/modals-collection/`.
### Forms
`useZodForm` from `@homarr/form` wraps Mantine `useForm` with zod validation. Shared form UIs in `packages/forms-collection/`.
### Routing (App Router)
- `apps/nextjs/src/app/[locale]/` — all UI routes under locale segment
- Route groups: `(home)`, `(content)`, `(board)` — don't affect URLs
- Key routes: `boards/[name]`, `manage/...` (admin), `auth/login`, `init` (onboarding), `widgets/[kind]`
- Locale driven by cookie (no URL prefix)
### Auth Providers
Configured via `AUTH_PROVIDERS` env. Supports: `credentials` (local), `ldap`, `oidc`. Database sessions with httpOnly cookies. API key auth for programmatic access.
## Environment Variables
Key env vars (from `.env.example`):
- `DB_DRIVER`: `better-sqlite3` | `mysql2` | `node-postgres`
- `DB_URL`: Database connection string
- `AUTH_SECRET`: NextAuth secret
- `SECRET_ENCRYPTION_KEY`: AES-256-CBC for integration secrets
- `AUTH_PROVIDERS`: Comma-separated (`credentials`, `ldap`, `oidc`)
- `CRON_JOB_API_KEY`: Shared key between Next.js and tasks service
- `REDIS_*`: Redis connection (host, port, password)
- `ENABLE_KUBERNETES`: Optional Kubernetes integration
## Development Commands
- `pnpm dev` — runs all three services in parallel via Turborepo
- `pnpm docker:dev` — starts Redis + MySQL + PostgreSQL containers
- `pnpm db:push` — push schema to SQLite (dev)
- `pnpm db:studio` — Drizzle Studio
- `pnpm test` — Vitest unit tests
- `pnpm test:e2e` — Playwright E2E
- `pnpm lint` / `pnpm format` — oxlint + oxfmt
- `pnpm dev:docs` — Docusaurus docs site (port 3003)
- `pnpm typecheck` — tsc --noEmit across all packages
## Conventions
- Workspace packages use `@homarr/` scope
- All versions managed via pnpm `catalog:` in workspace
- Path alias `~/*` → `./src/*` in Next.js app
- Mantine for all UI (no Tailwind) — primary color is red, `autoContrast: true`
- Icons from `@tabler/icons-react`
- Drag-and-drop via `@dnd-kit/*`
- `typescript.ignoreBuildErrors: true` in next.config (types checked separately via `typecheck`)
- `serverExternalPackages`: `dockerode`, `isomorphic-dompurify`, `jsdom`
@@ -1,4 +0,0 @@
interface:
display_name: "Homarr Codebase Context"
short_description: "Navigate Homarr architecture and shared APIs"
default_prompt: "Use $codebase-context to locate the right Homarr package and reuse existing shared APIs."
@@ -1,110 +0,0 @@
# Homarr architecture
## Contents
- [Applications and runtimes](#applications-and-runtimes)
- [Package map](#package-map)
- [Dependency direction](#dependency-direction)
- [Core patterns](#core-patterns)
- [Commands and conventions](#commands-and-conventions)
## Applications and runtimes
| Path | Role |
| ---------------- | ---------------------------------------------------------------------------------------- |
| `apps/nextjs` | Main Next.js application and HTTP APIs on port 3000 |
| `apps/docs` | Docusaurus documentation site on port 3003 |
| `apps/tasks` | Cron-job initialization and scheduling runtime; it does not expose the old port-3002 API |
| `apps/websocket` | tRPC WebSocket subscriptions on port 3001 |
| `apps/workshop` | Go/PocketBase Workshop service and provider integration |
`pnpm dev` runs only `@homarr/nextjs`. Use `pnpm --filter @homarr/tasks dev`, `pnpm --filter @homarr/websocket dev`, or `pnpm dev:docs` when a task needs those runtimes.
In production, `apps/nextjs/src/instrumentation-node.ts` starts the tasks and WebSocket runtimes inside the Node process. Nginx listens on port 7575, proxies `/websockets` to port 3001, and sends other traffic to port 3000. Redis can run internally or externally.
## Package map
### Foundation and domain
- `core`: environment validation, database and Redis factories, logging, infrastructure.
- `definitions`: domain enums, integration definitions, widget kinds, permissions.
- `common`: shared utilities with environment-specific entrypoints. Search it before adding helpers.
- `validation`: shared Zod schemas.
- `server-settings`: typed server-setting keys and access.
### Data, identity, and transport
- `db`: Drizzle schemas, queries, migrations, and three database drivers.
- `auth`: Auth.js/NextAuth configuration, sessions, providers, and API keys.
- `api`: tRPC routers, contexts, MCP metadata extraction, OpenAPI, and client/server entrypoints.
- `redis`: Redis clients, pub/sub channels, and caches.
- `cron-jobs-core`, `cron-jobs`, `cron-job-status`: scheduling primitives, job implementations, and status transport.
### Backends and external systems
- `integrations`: typed clients for external applications.
- `request-handler`: shared server request flows.
- `docker`, `ping`, `image-proxy`, `icons`, `analytics`: bounded infrastructure capabilities.
- `custom-widgets`: Custom JSX v2 schema, validation, runtime, and security contracts.
- `workshop`: TypeScript Workshop client, backend contracts, and schemas used by Homarr.
### UI and composed features
- `ui`: shared Mantine components, theme, and hooks. Search here before creating a primitive.
- `form`, `forms-collection`: Zod-backed form setup and reusable forms.
- `modals`, `modals-collection`: modal primitives and feature modals.
- `boards`: board state, edit mode, and cache coordination.
- `settings`, `spotlight`, `notifications`, `onboarding`: cross-feature UI flows.
- `widgets`: widget definitions, dynamic component loaders, and shared widget UI.
- `translation`: next-intl configuration and locale catalogs.
- `cli`: Node operations CLI. `tools/homarr-dev` is the separate Go developer CLI.
## Dependency direction
Prefer these directions; verify the exact dependency in package manifests before changing a boundary.
1. Put infrastructure in `core` and stable domain names in `definitions`.
2. Put reusable, dependency-light helpers in `common`; use its public environment-specific entrypoints.
3. Build persistence in `db`, identity in `auth`, transport in `redis`, and external clients in `integrations`.
4. Compose server behavior in `api`, `cron-jobs`, and request-handler packages.
5. Build primitives in `ui`, `form`, and `modals`; compose product surfaces in widgets and collection packages.
6. Keep app entrypoints in `apps/*` thin. Avoid moving reusable behavior into `apps/nextjs` when a package already owns it.
## Core patterns
### tRPC and MCP
`packages/api/src/root.ts` lazily composes the application router. Server components use `@homarr/api/server`; client components use `@homarr/api/client`. MCP extraction requires the eager router in `packages/api/src/mcp.ts`, so MCP-enabled subrouters must be imported and registered there directly. Use the `mcp-integration` skill for exposure and security rules.
### Databases
Drizzle schemas live in parallel files under `packages/db/schema/`. `DB_DRIVER` selects `better-sqlite3`, `mysql2`, or `node-postgres`; migrations live in matching directories under `packages/db/migrations/`. Apply schema behavior consistently across all supported drivers unless the task is explicitly driver-specific.
### Widgets
`packages/widgets/src/registry.ts` owns the explicit widget-module and component-loader registries. `packages/widgets/src/manifest.ts` caches those loaders and exposes definitions, components, and combined resources. Widget modules use `createWidgetDefinition` and typed option builders. Data commonly comes from tRPC queries or Redis-backed cron results. Use existing shared widget components before creating local variants.
### Integrations
`packages/definitions/src/integration.ts` owns `integrationDefs` and `IntegrationKind`. `packages/integrations/src/base/creator.ts` owns `createIntegrationAsync`; concrete integrations implement the shared base contract. Keep credentials and permission checks on server-owned paths.
### Cron jobs
Jobs live in `packages/cron-jobs/src/jobs/` and use `createCronJob`. `apps/tasks` initializes and starts the job group. Job results and status can flow through Redis and tRPC; the retired port-3002 management-API model is no longer part of the architecture.
### Routing and localization
User routes live below `apps/nextjs/src/app/[locale]/`; the locale segment does not add a URL prefix. Route groups organize home and board surfaces. Administrative routes live under `manage`, Custom Widget configuration has a token route, and Workshop has a management surface.
### Custom Widgets and Workshop
`packages/custom-widgets` owns the Custom JSX v2 contract. `packages/workshop` is the Homarr-side client/contract package; `apps/workshop` is the separate Go/PocketBase service. Use the `homarr-custom-widget` skill for authoring and security constraints.
## Commands and conventions
- Read current scripts from the root or package `package.json`; avoid caching commands that do not exist.
- Use `pnpm dev` for Next.js, `pnpm dev:docs` for docs, and `pnpm dev:cli -- <args>` for the Go developer CLI.
- Use `pnpm docker:dev:up` for the Redis development dependency.
- Use package-filtered typechecks or focused tests for the touched behavior.
- Use `~/*` only inside the Next.js app. Use public `@homarr/*` entrypoints across package boundaries.
- Keep Mantine application styles separate from the Tailwind-based docs app.
@@ -1,85 +0,0 @@
# `@homarr/common` public API
## Contents
- [Reuse workflow](#reuse-workflow)
- [Entrypoints](#entrypoints)
- [Root exports](#root-exports)
- [Types exports](#types-exports)
- [Server exports](#server-exports)
- [Client and environment exports](#client-and-environment-exports)
## Reuse workflow
Before writing a utility, search both the public name and the behavior:
```bash
rg -n "export (const|function|class|type|interface)|export \*" packages/common
rg -n "@homarr/common" apps packages
```
Import only from an exported package entrypoint. Do not deep-import `@homarr/common/src/*`. Check `packages/common/package.json` and the source again when adding or renaming an export; this inventory describes the current repository tree.
## Entrypoints
| Import | Source | Environment |
| ----------------------- | ---------------------------- | ----------------------------------------------------------------------------------- |
| `@homarr/common` | `index.ts` -> `src/index.ts` | Shared root; includes React hooks and some Next/Mantine-aware helpers |
| `@homarr/common/types` | `src/types.ts` | Type-only utilities |
| `@homarr/common/server` | `src/server.ts` | Node/server-only security, encryption, request, error, and bounded-response helpers |
| `@homarr/common/client` | `src/client.ts` | Client-facing Next server action export |
| `@homarr/common/env` | `env.ts` | Validated package environment |
## Root exports
| Area | Public exports | Purpose |
| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
| Objects | `objectKeys`, `objectEntries`, `isRecord`, `hashObjectBase64` | Typed key/entry iteration, record guards, and stable query-key hashing |
| Strings | `capitalize`, `isNullOrWhitespace`, `bestMatch`, `normalizeImageName`, `getImageMatchRank` | Text normalization and matching |
| Cookies | `parseCookies`, `setClientCookie` | Cookie parsing and browser cookie serialization |
| Arrays | `splitToNChunks`, `splitToChunksWithNItems` | Split by number of groups or maximum group size |
| Dates | `formatDuration`, `toValidDate`, `isDateWithin` | Duration formatting, safe date coercion, relative-window checks |
| Timing | `Stopwatch` | Elapsed-time measurement and formatting |
| React hooks | `useTimeAgo`, `useIntegrationConnected` | Relative time and integration freshness state |
| URLs | `removeTrailingSlash`, `extractBaseUrlFromHeaders`, `getPortFromUrl`, `isAbsoluteUrl`, `SAFE_NEW_TAB_REL`, `getSafeApplicationUrl`, `getSafeAppHref` | URL normalization, safe navigation, ports, and new-tab policy |
| Numbers and bytes | `formatNumber`, `formatBytes`, `formatBytesPair`, `formatByteRate`, `formatBitRate`, `defaultByteUnitSystem`, `metricToImperial` | Compact numbers, byte displays, byte/bit rates, and unit conversion |
| Byte types | `ByteUnitSystem`, `FormatBytesOptions`, `FormatBitRateOptions` | Options for byte and bit-rate formatting |
| Errors | `extractErrorMessage`, `FlattenError` | Unknown-error messages and flattenable domain errors |
| Theme | `getMantineColor` | Resolve a Mantine color and shade |
| Functions | `isFunction` | Unknown-value function guard |
| Grid | `GridAlgorithmItem`, `generateResponsiveGridFor` | Responsive nested-section grid placement |
| IDs | `createId` | CUID2 ID creation |
The root deliberately does not re-export `types`, `server`, `client`, or `env`; use their named entrypoints.
## Types exports
`@homarr/common/types` exports:
- `MaybePromise<T>`
- `AtLeastOneOf<T>`
- `Modify<T, R>`
- `RemoveReadonly<T>`
- `MaybeArray<T>`
- `Inverse<T>`
- `inferSearchParamsFromSchema<TSchema>`
## Server exports
`@homarr/common/server` exports:
- Security: `generateSecureRandomToken`.
- Encryption: `encryptSecret`, `decryptSecret`, `decryptSecretWithKey`, `createKeyedFingerprint`, `verifyKeyedFingerprint`.
- Requests: `userAgent`, `DeviceType`, `ipAddressFromHeaders`.
- Bounded bodies: `readBoundedJsonResponseAsync`.
- Parse errors: `ParseError`, `ParseErrorHandler`, `ZodParseErrorHandler`, `JsonParseErrorHandler`.
- HTTP handlers: `HttpErrorHandler`, `FetchHttpErrorHandler`, `matchErrorCode`, `OFetchHttpErrorHandler`, `AxiosHttpErrorHandler`, `TsdavHttpErrorHandler`, `OctokitHttpErrorHandler`.
- HTTP errors: `RequestError`, `ResponseError`, `requestErrorMap`.
- HTTP error types: `AnyRequestError`, `AnyRequestErrorInput`, `RequestErrorInput`, `RequestErrorType`, `RequestErrorReason`, `AnyRequestErrorReason`, `RequestErrorCode`.
`NodeFetchHttpErrorHandler` exists as an implementation file but is not exported by the public server entrypoint. Add an intentional public export before using it across packages.
## Client and environment exports
- `@homarr/common/client` exports `revalidatePathActionAsync`, a Next server action for page revalidation.
- `@homarr/common/env` exports `env` with validated `NODE_ENV`, `SECRET_ENCRYPTION_KEY`, and `NO_EXTERNAL_CONNECTION` values.
+691
View File
@@ -0,0 +1,691 @@
---
name: datatable-migration
description: Playbook for migrating tables from mantine-react-table (MRT) to mantine-datatable. Use when converting any table component from mantine-react-table to mantine-datatable, working on DataTable column/sort/expansion/context-menu code, or implementing responsive column visibility, column persistence, or transparent tables in widgets.
---
# Migrating Tables from mantine-react-table to mantine-datatable
This document captures every pattern, fix, and decision from the downloads widget rework (PR #6430). Use it as a playbook when porting other tables.
## Migration Candidates
Current `mantine-react-table` consumers that could be migrated:
**Widgets (in `packages/widgets/`):**
- `docker/component.tsx` — Docker container table
- `media-server/component.tsx` — Media server sessions
**Admin pages (in `apps/nextjs/src/app/[locale]/manage/`):**
- `tools/docker/docker-table.tsx`
- `tools/tasks/_components/tasks-table.tsx`
- `tools/api/components/api-keys.tsx`
- `users/_components/user-list.tsx`
- `users/invites/_components/invite-list.tsx`
- `tools/kubernetes/` — 8 Kubernetes resource tables
**Modals:**
- `packages/modals-collection/src/search-engines/request-media-modal.tsx`
**Shared hook to remove once all migrated:**
- `packages/ui/src/hooks/use-translated-mantine-react-table.ts`
## 1. Core Migration: mantine-react-table → mantine-datatable
### Import changes
```typescript
// BEFORE
import { MantineReactTable, useMantineReactTable } from "mantine-react-table";
import type { MRT_ColumnDef } from "mantine-react-table";
// AFTER
import { DataTable, useDataTableColumns } from "mantine-datatable";
import type { DataTableColumn, DataTableSortStatus } from "mantine-datatable";
```
### Column definition shape
```typescript
// BEFORE (MRT)
const columns: MRT_ColumnDef<MyRow>[] = [
{
accessorKey: "name",
header: "Name",
size: 200,
enableSorting: true,
Cell: ({ row }) => <Text>{row.original.name}</Text>,
},
];
// AFTER (mantine-datatable)
const columns: DataTableColumn<MyRow>[] = [
{
accessor: "name", // not accessorKey
title: "Name", // not header
width: 200, // not size
sortable: true, // not enableSorting
render: (record) => <Text>{record.name}</Text>, // not Cell
},
];
```
Key differences:
- `accessorKey` → `accessor`
- `header` → `title`
- `size` → `width`
- `Cell: ({ row }) =>` → `render: (record) =>`
- `enableSorting` → `sortable`
- `enableColumnActions` → does not exist (no column action menus)
- `mantineTableBodyCellProps` → `cellsStyle` in `defaultColumnProps`
### Table component
```typescript
// BEFORE
const table = useTranslatedMantineReactTable({ columns, data, ... });
return <MantineReactTable table={table} />;
// AFTER
return (
<DataTable
records={data}
columns={columns}
sortStatus={sortStatus}
onSortStatusChange={setSortStatus}
...
/>
);
```
### Sorting is manual
mantine-datatable does NOT sort for you. You must sort the data yourself:
```typescript
const [sortStatus, setSortStatus] = useState<DataTableSortStatus<MyRow>>({
columnAccessor: "name",
direction: "asc",
});
const sortedData = useMemo(() => {
const mult = sortStatus.direction === "desc" ? -1 : 1;
return [...data].toSorted((a, b) => {
const aVal = a[sortStatus.columnAccessor];
const bVal = b[sortStatus.columnAccessor];
if (typeof aVal === "string") return aVal.localeCompare(bVal as string) * mult;
return ((aVal as number) - (bVal as number)) * mult;
});
}, [data, sortStatus]);
// Pass sortedData to records, not data
<DataTable records={sortedData} sortStatus={sortStatus} onSortStatusChange={setSortStatus} />
```
## 2. Making Tables Transparent (Widget Context)
Widgets sit inside board cards that have user-controlled opacity. The table must be fully transparent so the card background shows through. mantine-datatable defaults to `var(--mantine-color-body)` which is opaque.
Create a CSS file alongside the component:
```css
/* styles.css */
.my-table,
.my-table .mantine-datatable-table,
.my-table .mantine-datatable-table thead,
.my-table .mantine-datatable-table tbody,
.my-table .mantine-datatable-table tfoot,
.my-table th,
.my-table td {
background-color: transparent !important;
}
.my-table .mantine-datatable-table tr {
background-color: transparent;
}
/* Hover: translucent instead of opaque */
.my-table .mantine-datatable-table tbody tr:hover {
background-color: color-mix(
in srgb,
var(--mantine-color-default-hover) 40%,
transparent
) !important;
}
/* Headers: frosted glass effect */
.my-table th {
white-space: nowrap;
background-color: color-mix(
in srgb,
var(--mantine-color-body) 60%,
transparent
) !important;
backdrop-filter: blur(8px);
}
/* Cell overflow */
.my-table td {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Hide sort icon until hover */
.my-table .mantine-datatable-header-cell-sortable-icon {
opacity: 0;
transition: opacity 150ms;
}
.my-table th:hover .mantine-datatable-header-cell-sortable-icon {
opacity: 1;
}
/* Row expansion area */
.my-table .mantine-datatable-row-expansion-cell {
background-color: color-mix(
in srgb,
var(--mantine-color-body) 40%,
transparent
) !important;
}
```
Import it: `import "./styles.css";` and apply via `className="my-table"`.
## 3. Responsive Column Visibility
Use a lookup table of width breakpoints instead of media queries. The widget receives `width` as a prop.
```typescript
interface SizeConfig {
fontSize: "xs" | "sm";
iconSize: number;
cellPadding: number;
showSpeedColumns: boolean;
showTimeColumn: boolean;
showStateColumn: boolean;
}
const SIZE_BREAKPOINTS: { maxWidth: number; config: SizeConfig }[] = [
{
maxWidth: 300,
config: {
fontSize: "xs",
iconSize: 12,
cellPadding: 2,
showSpeedColumns: false,
showTimeColumn: false,
showStateColumn: false,
},
},
{
maxWidth: 500,
config: {
fontSize: "xs",
iconSize: 14,
cellPadding: 4,
showSpeedColumns: false,
showTimeColumn: true,
showStateColumn: false,
},
},
];
const DEFAULT_SIZE_CONFIG: SizeConfig = {
fontSize: "sm",
iconSize: 14,
cellPadding: 4,
showSpeedColumns: true,
showTimeColumn: true,
showStateColumn: true,
};
function getSizeConfig(width: number): SizeConfig {
for (const { maxWidth, config } of SIZE_BREAKPOINTS) {
if (width < maxWidth) return config;
}
return DEFAULT_SIZE_CONFIG;
}
// Usage: memoize it
const size = useMemo(() => getSizeConfig(width), [width]);
```
Then use a visibility check lookup table for columns:
```typescript
const columnVisibilityChecks: Partial<
Record<string, (ctx: ColumnContext) => boolean>
> = {
upSpeed: (ctx) => ctx.hasTorrents && ctx.size.showSpeedColumns,
downSpeed: (ctx) => ctx.size.showSpeedColumns,
time: (ctx) => ctx.size.showTimeColumn,
state: (ctx) => ctx.size.showStateColumn,
};
function shouldIncludeColumn(accessor: string, ctx: ColumnContext): boolean {
if (!ctx.optionsColumnSet.has(accessor)) return false;
const check = columnVisibilityChecks[accessor];
if (check && !check(ctx)) return false;
return true;
}
```
## 4. Column Persistence (Drag/Resize → Server)
`useDataTableColumns` stores column order and widths in localStorage by default. For widgets, this must persist server-side so it works across devices.
### Hidden widget options
In `index.ts`, add hidden text options:
```typescript
columnOrder: factory.text({ defaultValue: "" }),
columnWidths: factory.text({ defaultValue: "" }),
```
With visibility override:
```typescript
{
columnOrder: { shouldHide: () => true },
columnWidths: { shouldHide: () => true },
}
```
### Translation keys required
Even hidden options need translation keys or the translation test fails:
```json
"widget.mywidget.option.columnOrder.label": "Column order",
"widget.mywidget.option.columnWidths.label": "Column widths"
```
### Dynamic storeKey
Use a key that includes `itemId` and sorted column names. This prevents cross-widget localStorage bleeding:
```typescript
const storeKey = `mytable-${itemId ?? "preview"}-${[...options.columns].toSorted().join(",")}`;
```
### Hydration from server → localStorage
On mount, seed `useDataTableColumns` with server-persisted values. Use a `hydrated` ref to prevent the persist effect from immediately writing defaults back:
```typescript
const {
effectiveColumns,
columnsOrder,
columnsWidth,
setColumnsOrder,
setMultipleColumnWidths,
} = useDataTableColumns<MyRow>({ key: storeKey, columns });
const lastStoreKey = useRef(storeKey);
const hydrated = useRef(false);
useEffect(() => {
if (lastStoreKey.current !== storeKey) {
lastStoreKey.current = storeKey;
hydrated.current = false;
}
if (hydrated.current) return;
if (savedOrder.length > 0) setColumnsOrder(savedOrder);
if (Object.keys(savedWidths).length > 0) {
setMultipleColumnWidths(
Object.entries(savedWidths).map(([accessor, w]) => ({
accessor,
width: w,
})),
);
}
requestAnimationFrame(() => {
hydrated.current = true;
});
}, [
storeKey,
savedOrder,
savedWidths,
setColumnsOrder,
setMultipleColumnWidths,
]);
```
### Persist localStorage → server
Watch for changes and write back:
```typescript
const prevOrder = useRef(columnsOrder);
const prevWidths = useRef(columnsWidth);
useEffect(() => {
if (!hydrated.current) return;
const orderChanged =
JSON.stringify(columnsOrder) !== JSON.stringify(prevOrder.current);
const widthsChanged =
JSON.stringify(columnsWidth) !== JSON.stringify(prevWidths.current);
prevOrder.current = columnsOrder;
prevWidths.current = columnsWidth;
if (!orderChanged && !widthsChanged) return;
if (orderChanged)
persistOption({ columnOrder: JSON.stringify(columnsOrder) });
if (widthsChanged) {
const widthMap: Record<string, number> = {};
for (const entry of columnsWidth) {
const key = Object.keys(entry)[0];
if (!key) continue;
const v = entry[key as keyof typeof entry];
if (typeof v === "number") widthMap[key] = v;
else if (typeof v === "string" && v.endsWith("px"))
widthMap[key] = parseInt(v, 10);
}
persistOption({ columnWidths: JSON.stringify(widthMap) });
}
}, [columnsOrder, columnsWidth, persistOption]);
```
The `persistOption` helper calls both `setOptions` (local state) and `saveItemOptions` (server mutation).
## 5. Context Menu (Right-Click)
Do NOT install `mantine-contextmenu`. Use Mantine's `Menu` + `Portal` directly:
```typescript
interface ContextMenuState {
x: number;
y: number;
item: MyRow;
}
const [contextMenu, setContextMenu] = useState<ContextMenuState | null>(null);
const handleContextMenu = useCallback(
({ record, event }: { record: MyRow; event: React.MouseEvent }) => {
event.preventDefault();
setContextMenu({ x: event.clientX, y: event.clientY, item: record });
},
[],
);
// On DataTable:
<DataTable onRowContextMenu={handleContextMenu} />
// Render:
{contextMenu && <RowContextMenu state={contextMenu} onClose={closeContextMenu} />}
```
The menu component uses `Portal` for correct z-index:
```tsx
function RowContextMenu({
state,
onClose,
}: {
state: ContextMenuState;
onClose: () => void;
}) {
return (
<Portal>
<Menu
opened
onClose={onClose}
closeOnItemClick={false}
position="right-start"
offset={0}
>
<Menu.Target>
<Box
style={{
position: "fixed",
left: state.x,
top: state.y,
width: 0,
height: 0,
}}
/>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item
onClick={() => {
/* action */ onClose();
}}
>
Action
</Menu.Item>
</Menu.Dropdown>
</Menu>
</Portal>
);
}
```
**Key fix:** Use `closeOnItemClick={false}` if any menu item needs a two-step confirmation (like delete → confirm). Close the menu on scroll: `onScroll={() => { if (contextMenu) closeContextMenu(); }}`.
## 6. Row Expansion
```typescript
<DataTable
rowExpansion={{
trigger: "click",
allowMultiple: true,
collapseProps: {
transitionDuration: 200,
animateOpacity: true,
transitionTimingFunction: "ease-out",
},
content: ({ record, collapse }) => <ExpandedRow item={record} collapse={collapse} />,
}}
/>
```
**Critical:** Do NOT set `onRowClick` when using `rowExpansion` with `trigger: "click"`. They conflict and the expansion won't work.
## 7. Common Pitfalls & Fixes
### Status indicator dots
Don't use `<Badge variant="dot" />` without children — it renders a pill with empty space. Use a plain `Box`:
```tsx
<Box
w={8}
h={8}
style={{
borderRadius: "50%",
backgroundColor: `var(--mantine-color-${color}-filled)`,
flexShrink: 0,
}}
/>
```
### Tooltip overflow on long text columns
When a column has `ellipsis: true`, the tooltip target element can still extend beyond the cell bounds if inner elements don't clip. Fix:
```tsx
<Tooltip position="bottom-start" ...>
<Group style={{ overflow: "hidden" }}>
...
</Group>
</Tooltip>
```
Use `position="bottom-start"` instead of `position="right"` so the tooltip never escapes the widget horizontally.
### Dark mode tooltips
Mantine tooltips default to the theme's tooltip color which may be white in light mode and stay white in dark mode. Always set `color="dark"` on informational tooltips inside widgets.
### Division by zero
Guard all ratio calculations:
```typescript
if (item.size > 0) ratio = item.sent / item.size;
if (totalDown > 0) ratio = totalUp / totalDown;
```
### Composite idAccessor
When rows can come from multiple integrations, IDs may collide. Use a composite key:
```typescript
idAccessor={(record) => `${record.integration.id}:${record.id}`}
```
### JSON option parsing
Always wrap in try/catch:
```typescript
function parseJsonOption<T>(value: string | undefined, fallback: T): T {
if (!value) return fallback;
try {
return JSON.parse(value) as T;
} catch {
return fallback;
}
}
```
### Clipboard API
`navigator.clipboard.writeText()` can throw in non-HTTPS contexts. Always catch:
```typescript
void navigator.clipboard.writeText(text).catch(() => {});
```
### Sync sortStatus with widget options
If the widget has configurable default sort, sync it:
```typescript
useEffect(() => {
setSortStatus({
columnAccessor: options.defaultSort,
direction: defaultSortDirection,
});
}, [options.defaultSort, defaultSortDirection]);
```
## 8. Accessibility
- All `ActionIcon` components need `aria-label`
- Filter toggles (client badges, status badges) need `aria-pressed={isActive}`
- `textSelectionDisabled` on DataTable prevents accidental text selection during drag/resize
## 9. Error Handling
- Every mutation needs an `onError` callback with `showErrorNotification`
- The `saveItemOptions` mutation (column persistence) also needs `onError`
- Check `isError` on the main query and show a fallback UI
- Use a shared `mutationOptions` object to avoid repeating `onSettled`/`onError`:
```typescript
const mutationOptions = {
onSettled: () => void utils.myQuery.invalidate(),
onError: () =>
showErrorNotification({
title: t("errors.actionFailed"),
message: t("errors.actionFailedMessage"),
}),
};
```
## 10. i18n
- No hardcoded strings. Every user-visible string goes through `useScopedI18n`
- Column titles: `t("items.{accessor}.columnTitle")`
- Detail labels: `t("items.{accessor}.detailsTitle")`
- States: `t("states.{state}")`
- Actions: `t("actions.{action}")`
- Errors: `t("errors.{errorKey}")`
- Hidden options still need `.label` translation keys
## 11. Edit Mode
Disable pointer events on the table during edit mode to prevent conflicts with the board's drag-and-drop:
```typescript
let tablePointerEvents: React.CSSProperties["pointerEvents"];
if (isEditMode) tablePointerEvents = "none";
let rowContextMenuHandler = handleContextMenu;
if (isEditMode) rowContextMenuHandler = undefined;
<DataTable style={{ pointerEvents: tablePointerEvents }} />
```
## 12. DataTable Props Reference (Commonly Used)
```typescript
<DataTable
records={sortedData}
columns={effectiveColumns}
storeColumnsKey={storeKey}
withTableBorder={false}
borderRadius={0}
highlightOnHover
striped="odd"
stripedColor={{ dark: "dark.7", light: "gray.0" }}
highlightOnHoverColor={{ dark: "dark.5", light: "gray.1" }}
verticalAlign="center"
fetching={isFetching && data.length === 0}
loaderBackgroundBlur={2}
fz={size.fontSize}
textSelectionDisabled
sortStatus={sortStatus}
onSortStatusChange={setSortStatus}
noRecordsText={t("errors.noItems")}
idAccessor={(record) => record.uniqueKey}
height="100%"
className="my-table"
defaultColumnProps={{
noWrap: true,
draggable: true,
resizable: true,
cellsStyle: () => ({ padding: `${size.cellPadding}px 8px` }),
}}
scrollAreaProps={{ type: "auto", scrollbarSize: 6 }}
rowBackgroundColor={(record) => conditionalBgMap[record.state]}
onRowContextMenu={handleContextMenu}
rowExpansion={{ ... }}
onScroll={() => { if (contextMenu) closeContextMenu(); }}
/>
```
## 13. Layout Stability
- Don't use `table-layout: fixed` — it fights mantine-datatable's resize logic
- Give non-name columns explicit `width` values so only the name column flexes
- Use `ellipsis: true` on the name column — it handles `text-overflow: ellipsis` internally
- Names should truncate at the cell boundary, not a character count, so the layout is stable regardless of content length
- `flexShrink: 0` on badges/icons prevents them from being squeezed by long names
## 14. Checklist for Each Migration
1. Replace imports (`mantine-react-table` → `mantine-datatable`)
2. Convert column definitions (`accessorKey` → `accessor`, `header` → `title`, `Cell` → `render`, etc.)
3. Implement manual sorting with `useState` + `useMemo`
4. Add CSS file for transparent backgrounds (if widget context)
5. Add responsive column visibility (if widget context)
6. Add hidden options + hydration logic (if column persistence needed)
7. Add translation keys (including hidden option labels)
8. Add context menu if row actions exist
9. Add row expansion if detail view exists
10. Add error handling on all mutations
11. Add accessibility attributes
12. Test at multiple widget widths
13. Update docs in `apps/docs/docs/widgets/` if applicable
14. Run `pnpm turbo typecheck`
15. Run translation spec — catches missing keys
+28 -35
View File
@@ -1,51 +1,44 @@
---
name: documentation-sync
description: Keep Homarr's Docusaurus documentation aligned with user-facing code. Use when adding or changing integrations, widgets, APIs, environment variables, CLI commands, authentication, permissions, cron jobs, settings, or UI behavior that users must understand.
description: Keep Homarr documentation in sync with code changes. Use when modifying code that affects user-facing behavior, or when adding/modifying widgets, integrations, cron jobs, API routes, env vars, permissions, auth providers, or UI in apps/docs. Covers the docs/ mapping table, integration doc pattern, and widget doc pattern.
---
# Documentation Sync
Update `apps/docs/` in the same change as user-facing behavior. Inspect adjacent pages and the docs types before copying a pattern; the source and docs evolve together.
When modifying code that affects user-facing behavior, you MUST also update the documentation in `apps/docs/`.
## Map the change
## What triggers a doc update
| Code change | Documentation target |
| ----------------------------------- | -------------------------------------------------------------------- |
| New integration | `apps/docs/docs/integrations/<slug>/index.mdx` and `index.ts` |
| New widget | `apps/docs/docs/widgets/<slug>/index.mdx` and `index.ts` |
| Changed public or management API | `apps/docs/docs/management/api/index.mdx` |
| New or changed environment variable | Relevant page under `apps/docs/docs/advanced/` or installation docs |
| New or changed CLI command | `apps/docs/docs/advanced/command-line/` |
| Authentication/provider change | Relevant SSO or authentication page under `apps/docs/docs/advanced/` |
| UI or workflow change | Relevant getting-started, management, widget, or integration page |
| Cron-job behavior | `apps/docs/docs/management/tasks.mdx` |
| Permission behavior | `apps/docs/docs/management/users.mdx` and the affected feature page |
| Custom Widget or Workshop behavior | Relevant Custom Widget, Assistant, or Workshop documentation |
| Code change | Doc location |
| ---------------------- | ----------------------------------------------------------- |
| New integration | `apps/docs/docs/integrations/<slug>/index.mdx` + `index.ts` |
| New widget | `apps/docs/docs/widgets/<slug>/index.mdx` + `index.ts` |
| Changed API surface | `apps/docs/docs/management/api.mdx` |
| New/changed env vars | `apps/docs/docs/advanced/` |
| New CLI commands | `apps/docs/docs/advanced/command-line/` |
| Changed auth providers | `apps/docs/docs/advanced/` SSO pages |
| UI/UX changes | Relevant getting-started or management pages |
| New cron job | `apps/docs/docs/management/tasks.mdx` |
| Changed permissions | `apps/docs/docs/management/users.mdx` |
If no existing row fits, search by the user-visible term and update the page where a user would look for the behavior:
## Integration doc pattern
```bash
rg -n "<term>" apps/docs/docs apps/docs/src
```
Each integration has a folder under `apps/docs/docs/integrations/<slug>/`:
## Follow existing typed patterns
- `index.ts` — exports a typed `IntegrationDefinition` with name, description, iconUrl, path
- `index.mdx` — uses shared components: `IntegrationHeader`, `IntegrationCapabilites`, `IntegrationSecrets`
- Import metadata from `@homarr/definitions` where possible
For an integration:
## Widget doc pattern
- Add or update `apps/docs/docs/integrations/<slug>/index.ts` with the local `IntegrationDefinition` pattern.
- Add or update `index.mdx` with the established `IntegrationHeader`, `IntegrationCapabilites`, and `IntegrationSecrets` components as applicable.
- Reuse metadata from `@homarr/definitions` when the docs app already exposes it.
Each widget has a folder under `apps/docs/docs/widgets/<slug>/`:
For a widget:
- `index.ts` — exports a typed `WidgetDefinition` with icon, name, description, path, configuration
- `index.mdx` — uses shared components: `WidgetHeader`, `WidgetConfig`, `WidgetAdding`
- Add or update `apps/docs/docs/widgets/<slug>/index.ts` with the local `WidgetDefinition` pattern.
- Add or update `index.mdx` with the established `WidgetHeader`, `WidgetConfig`, and `AddingWidget` components as applicable.
- Document options, required integrations, permissions, empty states, and changed behavior that affect setup or use.
## Development
## Completion criteria
1. Describe the shipped behavior, not an implementation plan.
2. Keep names, defaults, paths, screenshots, links, and prerequisites consistent with code.
3. Update every affected page and remove superseded guidance.
4. Run the narrowest useful docs validation. Use `pnpm turbo build --filter=@homarr/docs` when links, MDX, generated definitions, or navigation can fail; otherwise run the docs package formatter on touched files.
5. Treat broken links and anchors as failures; Docusaurus checks them strictly.
- Run docs locally: `pnpm dev:docs` from root
- Build docs: `pnpm turbo build --filter=@homarr/docs`
- The docs app can import from `@homarr/definitions` for type-safe integration/widget metadata
- Docusaurus strict mode throws on broken links — fix any broken references before committing
@@ -1,4 +0,0 @@
interface:
display_name: "Homarr Documentation Sync"
short_description: "Keep user-facing docs aligned with code"
default_prompt: "Use $documentation-sync to map this Homarr code change to the required documentation updates."
@@ -1,43 +0,0 @@
---
name: homarr-custom-widget
description: Author, validate, preview, test, install, or configure static or API-backed Homarr Custom JSX v2 widgets.
---
# Homarr Custom Widget
Author one widget or a coordinated set. Load release-matched context on demand. Run lifecycle tools alone; independent reads may run together. Finish each widget's validation, evidence, and persistence before the next.
1. For connected widgets, read primary API documentation; use web search when it is missing or may have changed.
2. Create credential-free definitions with keyed `sources`, `requests`, optional `options`, and safe JSX `template`. Static widgets use empty `sources` and `requests`; an API is optional.
3. While drafting, use `customWidget_validateTemplate` for focused JSX diagnostics without resending the manifest.
4. Send the definition once to `customWidget_previewCreate` and test its queries/actions. For a JSX-only fix, validate, call `customWidget_previewReviseTemplate` with its session, and retest; it inherits the manifest and resets evidence. Create a preview only for source/request/option changes.
5. Configure deployment-specific source URLs and credentials through Homarr; never repeat plaintext.
6. Persist each exact final tested preview with `customWidget_createFromPreview`. Do not resend a large definition through `customWidget_create` when a preview session is available.
Use supplied samples or preview responses as the data contract. Render requested fields and all returned items; guard optional nested values. Humanize numeric enums with label arrays, omit absent numbers, and label timestamp timezones. Give load errors and empty states a refresh or retry path.
Use `{option:name}` or `$option` for saved options. Use `{param:name}` or `$param` for values supplied by `SubFetch`, `ActionButton`, or `ToggleSwitch`. Load queries cannot use invocation parameters. Render load queries from `data` and `status` with `RefreshButton`; reserve `SubFetch` for manual parameterized queries. Templates read `data`, `status`, `options`, and temporary `inputs`.
For a bound control that depends on another input, declare its default and set `resetKey` to that scalar dependency. For example, `<Pagination bind="page" defaultValue={1} resetKey={inputs.search} />` restores page 1 when a search changes without fetching by itself.
`SubFetch` with `trigger="manual"` renders its own load button. Pass a card or image through `triggerContent` with `triggerAriaLabel` when that content should launch the request. Its callback is `(result, meta)`; callback names must not shadow the reserved roots `data`, `status`, `options`, or `inputs`. Never author `onClick` or a fetch callback. Use `Icon` or `TablerIcon` with a `name`; never invent an `IconFoo` component.
Plan capabilities. Use one `customWidget_findComponents` search per job. Batch selected non-obvious binding or interaction documentation with `customWidget_getComponents`; reserve `customWidget_getComponent` for one unknown-prop repair. Failed validation reopens discovery; otherwise search only for a missing capability. The full catalog is for broad exploration; load at most one example. Context never limits composition. Prefer clear hierarchy, responsive grids, divided lists, and aligned actions over nested row cards.
Do not simplify a useful workflow merely because the template is interpreted. Freely compose any supported installed components with multiple sources, requests, options, `choicesFrom`, bound filters, charts, responsive detail areas, manual queries, and safe actions when they improve the user's job. Complexity must remain purposeful rather than decorative.
Use divided lists or responsive media grids, one primary metric/action, compact headers, quiet metadata, and one badge. Full-width artwork wraps at base and caps above xs.
Make initial states actionable and wrap variable labels/values on narrow tiles. Do not use an unlabeled decorative icon as an empty state.
Do not use imports, hooks, refs, raw HTML, raw event callbacks, browser requests, arbitrary functions, eval, bigint, npm packages, authored statement blocks, IIFEs, or recursion. Do not pretend MCP tools exist in an offline chat session.
For a community widget, use `customWidget_workshopSearch`, `customWidget_workshopGet`, then `customWidget_workshopInstall`; configure its source securely. Preview configuration expires, so create before persisting configuration.
Reference routing:
- Load `schema` once before a new manifest; otherwise only for concrete request or option ambiguity.
- Load `runtime` for bound inputs, manual queries, actions, or other interaction.
- Load `security` once for any authenticated source or mutation, or to resolve a URL or interpreter limitation.
Repository installations expose these as files under `references/`. MCP clients can call `customWidget_getReference` or read `homarr://custom-widgets/references/{name}` instead of loading every reference.
@@ -1,4 +0,0 @@
interface:
display_name: "Homarr Custom Widget"
short_description: "Build and deploy safe Custom JSX widgets"
default_prompt: "Use $homarr-custom-widget to author, validate, preview, and configure a safe Custom JSX v2 widget."
@@ -1,37 +0,0 @@
# Runtime
Templates read `data.requestId`, `status.requestId`, `options.name`, and temporary `inputs.name`. Status is `{ loading, ok, status, statusText, error }`. Render load queries directly from `data` and `status` with `RefreshButton`; never wrap them in `SubFetch`.
`bind="search"` creates an in-memory input. It is never persisted. Supply invocation values only through `params`, for example:
```jsx
<TextInput bind="search" label="Search" />
<Pagination bind="page" resetKey={inputs.search} defaultValue={1} total={5} />
<SubFetch requestId="search" trigger="manual" params={{ query: inputs.search }}>
{(result) => <Stack>{(result.results ?? []).map(item => <Text key={item.id}>{item.name}</Text>)}</Stack>}
</SubFetch>
```
Manual queries require `trigger: "manual"` on request and `SubFetch`; otherwise they run automatically. `triggerContent` with `triggerAriaLabel` makes custom content the launcher. `SubFetch` owns loading/error/retry; its child receives success plus `{ ok, status, statusText, loading: false }`. Never author `onClick` or fetch callbacks.
`SubFetch`, `ActionButton`, and `ToggleSwitch` need literal `requestId`; validation rejects missing/computed IDs.
Inside a successful manual result, `<RefreshButton requestId="search" label="Run again" />` reruns the same parameters.
When a manual SubFetch request ID, parameters, or effective definition changes, Homarr immediately hides its prior result and returns to the trigger. It cannot fetch the new parameters until the user triggers it again.
The `SubFetch` callback receives the entire JSON response exactly as previewed. If the response is `{ "results": [...] }`, render and map `result.results`; never map the envelope itself. Trace every rendered field from the preview response before persistence.
Format timestamps with safe static helpers; never use `new Date`. Never invent a formatter component. Use `Date.toLocaleString(value, "en-US", "UTC")` plus a visible `UTC` label. Also available: `Date.toISOString`, `Date.toLocaleDateString`, and `Date.toLocaleTimeString`.
For compact numeric enums, index a literal label array with a fallback:
```jsx
<Text>{["Unknown", "Pending", "Ready"][(item.status ?? 1) - 1] ?? "Unknown"}</Text>
```
Every stateful control must use `bind`, and its `inputs.<name>` value must feed a supported request/helper when it is meant to change remote data. For dependent pagination, declare `defaultValue={1}` and use `resetKey={inputs.search}` to restore page 1 when the query changes. If a control cannot affect the workflow through a binding, option, or runtime helper, render concise context instead of a dead control.
Callback parameters must not shadow the reserved roots `data`, `status`, `options`, or `inputs`. Use `<Icon name="refresh" />` or `<TablerIcon name="refresh" />`; never invent components such as `<IconFoo />`.
Use expression callbacks for supported collections and trusted slots. No callback blocks, IIFEs, authored recursion, or raw events. Regex is limited to safe string operations.
@@ -1,49 +0,0 @@
# Schema
```ts
interface HomarrCustomWidgetV2 {
$schema: "homarr-custom-widget-v2";
name: string;
description?: string;
iconUrl?: string;
sources: Record<string, CustomWidgetSource>;
requests: Record<string, CustomWidgetRequest>;
options?: Record<string, CustomWidgetOption>;
template: string;
}
```
Static widgets use empty `sources` and `requests`. Each request must resolve to a defined source; omitting `source` uses `default` only when that ID exists. Source properties are `name?`, `baseUrl`, `networkScope`, and `auth?`:
```json
{
"sources": {
"default": {
"name": "Service",
"baseUrl": "http://service.local:5055/api/v1",
"networkScope": "private",
"auth": { "type": "apiKeyHeader", "name": "X-Api-Key" }
}
},
"requests": {
"summary": { "path": "/summary" },
"search": { "trigger": "manual", "path": "/search", "query": { "q": { "$param": "query" } } },
"create": {
"kind": "action",
"method": "POST",
"path": "/items",
"body": { "id": { "$param": "id" } },
"confirmation": "Create this item?",
"invalidates": ["search"]
}
}
}
```
Auth is `none`, `bearer`, `basic`, `{ "type": "apiKeyHeader", "name": "X-Api-Key" }`, or `{ "type": "apiKeyQuery", "name": "api_key" }`. A request defaults to source `default`, kind `query`, method `GET`, trigger `load`, inherited auth, and view permission. Set `trigger: "manual"` for a parameterized query. An action defaults to manual and modify permission. DELETE uses full permission and confirmation. Do not use `load: false`.
Use stable real URLs for public APIs and clear suggested URLs for self-hosted services. Homarr collects the installer's server URL, network scope, and credentials as source setup; credentials remain outside the manifest.
Paths use `{option:name}` and `{param:name}`; query/body references use `{ "$option": "name" }` and `{ "$param": "name" }`. Constants stay primitive (`take: 10`); `$param` is only for manual helpers, never load queries. Names and types are inferred.
Every option has `label`, `control`, and `default`. Optional fields are `description`, `choices`, `choicesFrom`, `min`, `max`, `step`, `advanced`, and `group`.
@@ -1,9 +0,0 @@
# Security
Private access includes LAN, Docker DNS, and Tailscale CGNAT addresses. Loopback means the Homarr server or container, not the viewer's browser.
All requests use Homarr's protected server executor. Source origin, network scope, DNS, redirects, SSRF, rate limits, permissions, size limits, timeouts, and encrypted credential injection remain enforced.
The JSX interpreter blocks imports, hooks, refs, raw event callbacks, browser requests, eval, arbitrary functions, prototype access, unsafe URLs, global CSS escape, arbitrary portals, bigint, statement blocks, IIFEs, and recursion. Regex literals must be bounded and reject backreferences, lookbehind, nested quantifiers, excessive length, and unsupported flags.
Credentials are stored separately and never exported or returned to an agent. A published self-hosted source URL is only a suggestion: installers must confirm or replace private and loopback URLs for their own Homarr deployment. Source origins cannot be controlled through widget options.
+79 -67
View File
@@ -1,98 +1,110 @@
---
name: mcp-integration
description: Expose Homarr tRPC procedures safely through MCP. Use when adding or changing tRPC procedures under packages/api, adding MCP metadata, registering an eager MCP router, shaping tool inputs and descriptions, or reviewing MCP authorization, sensitive data, and destructive actions.
description: Expose tRPC procedures as MCP tools for AI clients. Use when modifying procedures in packages/api/src/router, adding a new tRPC procedure or router, or deciding what to expose via Homarr's MCP interface. Covers .meta({ mcp }), router registration in packages/api/src/mcp.ts, input schema rules, and what to expose vs skip.
---
# MCP Integration
Expose a procedure only when an AI client should call it. Preserve the procedure's existing authorization and add MCP-specific safeguards for sensitive or destructive behavior.
Every new tRPC procedure that provides useful functionality should be exposed as an MCP tool so AI assistants (Claude, Cursor, etc.) can use it. This is a first-class feature of Homarr.
## Assess exposure
Exposing a procedure makes it callable by any AI client with an API key. Before adding `.meta({ mcp })`, review what the tool exposes: authorization requirements, sensitive data, tenant isolation, auditability, and whether the action is destructive. Prefer exposing read-only queries over mutations unless the mutation is safe and permission-scoped. When in doubt, ask for a security review before shipping a new MCP tool.
Prefer:
## Adding MCP to a procedure
- Data queries: list, search, get, health, stats, and summaries.
- User-requested actions with explicit permission checks and bounded inputs.
- Serializable results that do not reveal credentials or internal-only state.
Skip subscriptions, session/onboarding internals, file streams, blobs, credential values, and procedures whose authorization or audit behavior is unclear. Ask for a security review when a mutation is destructive, tenant boundaries are ambiguous, or returned data is sensitive.
## Add metadata
Place `.meta()` before `.input()` and `.query()` or `.mutation()`:
Add `.meta({ mcp: { enabled: true, description: "..." } })` to the procedure chain:
```typescript
getAll: protectedProcedure
.meta({
mcp: {
enabled: true,
description: "List resources the current user can access, including stable IDs used by resource_get",
},
})
.input(z.object({ limit: z.number().int().min(1).max(100).default(50) }))
.query(async ({ ctx, input }) => {
// Preserve normal procedure authorization.
});
```
// Read-only tool (query)
myProcedure: protectedProcedure
.meta({ mcp: { enabled: true, description: "Get all items with their status and metadata" } })
.input(z.object({ limit: z.number().default(10) }))
.query(async ({ ctx, input }) => { ... })
No-input procedures can omit `.input()`; the MCP extractor accepts an empty object for them.
For mutations, state the effect and required permission in the description:
```typescript
remove: permissionRequiredProcedure
.meta({
mcp: {
enabled: true,
description: "Delete one resource by ID. Requires full access; get the ID from resource_all",
},
})
// Write tool (mutation)
myAction: protectedProcedure
.meta({ mcp: { enabled: true, description: "Delete an item by ID. Requires interact permission on the integration" } })
.input(z.object({ id: z.string() }))
.mutation(async ({ ctx, input }) => {
// Perform the permission-scoped action.
});
.mutation(async ({ ctx, input }) => { ... })
// No-input procedure (z.void) — works fine, no .input() needed
getAll: protectedProcedure
.meta({ mcp: { enabled: true, description: "List all resources" } })
.query(async ({ ctx }) => { ... })
```
## Write discoverable descriptions
The `.meta()` call must come **before** `.input()` and `.query()`/`.mutation()` in the chain.
Include every item that applies:
## Writing good descriptions
- What the tool returns or changes.
- Supported integrations or services.
- Required IDs and the tool that returns them.
- Permission requirements and the meaning of permission fields.
- Important bounds, confirmation semantics, or irreversible effects.
The description is the **only thing** an AI sees to decide when and how to use the tool. Write it like you're explaining to a colleague who has never used Homarr.
Use precise domain language. A description such as `Get calendar events` is insufficient because it omits supported services, required IDs, and result scope.
**Must include:**
## Shape inputs for clients
- What the tool returns or does
- Which integrations/services it works with (if applicable)
- How to get required IDs (e.g., "Use integration_all to get the integrationId")
- Permission requirements (if non-obvious)
- Use a top-level `z.object({...})` for parameterized tools.
- Bound strings, arrays, numeric ranges, pagination, and payload sizes.
- Use defaults where a safe, unsurprising value exists.
- Prefer explicit fields over a top-level union or discriminated union.
- Keep identifiers stable and describe how to obtain them.
- Keep secrets out of inputs unless the procedure is explicitly a credential-configuration flow with server-owned encryption and non-return guarantees.
**Good:**
## Register the eager router
```text
"Get calendar events for upcoming and recent media releases. Fetches from all connected Sonarr (TV), Radarr (movies), Lidarr (music), and Readarr (books) integrations. Requires integrationIds from integration_all"
```
The application router in `packages/api/src/root.ts` is lazy. MCP tool extraction is synchronous, so add an eager import and registration in `packages/api/src/mcp.ts`:
**Bad:**
```text
"Get calendar events"
```
**For tools that return permission fields**, explain what they mean:
```text
"List all integrations. Returns permissions.hasUseAccess (read) and permissions.hasInteractAccess (actions) — false means the API key lacks that permission, not an error"
```
## Registering in the MCP router
After adding `.meta()` to procedures, you must register the router in `packages/api/src/mcp.ts`. This file uses **eager imports** (no `lazy()`) because MCP tool extraction requires synchronous access to all procedure definitions.
```typescript
import { resourceRouter } from "./router/resource";
// packages/api/src/mcp.ts
import { myNewRouter } from "./router/my-new-feature";
export const mcpRouter = createTRPCRouter({
resource: resourceRouter,
// ... existing routers
myFeature: myNewRouter,
});
```
Register the smallest router that owns the enabled procedures. If a parent router uses `lazy()`, import the needed subrouter directly. Metadata on a procedure is not enough; a procedure absent from `mcpRouter` is not exposed.
**If your router is nested** (e.g., widget sub-routers), import and register the sub-router directly — do not import a parent that uses `lazy()`.
## Validate the tool surface
## What to expose vs. what to skip
1. Confirm the procedure still enforces its ordinary auth and permission boundary.
2. Confirm `packages/api/src/mcp.ts` eagerly includes the procedure.
3. Confirm the generated tool name, description, and JSON schema are unambiguous.
4. Run the focused API MCP spec when the tool list or extraction changes: `pnpm test packages/api/src/test/mcp.spec.ts`.
5. Run the focused route spec when protocol or transport behavior changes: `pnpm test apps/nextjs/src/app/api/mcp/[transport]/route.spec.ts`.
6. If manually probing `/api/mcp/<transport>`, use a scoped API key and avoid printing credentials or secret-bearing results.
**Expose:**
- Data queries (list, search, get by ID)
- Actions users would ask an AI to do (toggle, create, delete, approve)
- Status checks (health, stats, summaries)
**Skip:**
- Subscriptions (WebSocket-only, not supported by MCP)
- Internal procedures (session management, onboarding steps)
- File upload/download procedures
- Procedures that return non-serializable data (streams, blobs)
## Input schema rules
- Use `z.object({...})` for inputs — this maps cleanly to MCP tool parameters
- `z.void()` (no `.input()`) works — the MCP handler patches these to accept empty objects
- Avoid `z.union()` or `z.discriminatedUnion()` at the top level — AI clients struggle with these
- Use `.default()` on optional fields so the AI doesn't need to guess values
## Checklist for new features
1. Add `.meta({ mcp: { enabled: true, description: "..." } })` to relevant procedures
2. Import and register the router in `packages/api/src/mcp.ts` (eager, no lazy)
3. Write descriptions that explain what the tool does, what it needs, and what it returns
4. If the tool requires an ID from another tool, say which one in the description
5. Test with `curl` against `/api/mcp/mcp` to verify the tool appears in `tools/list`
@@ -1,4 +0,0 @@
interface:
display_name: "Homarr MCP Integration"
short_description: "Expose tRPC procedures safely through MCP"
default_prompt: "Use $mcp-integration to expose this Homarr tRPC procedure as a safe, discoverable MCP tool."
+8
View File
@@ -0,0 +1,8 @@
version = 1
[[analyzers]]
name = "javascript"
[analyzers.meta]
plugins = ["react"]
environment = ["nodejs"]
+8 -40
View File
@@ -1,24 +1,22 @@
# Since .env is gitignored, you can use .env.example to build a new `.env` file when you clone the repo.
# Keep this file up-to-date when you add new variables to `.env`.
# Keep this file up-to-date when you add new variables to \`.env\`.
# This file will be committed to version control, so make sure not to have any secrets in it.
# If you are cloning this repo, create a copy of this file named `.env` and populate it with your secrets.
# Generate two different 64-character hex values with `openssl rand -hex 32` before starting Homarr.
# Never reuse these values or commit the populated `.env` file.
AUTH_SECRET=
# The below secret is not used anywhere but required for Auth.js (Would encrypt JWTs and Mail hashes, both not used)
AUTH_SECRET="supersecret"
# The below secret is used to encrypt integration secrets in the database.
# Homarr will reject an empty or invalid value and print a randomly generated candidate.
SECRET_ENCRYPTION_KEY=
# It should be a 32-byte string, generated by running `openssl rand -hex 32` on Unix
# or starting the project without any (which will show a randomly generated one).
SECRET_ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000
LOG_LEVEL='info'
# This is how you can use the sqlite driver:
DB_DRIVER='better-sqlite3'
# Safe local source-development path. SQLite files are ignored by Git.
# Container images use /appdata/db/db.sqlite by default instead.
DB_URL='./db.sqlite'
DB_URL='FULL_PATH_TO_YOUR_SQLITE_DB_FILE'
# Those are the two ways to use the mysql2 driver:
# 1. Using the URL format:
@@ -63,39 +61,9 @@ TURBO_TELEMETRY_DISABLED=1
# Enable mock integration
UNSAFE_ENABLE_MOCK_INTEGRATION=true
# Public URL contract. Values must be absolute HTTP(S) URLs without credentials,
# query strings, or fragments. Trailing slashes are normalized at startup.
# Public Homarr origin used by MCP OAuth metadata and redirects behind proxies.
# BASE_URL=https://homarr.example.com
HOMARR_WEBSITE_URL=https://homarr.dev
# PocketBase API used by Homarr and by the standalone documentation site.
# Defaults to HOMARR_WEBSITE_URL.
# WORKSHOP_API_URL=https://homarr.dev
# Public Workshop website used by links in Homarr, Custom Widgets, onboarding,
# and assistant context. Defaults to HOMARR_WEBSITE_URL/workshop.
# WORKSHOP_WEB_URL=https://homarr.dev/workshop
# Documentation and PocketBase deployment settings.
# WORKSHOP_PUBLIC_ORIGIN=https://homarr.dev
# PB_ALLOWED_ORIGINS=*
# Local Workshop stack:
# HOMARR_WEBSITE_URL=http://127.0.0.1:3003
# WORKSHOP_API_URL=http://127.0.0.1:8090
# WORKSHOP_WEB_URL=http://127.0.0.1:3003/workshop
# WORKSHOP_PUBLIC_ORIGIN=http://127.0.0.1:3003
# Optional: GITHUB_CLIENT_ID=
# Optional: GITHUB_CLIENT_SECRET=
# Enable demo mode (seeds a demo user "demo"/"demo", a sample board and a mock integration)
# DEMO_MODE=true
# Whether demo mode is read-only. Defaults to true (the public demo blocks all mutations).
# Set to false for writable preview deployments so the demo user can modify the dashboard.
# DEMO_READ_ONLY=false
# Persistent trusted-widget build cache; activated artifacts are also retained in the database.
# Defaults to /appdata/custom-widgets in production and ./appdata/custom-widgets in development.
# CUSTOM_WIDGET_PACKAGE_PATH=/appdata/custom-widgets
# Maximum concurrently active trusted widget server artifacts per Homarr process.
# CUSTOM_WIDGET_MAX_SERVER_PROCESSES=16
# DEMO_READ_ONLY=false
+131
View File
@@ -0,0 +1,131 @@
name: Homarr v2 beta feedback
description: Report a specific bug, unexpected behavior, or improvement for the Homarr v2 beta
title: "feedback(v2): "
labels: ["needs triage"]
body:
- type: markdown
attributes:
value: |
Thanks for testing Homarr v2. It will become the default when it is ready, and every detailed report helps us get there.
We cannot guess what you wanted from “this looks wrong.” Tell us exactly which feature is affected, what happened, why it is wrong for your use case, and what you expected instead. Add screenshots or a short recording whenever possible.
**Not useful:** “The board looks wrong.”
**Useful:** “The container changes from four columns to three after I move a widget at 1440 × 900 and 100% browser zoom. I expected it to keep four columns.”
- type: dropdown
id: feedback-type
attributes:
label: Feedback type
options:
- Bug
- Upgrade or migration
- Usability or design
- Performance
- Other
validations:
required: true
- type: input
id: version
attributes:
label: Homarr v2 version or image tag
description: Copy the version from the About page or provide the exact Docker image tag you tested.
placeholder: ghcr.io/homarr-labs/homarr-test:v2
validations:
required: true
- type: input
id: affected-area
attributes:
label: Affected page, feature, or widget
description: Name the exact page, widget, integration, container, sidebar, or action involved.
placeholder: "Board editor: moving a widget into a container"
validations:
required: true
- type: textarea
id: behavior
attributes:
label: What happened, why is it wrong, and what should happen instead?
description: Describe the actual result, why it does not work for you, and the exact result you expected.
placeholder: |
What happened:
Why this is wrong for my use case:
What I expected instead:
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: Exact steps to reproduce
description: Start from the page you opened and list every click, keypress, drag, setting, and value needed to reproduce it.
placeholder: |
1. Open...
2. Set...
3. Drag or click...
4. Observe...
validations:
required: true
- type: textarea
id: visual-evidence
attributes:
label: Screenshots or screen recordings
description: Drag screenshots or a recording into this field. Include the full page for context and a close-up of the problem. Blur secrets and personal data. If you cannot add visuals, explain why.
placeholder: Drag screenshots or a recording here, or explain why none are available.
validations:
required: true
- type: textarea
id: environment
attributes:
label: Device and Homarr environment
description: Provide every detail that could affect the result.
placeholder: |
Device type and model:
Operating system and version:
Browser and version:
Installation method (Docker, Unraid, Kubernetes, other):
Database (SQLite, MySQL, PostgreSQL):
Reverse proxy and version, if used:
validations:
required: true
- type: textarea
id: board-context
attributes:
label: Board and display context
description: Include the layout details even when you are not sure they matter.
placeholder: |
Display resolution:
Browser zoom:
Desktop or mobile layout:
Board column count:
Approximate number of items:
Container or sidebar nesting:
Relevant board or widget settings:
validations:
required: true
- type: textarea
id: additional-information
attributes:
label: Logs and additional information
description: Add relevant browser console errors, Homarr logs, integration details, or anything else that may help. Remove secrets and personal information first.
- type: checkboxes
id: checks
attributes:
label: Before submitting
options:
- label: I added screenshots or a recording where possible, or explained why I could not.
required: true
- label: I described both what happened and the exact result I expected.
required: true
- label: I searched the existing issues for the same feedback.
required: true
- label: I removed passwords, API keys, tokens, and other private information.
required: true
@@ -20,7 +20,7 @@ runs:
using: "composite"
steps:
- name: Prebuilt debian dependencies
uses: ./.github/actions/prebuilt-debian
uses: homarr-labs/homarr/.github/actions/prebuilt-debian@dev
id: prebuilt-debian
with:
architecture: ${{ inputs.architecture }}
@@ -11,6 +11,9 @@ outputs:
runs:
using: "composite"
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
Binary file not shown.

Before

Width:  |  Height:  |  Size: 582 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 649 KiB

+8 -8
View File
@@ -4,18 +4,18 @@
<h3>Homarr</h3>
</div>
**Thank you for your contribution. Please ensure that your pull request meets the following requirements:**
**Thank you for your contribution. Please ensure that your pull request meets the following pull request:**
- [ ] Relevant checks pass without warnings or errors
- [ ] Pull request targets the intended base branch (`dev` by default; `release/v2` for v2 release work)
- [ ] Builds without warnings or errors (`pnpm build`, autofix with `pnpm format:fix`)
- [ ] Pull request targets `dev` branch
- [ ] Commits follow the [conventional commits guideline](https://www.conventionalcommits.org/en/v1.0.0/)
- [ ] No shorthand variable names are used (for example, `x`, `y`, `i`, or abbreviations)
- [ ] User-facing changes include matching documentation updates under `apps/docs`
- [ ] When using AI, no temporary files are checked in and the code style follows the rest of the project
- [ ] No shorthand variable names are used (eg. `x`, `y`, `i` or any abbrevation)
- [ ] Documentation is up to date. Create a pull request [here](https://github.com/homarr-labs/documentation/).
- [ ] When using AI; No temp files are checked in, the code style follows the rest of the project
**When contributing a new integration or widget:**
- [ ] I confirmed manually that the integration or widget is working with a real system and performed basic smoke
tests (e.g. what happens if it is offline)
tests (e.g. what happens if it is offline)
- [ ] Optional but recommended: I confirm that I am willing to be added to the `CODEOWNERS` of this feature and will
provide support and maintain the integration (e.g. in case of breaking or failure)
provide support and maintain the integration (e.g. in case of breaking or failure)
+22 -10
View File
@@ -1,35 +1,47 @@
name: Formatting
name: Auto-fix lint and format
on:
pull_request:
types: [opened, synchronize, reopened]
branches: ["**"]
branches: ["*"]
permissions:
contents: read
contents: write
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
format:
name: Check formatting
autofix:
name: Auto-fix
if: github.event.pull_request.head.repo.id == github.repository_id
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
persist-credentials: false
ref: ${{ github.head_ref }}
- name: Setup
uses: ./tooling/github/setup
- name: Check changed files
- name: Get changed files
id: changed
env:
BASE_REF: ${{ github.base_ref }}
run: |
git diff --name-only --diff-filter=ACMR -z "origin/${BASE_REF}...HEAD" -- \
'*.css' '*.js' '*.json' '*.jsonc' '*.jsx' '*.md' '*.mdx' '*.scss' '*.ts' '*.tsx' '*.yaml' '*.yml' \
| xargs -0 -r pnpm exec oxfmt --check --
git diff --name-only --diff-filter=ACMR "origin/${BASE_REF}...HEAD" -- '*.ts' '*.tsx' '*.js' '*.jsx' > /tmp/changed-files
echo "count=$(wc -l < /tmp/changed-files | tr -d '[:space:]')" >> $GITHUB_OUTPUT
- name: Auto-fix lint + format
if: steps.changed.outputs.count != '0'
run: |
xargs -r -a /tmp/changed-files pnpm exec oxlint --fix || true
xargs -r -a /tmp/changed-files pnpm exec oxfmt --write
- name: Commit fixes
uses: stefanzweifel/git-auto-commit-action@v7
with:
commit_message: "style: auto-fix lint and format"
@@ -20,7 +20,7 @@ jobs:
uses: actions/create-github-app-token@v3
with:
private-key: ${{ secrets.RENOVATE_APPROVE_PRIVATE_KEY }}
client-id: ${{ vars.RENOVATE_APPROVE_CLIENT_ID }}
client-id: ${{ secrets.RENOVATE_APPROVE_APP_ID }}
permission-pull-requests: write # required to enable auto-merge
- name: Install GitHub CLI
+1 -1
View File
@@ -28,7 +28,7 @@ jobs:
uses: actions/create-github-app-token@v3
with:
private-key: ${{ secrets.RENOVATE_APPROVE_PRIVATE_KEY }}
client-id: ${{ vars.RENOVATE_APPROVE_CLIENT_ID }}
client-id: ${{ secrets.RENOVATE_APPROVE_APP_ID }}
permission-pull-requests: write # required to approve a pull request and enable auto-merge
- name: Install GitHub CLI
+73 -216
View File
@@ -3,12 +3,12 @@ name: CI
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
branches: ["**"]
branches: ["*"]
pull_request_target:
types: [closed]
branches: ["**"]
branches: ["*"]
push:
branches: [main, dev, beta, next, v2, release/v2]
branches: [main, dev, beta, next]
merge_group:
workflow_dispatch:
inputs:
@@ -34,10 +34,8 @@ permissions:
contents: read
concurrency:
# Every event for a PR shares one group so a close event cancels stale image
# publication and a newer synchronize run cannot be overwritten by an older run.
group: ${{ github.workflow }}-${{ github.event.pull_request.number || inputs.pr_number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' || github.event_name == 'pull_request_target' || github.event_name == 'workflow_dispatch' || github.ref != 'refs/heads/main' }}
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || inputs.pr_number || github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
env:
FORCE_COLOR: 3
@@ -51,7 +49,7 @@ jobs:
name: Fast gate
if: github.event_name != 'pull_request_target' && (github.event_name != 'workflow_dispatch' || inputs.operation != 'cleanup')
runs-on: ubuntu-latest
timeout-minutes: 20
timeout-minutes: 15
env:
TURBO_API: ${{ secrets.TURBO_API }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
@@ -89,34 +87,21 @@ jobs:
- name: Validate workspace configuration
run: pnpm lint:ws
- name: Validate Custom Widget architecture
run: pnpm check:custom-widgets
- name: Validate checked-in OpenAPI schema
run: pnpm check:openapi
- name: Validate Custom Widget bundle budget
run: |
pnpm turbo run build --filter=@homarr/nextjs
pnpm check:custom-widget-bundle
- name: Run unit tests
run: pnpm test
container:
name: Container build
# Same-repository previews already build amd64; only build a fallback when no digest is available.
needs: [fast-gate, preview-amd64]
name: Container and E2E
needs: fast-gate
if: |
!cancelled() && needs.fast-gate.result == 'success' &&
needs.preview-amd64.result != 'failure' && needs.preview-amd64.outputs.digest == '' &&
(
github.event_name == 'merge_group' ||
(github.event_name == 'push' && github.ref == 'refs/heads/release/v2') ||
(github.event_name == 'pull_request' && github.event.pull_request.draft == false) ||
(github.event_name == 'workflow_dispatch' && (inputs.operation == 'validate' || inputs.operation == 'publish'))
)
github.event_name == 'merge_group' ||
(github.event_name == 'pull_request' && github.event.pull_request.draft == false) ||
(github.event_name == 'workflow_dispatch' && (inputs.operation == 'validate' || inputs.operation == 'publish'))
runs-on: ubuntu-latest
timeout-minutes: 20
timeout-minutes: 30
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v7
if: "!env.ACT"
@@ -124,12 +109,15 @@ jobs:
persist-credentials: false
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || '' }}
- name: Setup
uses: ./tooling/github/setup
- name: Set up Docker Buildx
if: needs.preview-amd64.outputs.digest == '' && !env.ACT
if: "!env.ACT"
uses: docker/setup-buildx-action@v4
- name: Build container once
if: needs.preview-amd64.outputs.digest == '' && !env.ACT
if: "!env.ACT"
uses: docker/build-push-action@v7
with:
context: .
@@ -137,13 +125,15 @@ jobs:
platforms: linux/amd64
load: true
push: false
tags: homarr-ci
tags: |
homarr-ci
homarr-e2e
labels: |
org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
org.opencontainers.image.revision=${{ github.sha }}
org.opencontainers.image.ref.name=pr-${{ github.event.pull_request.number || inputs.pr_number || 'none' }}
cache-from: type=gha,scope=homarr-e2e
cache-to: type=gha,scope=homarr-e2e,mode=max
cache-from: type=gha,scope=homarr-linux-amd64
cache-to: type=gha,scope=homarr-linux-amd64,mode=max
secrets: |
TURBO_API=${{ secrets.TURBO_API }}
TURBO_TEAM=${{ secrets.TURBO_TEAM }}
@@ -165,191 +155,66 @@ jobs:
fi
done
DOCKER_BUILDKIT=1 docker build --platform linux/amd64 \
--tag homarr-ci "${secret_args[@]}" .
--tag homarr-ci --tag homarr-e2e "${secret_args[@]}" .
preview-amd64:
name: Build amd64 preview image
needs: fast-gate
if: |
(
(github.event_name == 'pull_request' && github.event.pull_request.draft == false && github.event.pull_request.head.repo.full_name == github.repository) ||
(github.event_name == 'push' && github.ref == 'refs/heads/release/v2') ||
(github.event_name == 'workflow_dispatch' && inputs.operation == 'publish')
)
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: read
packages: write
outputs:
digest: ${{ steps.preview-build.outputs.digest }}
steps:
- uses: actions/checkout@v7
- name: Cache Playwright browsers
if: "!env.ACT"
id: playwright-cache
uses: actions/cache@v6
with:
persist-credentials: false
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || '' }}
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
- name: Log in to GHCR
if: "!env.ACT"
uses: docker/login-action@v4
- name: Install Playwright Chromium
if: steps.playwright-cache.outputs.cache-hit != 'true' || env.ACT
run: pnpm exec playwright install --with-deps chromium
- name: Install Playwright system deps
if: steps.playwright-cache.outputs.cache-hit == 'true'
run: pnpm exec playwright install-deps chromium
- name: Run E2E tests
run: pnpm test:e2e
- name: Upload E2E artifacts
if: failure() && !env.ACT
uses: actions/upload-artifact@v7
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
name: playwright-report
path: e2e/test-results/
retention-days: 7
- name: Set up Docker Buildx
if: "!env.ACT"
uses: docker/setup-buildx-action@v4
- name: Build and push amd64 preview image by digest
id: preview-build
if: "!env.ACT"
uses: docker/build-push-action@v7
with:
context: .
network: host
platforms: linux/amd64
outputs: type=image,"name=${{ env.PREVIEW_IMAGE }}",push-by-digest=true,name-canonical=true,push=true
cache-from: type=gha,scope=homarr-linux-amd64
cache-to: type=gha,scope=homarr-linux-amd64,mode=max
secrets: |
TURBO_API=${{ secrets.TURBO_API }}
TURBO_TEAM=${{ secrets.TURBO_TEAM }}
TURBO_TOKEN=${{ secrets.TURBO_TOKEN }}
TURBO_REMOTE_CACHE_SIGNATURE_KEY=${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }}
preview-arm64:
name: Build arm64 preview image
needs: fast-gate
if: |
(
(github.event_name == 'pull_request' && github.event.pull_request.draft == false && github.event.pull_request.head.repo.full_name == github.repository) ||
(github.event_name == 'push' && github.ref == 'refs/heads/release/v2') ||
(github.event_name == 'workflow_dispatch' && inputs.operation == 'publish')
)
runs-on: ubuntu-24.04-arm
timeout-minutes: 20
permissions:
contents: read
packages: write
outputs:
digest: ${{ steps.preview-build.outputs.digest }}
steps:
- uses: actions/checkout@v7
if: "!env.ACT"
with:
persist-credentials: false
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || '' }}
- name: Log in to GHCR
if: "!env.ACT"
uses: docker/login-action@v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
if: "!env.ACT"
uses: docker/setup-buildx-action@v4
- name: Build and push arm64 preview image by digest
id: preview-build
if: "!env.ACT"
uses: docker/build-push-action@v7
with:
context: .
network: host
platforms: linux/arm64
outputs: type=image,"name=${{ env.PREVIEW_IMAGE }}",push-by-digest=true,name-canonical=true,push=true
cache-from: type=gha,scope=homarr-linux-arm64
cache-to: type=gha,scope=homarr-linux-arm64,mode=max
secrets: |
TURBO_API=${{ secrets.TURBO_API }}
TURBO_TEAM=${{ secrets.TURBO_TEAM }}
TURBO_TOKEN=${{ secrets.TURBO_TOKEN }}
TURBO_REMOTE_CACHE_SIGNATURE_KEY=${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }}
publish-preview:
name: Publish multi-platform preview image
needs: [fast-gate, preview-amd64, preview-arm64]
if: |
!cancelled() &&
needs.fast-gate.result == 'success' &&
needs.preview-amd64.result == 'success' &&
needs.preview-arm64.result == 'success' &&
(
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) ||
(github.event_name == 'push' && github.ref == 'refs/heads/release/v2') ||
(github.event_name == 'workflow_dispatch' && inputs.operation == 'publish')
)
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
packages: write
pull-requests: read
env:
PREVIEW_TAG: ${{ github.event_name == 'push' && 'v2' || format('pr-{0}', github.event.pull_request.number || inputs.pr_number) }}
steps:
- name: Log in to GHCR
if: "!env.ACT"
uses: docker/login-action@v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
if: "!env.ACT"
uses: docker/setup-buildx-action@v4
- name: Verify PR head before publication
if: github.event_name == 'pull_request'
uses: actions/github-script@v8
- name: Determine preview tag
id: preview
if: |
!env.ACT &&
(
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) ||
(github.event_name == 'workflow_dispatch' && inputs.operation == 'publish')
)
env:
EXPECTED_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
PR_NUMBER: ${{ github.event.pull_request.number || inputs.pr_number }}
run: echo "tag=pr-${PR_NUMBER}" >> "$GITHUB_OUTPUT"
- name: Log in to GHCR
if: steps.preview.outputs.tag != ''
uses: docker/login-action@v4
with:
script: |
const { data: pull } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.payload.pull_request.number,
});
if (pull.state !== "open") {
core.setFailed(`PR #${pull.number} is ${pull.state}; refusing to publish its preview.`);
return;
}
if (pull.head.sha !== process.env.EXPECTED_HEAD_SHA) {
core.setFailed(`PR #${pull.number} moved from ${process.env.EXPECTED_HEAD_SHA} to ${pull.head.sha}.`);
}
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Publish preview manifest
if: "!env.ACT"
- name: Push preview image
if: steps.preview.outputs.tag != ''
env:
AMD64_DIGEST: ${{ needs.preview-amd64.outputs.digest }}
ARM64_DIGEST: ${{ needs.preview-arm64.outputs.digest }}
PREVIEW_TAG: ${{ steps.preview.outputs.tag }}
run: |
tags=(-t "${PREVIEW_IMAGE}:${PREVIEW_TAG}")
docker buildx imagetools create "${tags[@]}" \
"${PREVIEW_IMAGE}@${AMD64_DIGEST}" \
"${PREVIEW_IMAGE}@${ARM64_DIGEST}"
docker tag homarr-ci "${PREVIEW_IMAGE}:${PREVIEW_TAG}"
docker push "${PREVIEW_IMAGE}:${PREVIEW_TAG}"
- name: Verify preview manifest platforms
if: "!env.ACT"
run: |
tags=("${PREVIEW_TAG}")
for tag in "${tags[@]}"; do
image="${PREVIEW_IMAGE}:${tag}"
manifest="$(docker buildx imagetools inspect "$image" --raw)"
for architecture in amd64 arm64; do
jq --exit-status --arg architecture "$architecture" \
'any(.manifests[]; .platform.os == "linux" and .platform.architecture == $architecture)' \
<<<"$manifest" >/dev/null
done
docker buildx imagetools inspect "$image"
done
- name: Show Act publication result
if: env.ACT
run: echo "Act validated the publication path; external GHCR writes are disabled locally."
cleanup:
name: Clean up preview image
@@ -386,14 +251,6 @@ jobs:
core.info(`No package version found with tag "${tag}"`);
return;
}
const targetTags = target.metadata?.container?.tags ?? [];
const extraTags = targetTags.filter((targetTag) => targetTag !== tag);
if (extraTags.length > 0) {
core.warning(
`Package version ${target.id} also carries tag(s) ${extraTags.map((targetTag) => `"${targetTag}"`).join(", ")}; refusing cleanup.`,
);
return;
}
await github.rest.packages.deletePackageVersionForOrg({
package_type: "container",
package_name: packageName,
@@ -22,7 +22,7 @@ jobs:
uses: actions/create-github-app-token@v3
with:
private-key: ${{ secrets.CROWDIN_APP_PRIVATE_KEY }}
client-id: ${{ vars.CROWDIN_CLIENT_ID }}
client-id: ${{ secrets.CROWDIN_APP_ID }}
permission-contents: write # required to commit to crowdin branch
permission-pull-requests: write # required to create pull request
@@ -52,7 +52,7 @@ jobs:
uses: actions/create-github-app-token@v3
with:
private-key: ${{ secrets.RENOVATE_APPROVE_PRIVATE_KEY }}
client-id: ${{ vars.RENOVATE_APPROVE_CLIENT_ID }}
client-id: ${{ secrets.RENOVATE_APPROVE_APP_ID }}
permission-pull-requests: write
- name: Approve PR
env:
@@ -64,7 +64,7 @@ jobs:
uses: actions/create-github-app-token@v3
with:
private-key: ${{ secrets.RENOVATE_MERGE_PRIVATE_KEY }}
client-id: ${{ vars.RENOVATE_MERGE_CLIENT_ID }}
client-id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
permission-contents: write # write to beta branch (due to merge)
permission-pull-requests: write # merge pull request
- id: automerge
+11 -65
View File
@@ -13,7 +13,7 @@ on:
type: boolean
required: false
default: true
description: Send notifications when redeploying an existing release
description: Send notifications
permissions:
contents: write # Required to update package.json version
@@ -38,11 +38,10 @@ jobs:
SKIP_RELEASE: ${{ github.event_name == 'workflow_dispatch' || github.ref_name == 'dev' || github.ref_name == 'next' }}
outputs:
version: ${{ steps.read-semver.outputs.version || steps.version-fallback.outputs.version }}
git_ref: ${{ steps.read-git-ref.outputs.ref || steps.read-semver.outputs.version || github.ref }}
git_ref: ${{ steps.read-git-ref.outputs.ref || github.ref }}
skipped: ${{ env.SKIP_RELEASE }}
upstream_sync: ${{ steps.update-upstream.outcome }}
steps:
- run: echo "Manual dispatch redeploys the latest existing release without creating a new one"
- run: echo "Skipping release for workflow_dispatch event"
if: env.SKIP_RELEASE == 'true'
# The below generated version fallback represents a normalized branch name, for example "feature/branch-name" -> "feature-branch-name"
- run: echo "version="$(echo ${{github.ref_name}} | sed 's/[^a-zA-Z0-9\-]/-/g') >> "$GITHUB_OUTPUT"
@@ -55,13 +54,12 @@ jobs:
uses: actions/create-github-app-token@v3
with:
private-key: ${{ secrets.RENOVATE_MERGE_PRIVATE_KEY }}
client-id: ${{ vars.RENOVATE_MERGE_CLIENT_ID }}
client-id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
permission-contents: write # required to commit package.json & changelog changes, merge them to dev / next and publish the release
- uses: actions/checkout@v7
if: env.SKIP_RELEASE == 'false' || github.ref_name == 'main' || github.ref_name == 'beta'
if: env.SKIP_RELEASE == 'false'
with:
fetch-depth: 0
persist-credentials: false
- uses: pnpm/action-setup@v6
if: env.SKIP_RELEASE == 'false'
@@ -70,29 +68,10 @@ jobs:
with:
node-version: 24.18.0
package-manager-cache: false
- name: Verify major release intent
if: env.SKIP_RELEASE == 'false'
run: |
latest_tag="$(git describe --tags --abbrev=0)"
latest_major="${latest_tag#v}"
latest_major="${latest_major%%.*}"
package_version="$(jq --raw-output '.version' package.json)"
package_major="${package_version%%.*}"
if (( package_major <= latest_major )); then
exit 0
fi
if git log --format='%s%n%b' "${latest_tag}..HEAD" \
| grep --extended-regexp --quiet '(^[[:alnum:]-]+(\([^)]*\))?!:)|(^BREAKING[ -]CHANGE:)'; then
exit 0
fi
echo "::error::package.json declares ${package_version}, but commits since ${latest_tag} contain no breaking-change signal. Use a conventional ! commit or BREAKING CHANGE footer so Semantic Release creates the intended major version."
exit 1
- name: Install dependencies
if: env.SKIP_RELEASE == 'false'
run: pnpm install --frozen-lockfile
run: |
pnpm install
- name: Run Semantic Release
if: env.SKIP_RELEASE == 'false'
env:
@@ -116,10 +95,8 @@ jobs:
run: |
echo "ref=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
- name: Update dev / next branch
id: update-upstream
if: env.SKIP_RELEASE == 'false'
# Finish publishing the already-created release, then fail in the publish job if synchronization did not work.
continue-on-error: true
continue-on-error: true # Prevent pipeline from failing when merge fails
env:
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
UPSTREAM_BRANCH: ${{ github.ref_name == 'main' && 'dev' || 'next' }}
@@ -226,14 +203,9 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v7
if: needs.release.outputs.skipped == 'false'
with:
ref: ${{ needs.release.outputs.git_ref }}
persist-credentials: false
- name: Extract amd64
if: needs.release.outputs.skipped == 'false'
uses: ./.github/actions/extract-build-artifact
uses: homarr-labs/homarr/.github/actions/extract-build-artifact@dev
with:
digest: "${{ env.GHCR_REPO }}@${{ needs.build-amd64.outputs.digest }}"
architecture: amd64
@@ -247,14 +219,9 @@ jobs:
runs-on: ubuntu-24.04-arm
timeout-minutes: 5
steps:
- uses: actions/checkout@v7
if: needs.release.outputs.skipped == 'false'
with:
ref: ${{ needs.release.outputs.git_ref }}
persist-credentials: false
- name: Extract arm64
if: needs.release.outputs.skipped == 'false'
uses: ./.github/actions/extract-build-artifact
uses: homarr-labs/homarr/.github/actions/extract-build-artifact@dev
with:
digest: "${{ env.GHCR_REPO }}@${{ needs.build-arm64.outputs.digest }}"
architecture: arm64
@@ -277,7 +244,7 @@ jobs:
uses: actions/create-github-app-token@v3
with:
private-key: ${{ secrets.RENOVATE_MERGE_PRIVATE_KEY }}
client-id: ${{ vars.RENOVATE_MERGE_CLIENT_ID }}
client-id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
permission-contents: write # required to publish the release
- name: Log in to the Container registry
@@ -314,30 +281,9 @@ jobs:
run: |
gh release edit --repo ${{ github.repository }} ${{ needs.release.outputs.version }} --draft=false
- name: Verify dev / next synchronization
if: needs.release.outputs.skipped == 'false' && needs.release.outputs.upstream_sync != 'success'
run: |
echo "::error::The released commit was not synchronized back to the upstream development branch."
exit 1
- name: Discord notification
if: github.event_name != 'workflow_dispatch' || inputs['send-notifications']
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@0.4.0
with:
args: "Successfully deployed images for branch **${{ github.ref_name }}**. Tagged as **${{env.NEXT_VERSION}}**."
notify-failure:
name: Notify deployment failure
if: ${{ always() && (failure() || cancelled()) && (github.event_name != 'workflow_dispatch' || inputs['send-notifications']) }}
needs: [release, build-amd64, build-arm64, extract-asset-amd64, extract-asset-arm64, publish]
runs-on: ubuntu-latest
timeout-minutes: 2
steps:
- name: Discord failure notification
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@0.4.0
with:
args: "The deployment for branch **${{ github.ref_name }}** failed or was cancelled: [run ${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})"
+11 -31
View File
@@ -19,10 +19,10 @@ permissions:
jobs:
create-and-merge-pr:
runs-on: ubuntu-latest
timeout-minutes: 10
timeout-minutes: 2
steps:
- name: Discord notification
if: ${{ github.event_name == 'schedule' || inputs.send-notifications }}
if: ${{ github.event.inputs.send-notifications }}
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@0.4.0
@@ -38,30 +38,13 @@ jobs:
with:
token: ${{ github.token }}
branch: dev
noNewCommitBehavior: silent
noVersionBumpBehavior: silent
- name: No release needed
if: ${{ steps.semver.outputs.bump == '' || steps.semver.outputs.bump == 'none' }}
run: echo "No release-worthy commits were found since ${{ steps.semver.outputs.current }}." >> "$GITHUB_STEP_SUMMARY"
- name: Create pull request
id: create-pull-request
if: ${{ steps.semver.outputs.bump != '' && steps.semver.outputs.bump != 'none' }}
run: |
pr_url="$(gh pr list --base main --head dev --state open --json url --jq '.[0].url // empty')"
if [[ -z "$pr_url" ]]; then
pr_url="$(gh pr create \
--title "chore(release): automatic release ${{ steps.semver.outputs.next }}" \
--body "**This is an automatic release**.<br/>Manual action may be required for major bumps.<br/>Detected change to be ``${{ steps.semver.outputs.bump }}``<br/>Bump version from ``${{ steps.semver.outputs.current }}`` to ``${{ steps.semver.outputs.next }}``" \
--base main \
--head dev \
--label automerge)"
fi
echo "url=$pr_url" >> "$GITHUB_OUTPUT"
echo "pr_number=${pr_url##*/}" >> "$GITHUB_OUTPUT"
run: 'gh pr create --title "chore(release): automatic release ${{ steps.semver.outputs.next }}" --body "**This is an automatic release**.<br/>Manual action may be required for major bumps.<br/>Detected change to be ``${{ steps.semver.outputs.bump }}``<br/>Bump version from ``${{ steps.semver.outputs.current }}`` to ``${{ steps.semver.outputs.next }}``" --base main --head dev --label automerge'
env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
- name: Discord notification
if: ${{ steps.create-pull-request.outcome == 'success' && (github.event_name == 'schedule' || inputs.send-notifications) }}
if: ${{ github.event.inputs.send-notifications }}
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@0.4.0
@@ -69,29 +52,26 @@ jobs:
args: "Created a release PR ${{ steps.create-pull-request.outputs.url }} for version ${{ steps.semver.outputs.next }} (new behaviour: ${{ steps.semver.outputs.bump }})"
- name: Obtain token
id: obtainApprovalToken
if: ${{ steps.create-pull-request.outcome == 'success' }}
uses: actions/create-github-app-token@v3
with:
private-key: ${{ secrets.RENOVATE_APPROVE_PRIVATE_KEY }}
client-id: ${{ vars.RENOVATE_APPROVE_CLIENT_ID }}
client-id: ${{ secrets.RENOVATE_APPROVE_APP_ID }}
permission-pull-requests: write
- name: Approve PR
if: ${{ steps.create-pull-request.outcome == 'success' }}
env:
GITHUB_TOKEN: ${{ steps.obtainApprovalToken.outputs.token }}
run: |
gh pr review "${{ steps.create-pull-request.outputs.url }}" --approve --body "Automatically approved by GitHub Action"
gh pr review --approve --body "Automatically approved by GitHub Action"
- name: Obtain token
id: obtainMergeToken
if: ${{ steps.create-pull-request.outcome == 'success' }}
uses: actions/create-github-app-token@v3
with:
private-key: ${{ secrets.RENOVATE_MERGE_PRIVATE_KEY }}
client-id: ${{ vars.RENOVATE_MERGE_CLIENT_ID }}
client-id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
permission-contents: write # write to main branch (due to merge)
permission-pull-requests: write # merge pull request
- id: automerge
if: ${{ steps.create-pull-request.outcome == 'success' && steps.semver.outputs.bump != 'major' }}
if: ${{ steps.semver.outputs.bump != 'major' }}
name: automerge
uses: "pascalgn/automerge-action@v0.16.4"
env:
@@ -105,21 +85,21 @@ jobs:
MERGE_REQUIRED_APPROVALS: 0 # do not require approvals
- name: Merged Discord notification
if: ${{ steps.automerge.outputs.mergeResult == 'merged' && (github.event_name == 'schedule' || inputs.send-notifications) }}
if: ${{ steps.automerge.outputs.mergeResult == 'merged' && github.event.inputs.send-notifications }}
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@0.4.0
with:
args: "Merged PR ${{ steps.create-pull-request.outputs.url }} for release ${{ steps.semver.outputs.next }}"
- name: Major Bump Discord notification
if: ${{ steps.semver.outputs.bump == 'major' && (github.event_name == 'schedule' || inputs.send-notifications) }}
if: ${{ steps.semver.outputs.bump == 'major' && github.event.inputs.send-notifications }}
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@0.4.0
with:
args: "The release PR must be manually merged because the next version is a major version: ${{ steps.create-pull-request.outputs.url }} for release ${{ steps.semver.outputs.next }}"
- name: Discord Fail Notification
if: ${{ failure() && (github.event_name == 'schedule' || inputs.send-notifications) }}
if: failure() && github.event.inputs.send-notifications
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@0.4.0
+30 -2
View File
@@ -26,8 +26,36 @@ jobs:
- name: Copy env
run: cp .env.example .env
- name: Generate and validate OpenAPI spec
run: pnpm scripts:update-openapi
- name: Start dev server
run: pnpm --filter @homarr/nextjs dev &
env:
TURBO_TELEMETRY_DISABLED: 1
DISABLE_TELEMETRY: 1
- name: Wait for server and fetch spec
run: |
set -euo pipefail
for i in $(seq 1 30); do
if curl -fsS http://localhost:3000/api/openapi -o /tmp/openapi.json 2>/dev/null; then
echo "Server is up, spec fetched"
break
fi
echo "Waiting for server... ($i/30)"
sleep 3
done
if [ ! -f /tmp/openapi.json ]; then
echo "::error::Server failed to start within timeout"
exit 1
fi
jq -e '.openapi and .paths' /tmp/openapi.json > /dev/null || {
echo "::error::Fetched file is not a valid OpenAPI spec"
exit 1
}
cp /tmp/openapi.json apps/docs/static/api/open-api-schema.json
echo "OpenAPI spec updated ($(jq '.paths | length' /tmp/openapi.json) paths)"
- name: Commit updated spec
uses: EndBug/add-and-commit@v10
@@ -5,7 +5,7 @@ permissions:
on:
pull_request:
branches: ["**"]
branches: ["*"]
paths: [".github/actions/prebuilt-debian/**", "deployments/prebuilt-debian/**"]
jobs:
@@ -5,7 +5,7 @@ permissions:
on:
pull_request:
branches: ["**"]
branches: ["*"]
paths: [".github/renovate.json5"]
jobs:
+34 -1
View File
@@ -5,6 +5,39 @@ on:
types: [released]
jobs:
trigger-docs-release:
name: Trigger Documentation Release
runs-on: ubuntu-latest
timeout-minutes: 2
steps:
- name: Obtain token
id: obtainToken
uses: actions/create-github-app-token@v3
with:
private-key: ${{ secrets.HOMARR_DOCS_RELEASE_APP_PRIVATE_KEY }}
client-id: ${{ vars.HOMARR_DOCS_RELEASE_APP_ID }}
owner: homarr-labs
repositories: |
documentation
permission-contents: write # required to dispatch repository workflow
- name: Trigger documentation release
env:
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
SOURCE_TAG: ${{ github.event.release.tag_name }}
run: |
curl -X POST \
-H "Authorization: token $GITHUB_TOKEN" \
-H "Accept: application/vnd.github+json" \
https://api.github.com/repos/homarr-labs/documentation/dispatches \
-d @- <<EOF
{
"event_type": "trigger-release",
"client_payload": {
"tag": "${SOURCE_TAG}"
}
}
EOF
update-bug-report-template:
name: Update Bug Report Template
runs-on: ubuntu-latest
@@ -15,7 +48,7 @@ jobs:
uses: actions/create-github-app-token@v3
with:
private-key: ${{ secrets.RENOVATE_MERGE_PRIVATE_KEY }}
client-id: ${{ vars.RENOVATE_MERGE_CLIENT_ID }}
client-id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
permission-contents: write # required to commit to branch
permission-pull-requests: write # required to create pr & enable automerge
- name: Checkout code
@@ -1,6 +1,5 @@
permissions:
statuses: write
contents: read
on:
pull_request:
@@ -17,20 +16,12 @@ jobs:
env:
GH_TOKEN: ${{secrets.GITHUB_TOKEN}}
run: |
for attempt in 1 2 3 4 5; do
if gh api \
--method POST \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
/repos/${{github.repository}}/statuses/${{github.event.pull_request.head.sha}} \
-f 'state=success' \
-f 'target_url=https://docs.renovatebot.com/configuration-options#minimumreleaseage' \
-f 'description=Skipped for non renovate branches' \
-f 'context=renovate/stability-days' \
--silent; then
exit 0
fi
echo "status POST failed (attempt $attempt/5), retrying in 5s"
sleep 5
done
exit 1
gh api \
--method POST \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
/repos/${{github.repository}}/statuses/${{github.event.pull_request.head.sha}} \
-f 'state=success' \
-f 'target_url=https://docs.renovatebot.com/configuration-options#minimumreleaseage' \
-f 'description=Skipped for non renovate branches' \
-f 'context=renovate/stability-days'
+6 -3
View File
@@ -15,13 +15,16 @@ jobs:
update-contributors:
runs-on: ubuntu-latest
timeout-minutes: 2
strategy:
matrix:
node-version: [22]
steps:
- name: Obtain token
id: obtainToken
uses: actions/create-github-app-token@v3
with:
private-key: ${{ secrets.HOMARR_UPDATE_CONTRIBUTORS_PRIVATE_KEY }}
client-id: ${{ vars.HOMARR_UPDATE_CONTRIBUTORS_CLIENT_ID }}
client-id: ${{ vars.HOMARR_UPDATE_CONTRIBUTORS_APP_ID }}
permission-contents: write # required to commit to branch
permission-pull-requests: write # required to create pr & enable automerge
@@ -31,10 +34,10 @@ jobs:
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
- uses: pnpm/action-setup@v6
- name: Use Node.js
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v6
with:
node-version: 24.18.0
node-version: ${{ matrix.node-version }}
cache: "pnpm"
- name: Run update script
@@ -24,7 +24,7 @@ jobs:
uses: actions/create-github-app-token@v3
with:
private-key: ${{ secrets.HOMARR_UPDATE_CONTRIBUTORS_PRIVATE_KEY }}
client-id: ${{ vars.HOMARR_UPDATE_CONTRIBUTORS_CLIENT_ID }}
client-id: ${{ vars.HOMARR_UPDATE_CONTRIBUTORS_APP_ID }}
permission-contents: write # required to commit to branch
permission-pull-requests: write # required to create pr & enable automerge
- name: Checkout code
-163
View File
@@ -1,163 +0,0 @@
name: Workshop
on:
pull_request:
paths:
- ".github/workflows/workshop.yml"
- "apps/workshop/**"
- "apps/docs/**"
- "packages/custom-widgets/**"
- "packages/definitions/**"
- "packages/workshop/**"
- "tooling/typescript/**"
- "patches/**"
- "package.json"
- "pnpm-lock.yaml"
- "pnpm-workspace.yaml"
- "turbo.json"
- ".npmrc"
push:
branches: [dev, v2, release/v2, main]
paths:
- ".github/workflows/workshop.yml"
- "apps/workshop/**"
- "apps/docs/**"
- "packages/custom-widgets/**"
- "packages/definitions/**"
- "packages/workshop/**"
- "tooling/typescript/**"
- "patches/**"
- "package.json"
- "pnpm-lock.yaml"
- "pnpm-workspace.yaml"
- "turbo.json"
- ".npmrc"
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ (github.ref_name == 'v2' || github.ref_name == 'release/v2') && 'v2-preview' || github.ref }}
cancel-in-progress: true
jobs:
validate:
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
- uses: actions/checkout@v7
- run: docker compose --env-file apps/workshop/.env.staging.example -f apps/workshop/docker-compose.staging.yml config --quiet
publish-amd64:
if: github.event_name == 'push' && (github.ref_name == 'v2' || github.ref_name == 'release/v2' || github.ref_name == 'main')
needs: validate
runs-on: ubuntu-24.04
timeout-minutes: 20
permissions:
contents: read
packages: write
outputs:
digest: ${{ steps.build.outputs.digest }}
steps:
- uses: actions/checkout@v7
- uses: docker/setup-buildx-action@v4
- uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v7
id: build
with:
context: .
file: apps/workshop/Dockerfile
target: production
platforms: linux/amd64
outputs: type=image,"name=ghcr.io/homarr-labs/workshop",push-by-digest=true,name-canonical=true,push=true
cache-from: type=gha,scope=workshop-linux-amd64
cache-to: type=gha,scope=workshop-linux-amd64,mode=max
publish-arm64:
if: github.event_name == 'push' && (github.ref_name == 'v2' || github.ref_name == 'release/v2' || github.ref_name == 'main')
needs: validate
runs-on: ubuntu-24.04-arm
timeout-minutes: 20
permissions:
contents: read
packages: write
outputs:
digest: ${{ steps.build.outputs.digest }}
steps:
- uses: actions/checkout@v7
- uses: docker/setup-buildx-action@v4
- uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v7
id: build
with:
context: .
file: apps/workshop/Dockerfile
target: production
platforms: linux/arm64
outputs: type=image,"name=ghcr.io/homarr-labs/workshop",push-by-digest=true,name-canonical=true,push=true
cache-from: type=gha,scope=workshop-linux-arm64
cache-to: type=gha,scope=workshop-linux-arm64,mode=max
publish:
if: github.event_name == 'push' && (github.ref_name == 'v2' || github.ref_name == 'release/v2' || github.ref_name == 'main')
needs: [publish-amd64, publish-arm64]
runs-on: ubuntu-24.04
timeout-minutes: 5
permissions:
contents: read
packages: write
env:
WORKSHOP_IMAGE: ghcr.io/homarr-labs/workshop
steps:
- uses: docker/setup-buildx-action@v4
- uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Verify branch head before publication
uses: actions/github-script@v8
with:
script: |
const branchName = context.ref.replace("refs/heads/", "");
const { data: branch } = await github.rest.repos.getBranch({
owner: context.repo.owner,
repo: context.repo.repo,
branch: branchName,
});
if (branch.commit.sha !== context.sha) {
core.setFailed(`Branch ${branchName} moved from ${context.sha} to ${branch.commit.sha}.`);
}
- name: Publish multi-platform image
env:
AMD64_DIGEST: ${{ needs.publish-amd64.outputs.digest }}
ARM64_DIGEST: ${{ needs.publish-arm64.outputs.digest }}
run: |
tags=(-t "${WORKSHOP_IMAGE}:sha-${GITHUB_SHA}")
if [[ "${GITHUB_REF_NAME}" == "v2" || "${GITHUB_REF_NAME}" == "release/v2" ]]; then
tags+=(-t "${WORKSHOP_IMAGE}:v2")
fi
if [[ "${GITHUB_REF_NAME}" == "main" ]]; then
tags+=(-t "${WORKSHOP_IMAGE}:latest")
fi
docker buildx imagetools create "${tags[@]}" \
"${WORKSHOP_IMAGE}@${AMD64_DIGEST}" \
"${WORKSHOP_IMAGE}@${ARM64_DIGEST}"
- name: Verify multi-platform image
run: |
image="${WORKSHOP_IMAGE}:sha-${GITHUB_SHA}"
manifest="$(docker buildx imagetools inspect "$image" --raw)"
for architecture in amd64 arm64; do
jq --exit-status --arg architecture "$architecture" \
'any(.manifests[]; .platform.os == "linux" and .platform.architecture == $architecture)' \
<<<"$manifest" >/dev/null
done
docker buildx imagetools inspect "$image"
-15
View File
@@ -16,7 +16,6 @@ out/
next-env.d.ts
# artifacts
.ai-evaluations/
packages/db/migrations/*/migrate.cjs
# nitro
@@ -25,8 +24,6 @@ packages/db/migrations/*/migrate.cjs
# production
build
tools/homarr-dev/homarr
tools/homarr-dev/homarr-dev
# misc
.DS_Store
@@ -69,7 +66,6 @@ e2e/shared/tmp
#personal backgrounds
apps/nextjs/public/images/background.png
apps/nextjs/public/__widget-editor/
@@ -84,14 +80,3 @@ apps/docs/.docusaurus
apps/docs/build
.pnpm-store
benchmark-results
# local performance investigation (never ship benchmark data or restored backups)
.widget-performance/
.bench/
homarr-backup*.zip
*.sqlite-wal
*.sqlite-shm
/scripts/benchmarks/
# PR description screenshots belong on the pull request, not in the repository
.screenshots/
+2 -2
View File
@@ -2,7 +2,7 @@
"mcpServers": {
"mantine": {
"command": "npx",
"args": ["-y", "@mantine/mcp-server@9.6.0"]
"args": ["-y", "@mantine/mcp-server"]
}
}
}
}
+1 -16
View File
@@ -3,20 +3,5 @@
"printWidth": 120,
"endOfLine": "lf",
"sortPackageJson": true,
"ignorePatterns": [
"**/*.cjs",
"**/lang/**",
"static-data/**",
"CHANGELOG.md",
"apps/docs/.docusaurus/**",
"apps/docs/build/**",
"apps/docs/blog/2022/**",
"apps/docs/blog/2023/**",
"apps/docs/blog/2024/**",
"apps/docs/blog/2025/**",
"apps/docs/blog/authors.yml",
"apps/docs/static/api/open-api-schema.json",
"packages/custom-widgets/src/core/component-catalog.generated.json",
"apps/docs/static/custom-widgets/component-catalog-v1.json"
]
"ignorePatterns": ["**/*.cjs", "**/lang/**", "static-data/**", "CHANGELOG.md", "apps/docs/.docusaurus/**", "apps/docs/build/**", "apps/docs/blog/**"]
}
+1 -29
View File
@@ -30,35 +30,7 @@
"options": {
"reportUnusedDisableDirectives": "off"
},
"overrides": [
{
"files": ["apps/nextjs/src/**/*.ts", "apps/nextjs/src/**/*.tsx"],
"excludeFiles": ["**/*.spec.ts", "**/*.spec.tsx"],
"rules": {
"eslint/no-restricted-imports": [
"error",
{
"paths": [
{
"name": "@homarr/widgets",
"allowTypeImports": true,
"message": "Import a narrow @homarr/widgets export to preserve widget lazy loading."
}
]
}
]
}
}
],
"ignorePatterns": [
"**/*.config.js",
"**/*.config.mjs",
"**/*.cjs",
"**/lang/*.d.json.ts",
".next/**",
"apps/docs/.docusaurus/**",
"apps/docs/build/**"
],
"ignorePatterns": ["**/*.config.js", "**/*.config.mjs", "**/*.cjs", "**/lang/*.d.json.ts", ".next/**", "apps/docs/.docusaurus/**", "apps/docs/build/**"],
"settings": {
"react": { "version": "19" }
}
+17 -19
View File
@@ -7,23 +7,22 @@ homarr/
├── apps/
│ ├── nextjs/ # Main Next.js application (port 3000)
│ ├── docs/ # Docusaurus 3 documentation site (@homarr/docs)
│ ├── tasks/ # Cron-job initialization and scheduling runtime
│ ├── websocket/ # Standalone tRPC WebSocket server (port 3001)
│ └── workshop/ # Go/PocketBase Workshop service
│ ├── tasks/ # Cron job runner + Fastify tRPC API (port 3002)
│ └── websocket/ # Standalone tRPC WebSocket server (port 3001)
├── packages/
│ ├── api/ # tRPC appRouter, procedures, OpenAPI
│ ├── auth/ # NextAuth config, providers, session, API keys
│ ├── db/ # Drizzle schema (3 DB drivers), migrations, queries
│ ├── core/ # Env validation, DB/Redis driver factories, logging
│ ├── definitions/ # Domain enums: WidgetKind, IntegrationKind, permissions
│ ├── widgets/ # Dashboard widget definitions and components
│ ├── widgets/ # All 39 dashboard widgets (definitions + components)
│ ├── integrations/ # Integration classes (HTTP clients to external apps)
│ ├── redis/ # Redis pub/sub channels, caching abstractions
│ ├── translation/ # next-intl setup, locale configs, lang JSON files
│ ├── ui/ # Shared Mantine components, theme, hooks
│ ├── validation/ # Shared zod schemas for API/forms
│ ├── common/ # Shared utilities, IDs, errors
│ ├── cron-jobs/ # Cron job implementations
│ ├── cron-jobs/ # Cron job implementations (25+ jobs)
│ ├── cron-jobs-core/ # Cron scheduling primitives
│ ├── cron-job-status/ # Cron status via Redis
│ ├── boards/ # Board context, edit mode, cache updater
@@ -41,9 +40,8 @@ homarr/
│ ├── analytics/ # Server-side analytics (Umami)
│ ├── server-settings/ # Server setting keys/types
│ ├── settings/ # User-facing settings UI context
│ ├── custom-widgets/ # Custom JSX v2 schema, validation, and runtime
│ ├── onboarding/ # Onboarding studio and setup flow
│ ├── workshop/ # Homarr-side Workshop client and contracts
│ ├── old-import/ # Legacy Homarr import
│ ├── old-schema/ # Legacy Homarr zod schemas
│ └── cli/ # Node CLI for ops (brocli)
├── tooling/
│ ├── typescript/ # Base tsconfig
@@ -63,7 +61,7 @@ When modifying user-facing code, you MUST also update the corresponding document
- New integration → `apps/docs/docs/integrations/<slug>/index.mdx` + `index.ts`
- New widget → `apps/docs/docs/widgets/<slug>/index.mdx` + `index.ts`
- Changed API → `apps/docs/docs/management/api/index.mdx`
- Changed API → `apps/docs/docs/management/api.mdx`
- New/changed env vars → `apps/docs/docs/advanced/`
- New CLI commands → `apps/docs/docs/advanced/command-line/`
- Auth changes → `apps/docs/docs/advanced/` SSO pages
@@ -86,19 +84,19 @@ When modifying user-facing code, you MUST also update the corresponding document
- Lint: oxlint (not ESLint)
- Format: oxfmt (not Prettier)
- UI: Mantine (not Tailwind) — Tailwind is only used in docs app
- Mantine: use the project-scoped MCP server in `.mcp.json` for current v9 APIs, and check `packages/ui/` before creating a new primitive.
- Mantine: use the `mantine` MCP server (configured in `.mcp.json`; tools `get_item_doc` / `get_item_props` / `search_docs`) for current v9 APIs before writing component code. Prefer built-in primitives (`Combobox`/`useCombobox` for selects, the polymorphic `component` prop, `@mantine/hooks`) over hand-rolled equivalents. Check `packages/ui/` for existing conventions first.
## MCP servers
`.mcp.json` at the repo root declares project-scoped MCP servers (the standard Claude Code layout; other tools can import the same `mcpServers` block). See the `mantine` entry there for the Mantine v9 server, and the upstream docs at https://mantine.dev/guides/llms/ for per-tool setup (OpenCode: `.opencode/opencode.json`, Codex: `.codex/config.toml`). Only keyless server definitions belong in `.mcp.json`; anything needing secrets stays in local dotfiles.
## Agent Skills
Portable agent skills live in `.agents/skills/` (the Agent Skills open standard). Cursor, Codex, and OpenCode read `.agents/skills/` natively. Claude Code does not scan it, so `.claude/skills/` is a symlink back to `.agents/skills/`. Install skills at `.agents/skills/<name>/SKILL.md` — that single copy is used everywhere. See `.agents/skills/` for available skills (documentation-sync, mcp-integration, codebase-context, datatable-migration).
- Icons: @tabler/icons-react
- Docs app can import from `@homarr/definitions` for shared types
- Run `pnpm dev:cli -- dev` to browse local `homarr:*` images and remote PR images.
- Run `pnpm dev:cli -- build <name>` from a Homarr checkout to build `homarr:<name>` with rebuild provenance.
- Run `pnpm dev:cli -- build --pr <number>` to build a PR locally from a temporary checkout.
- Run Go checks from `tools/homarr-dev` with `go test ./...` and `go vet ./...`.
## Agent Skills
Portable skills live in `.agents/skills/`. Read the relevant `SKILL.md` before working in that domain; detailed references are loaded only when needed. Claude-compatible discovery is provided through `.claude/skills`.
- `codebase-context` — architecture, package boundaries, and shared utilities
- `documentation-sync` — required user-facing documentation updates
- `mcp-integration` — safe tRPC-to-MCP exposure
- `homarr-custom-widget` — safe Custom JSX v2 authoring
+96
View File
@@ -0,0 +1,96 @@
# Changes
## Goal
Remove widget-only services from Homarr's global Integration system.
The affected services are release providers and Search.ch. They are only used by specific widgets, so keeping them as global integrations added unnecessary setup, UI, database rows, docs pages, and integration package code.
ArchiveTeam Warrior remains a proper integration but is no longer auto-seeded on first install.
## Why
These services do not need shared integration state.
- Release providers are selected per Releases widget repository.
- Search.ch is only used by the Timetable widget.
Moving them into widget options makes configuration simpler and removes dead-weight integration code from `packages/integrations`.
## What Changed
### Release Providers
- Added release provider definitions in `packages/definitions/src/release-provider.ts`.
- Removed release providers from `IntegrationKind`.
- Moved release-fetching code out of `packages/integrations` and into `packages/request-handler/src/release-providers.ts`.
- Updated the Releases widget to store provider configuration directly in widget options:
- `provider`
- `identifier`
- `versionFilter`
- `providerUrl`
- Updated the Releases widget editor to use a provider select instead of an integration select.
- Updated Docker import mapping so `ghcr.io` maps to `gitHubContainerRegistry`.
- GitHub Container Registry now uses the public OCI registry API and accepts full image references such as `ghcr.io/homarr-labs/homarr:latest`.
### Timetable / Search.ch
- Removed Search.ch as an integration.
- Added a Timetable widget `baseUrl` option, defaulting to `https://search.ch`.
- Moved Search.ch request/parsing logic into `packages/request-handler/src/timetable.ts`.
- Updated the Timetable API to receive `baseUrl` directly from widget options.
### ArchiveTeam Warrior
- Kept as a proper integration (not removed).
- Removed `defaultUrl` from the integration definition to prevent auto-seeding on first install.
### Integration Cleanup
- Removed creators, exports, docs references, and implementation files for the deleted integrations.
- Removed unused dependencies from `packages/integrations/package.json`.
### Database Migration
- Added `packages/db/migrations/custom/0003_remove_widget_only_integrations.ts`.
- The migration moves old integration-linked widget config into widget options, then deletes the old widget-only integration rows.
- Updated the earlier release widget migration to use the new `provider` field instead of `providerIntegrationId`.
### Docs
- Updated Releases widget docs to describe providers instead of integrations.
- Updated Timetable docs to remove Search.ch integration setup.
- Removed obsolete integration docs pages for the deleted integration kinds.
- Updated docs sitemap and integration docs mappings.
### Translations
- Added English labels for the new widget options:
- `widget.timetable.option.baseUrl.label`
- `widget.releases.option.repositories.providerUrl.label`
## How It Works Now
Release provider configuration lives on each Releases widget repository entry. The request handler resolves provider metadata and fetches releases directly from the provider-specific endpoint. GHCR uses anonymous OCI registry access for public packages.
Timetable passes its widget URL option directly to its API router and request handler. No global integration lookup is needed.
ArchiveTeam Warrior continues to use the standard integration system, but users must manually add the integration (it is not auto-created on install).
## Deliberate Non-Goals
- No provider secret/token support was added.
- No new secret storage model was introduced.
- No compatibility layer was kept in `packages/integrations`.
Widget options are client-visible, so storing provider credentials there would be wrong. If authenticated provider requests are needed later, they need a separate secret-storage design.
## Verification So Far
- Focused typechecks passed for affected packages.
- Full `pnpm turbo typecheck` passed.
- Full `pnpm lint` passed with existing warnings.
- Focused release provider normalization tests passed.
- Widget translation test passed.
- Live GHCR smoke checks passed for `ghcr.io/muchobien/pocketbase:latest` and `ghcr.io/homarr-labs/homarr:latest`.
- `pnpm test packages/integrations/test/pi-hole.spec.ts` passed earlier, but failed on rebased reruns because Pi-hole reported `domainsBeingBlocked` as `-2`/`0` instead of `> 1`.
+2 -2
View File
@@ -22,7 +22,7 @@ COPY --parents ./packages/definitions/src ./
# isolated linker, patches apply inside the virtual store (node_modules/.pnpm/...)
# which IS created by pnpm fetch. The original hoisted linker is restored
# before the install step so the final node_modules layout stays flat.
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store,sharing=locked \
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store \
pnpm config set store-dir /pnpm/store && \
sed -i 's/nodeLinker: hoisted/nodeLinker: isolated/' pnpm-workspace.yaml && \
pnpm fetch --ignore-scripts && \
@@ -31,7 +31,7 @@ RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store,sharing=locked \
# Install only from the fetched, committed lockfile so local and Docker builds
# resolve the same dependency graph. Serial lifecycle builds avoid esbuild's
# atomic binary replacement racing across the hoisted workspace (pnpm/pnpm#8200).
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store,sharing=locked \
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store \
npm_config_nodedir=/usr/local pnpm install --recursive --offline --frozen-lockfile --child-concurrency=1
COPY . .
+9 -2
View File
@@ -1,3 +1,10 @@
{
"cSpell.words": ["Homarr", "Lidarr", "Nextcloud", "Radarr", "Readarr", "Sonarr"]
}
"cSpell.words": [
"Homarr",
"Lidarr",
"Nextcloud",
"Radarr",
"Readarr",
"Sonarr"
]
}
+11 -18
View File
@@ -21,14 +21,8 @@ pnpm dev:docs
# Or from within this directory
pnpm dev
# Point the docs at a local PocketBase instance
WORKSHOP_API_URL=http://127.0.0.1:8090 pnpm dev:docs
```
The standalone docs use `WORKSHOP_API_URL` for PocketBase and `HOMARR_WEBSITE_URL` for the public site origin. The old
`WORKSHOP_URL` name is a deprecated one-release alias.
## Build
```bash
@@ -46,7 +40,7 @@ Uses oxlint and oxfmt (same as the rest of the monorepo):
```bash
pnpm lint # oxlint
pnpm format # oxfmt (check)
pnpm format:fix # oxfmt (fix)
pnpm format -- --write # oxfmt (fix)
```
## Typecheck
@@ -77,7 +71,7 @@ Each integration/widget doc follows a consistent pattern:
The docs app has `@homarr/definitions` as a workspace dependency. You can import integration kinds, widget kinds, and other definitions directly:
```typescript
import { IntegrationKind } from "@homarr/definitions";
import { IntegrationKind } from '@homarr/definitions';
```
This enables type-safe references to homarr domain concepts within documentation code.
@@ -96,17 +90,16 @@ pnpm verify:search
## Tech Stack
| Layer | Technology |
| -------------- | ------------------------------------------- |
| Layer | Technology |
|---|---|
| Site generator | Docusaurus 3 (`@docusaurus/preset-classic`) |
| Styling | Tailwind CSS 4 + Docusaurus Infima |
| Icons | `@tabler/icons-react` |
| Diagrams | Mermaid (`@docusaurus/theme-mermaid`) |
| Search | Algolia DocSearch |
| Analytics | PostHog |
| Charts | @nivo/line |
| Styling | Tailwind CSS 3 + Docusaurus Infima |
| Icons | `@tabler/icons-react` |
| Diagrams | Mermaid (`@docusaurus/theme-mermaid`) |
| Search | Algolia DocSearch |
| Analytics | PostHog |
| Charts | @nivo/line |
## Contributing
When a Homarr change affects user-facing behavior, update the corresponding documentation in this app. See the
[documentation-sync skill](../../.agents/skills/documentation-sync/SKILL.md) for the code-to-documentation mapping.
When making changes to the homarr codebase that affect user-facing behavior, **always update the corresponding documentation in this app**. See `.cursor/rules/documentation-sync.mdc` for the complete mapping of code changes to doc locations.
@@ -13,6 +13,8 @@ This will be a one time occurrence and version > ``1.0`` will have less breaking
This time more important than every; **read the breaking changes** list and **create a backup** before updating.
Some third party installation methods may take multiple days or weeks to update to the new version.
See [this guide on how to import your previous Homarr dashboard](/docs/getting-started/after-the-installation#importing-a-zip-from-version-before-100)
## Changes at a glance
- **Asynchronous integrations system**: In versions prior to ``1.0``, we fetched data upon each request synchronously.
This means that the user had to wait for the requests to finish so any data would appear.
@@ -10,11 +10,12 @@ If you are still on a version before 0.15.10, please update to 0.15.10 where we
<!-- truncate -->
## Preserve your old installation
## Export your data
Homarr 1.0 is a complete rewrite and cannot reuse the old Docker mounts. Keep the 0.15 installation and its data intact while you build the replacement. The historical `Management` > `Tools` > `Migrate to 1.0` export can still be kept as a record, but current Homarr releases cannot import that ZIP or its secret key.
Plan to recreate your apps, integrations, and boards manually in the new installation. A SQLite database backup can be restored from the onboarding welcome screen only when it was created by a compatible current Homarr release; it is not a conversion path for a 0.15 database.
As 1.0 is a complete rewrite of Homarr and therefor not compatible with the old docker mounts, we need to export your data first.
For this you can navigate to the `Migrate to 1.0` page which you can find under `Management` > `Tools` > `Migrate to 1.0`.
Here you can select which things you want to export and download the zip file.
It will also show you a secret key which you will need to import the secret data like user password hashes, salts and integration secrets.
![Export page](./img/old-homarr-export-data.png)
@@ -27,15 +28,28 @@ Please note that if you are using docker you need to specify a diffrent external
So in your compose file the ports would be `7576:7575` instead of `7575:7575`.
## Import your data
Once the installation is completed you can access the new installation. During the onboarding you'll need to select `Import from Homarr before 1.0`.
:::warning Legacy guide
![Onboarding initial step](./img/onboarding-step-1.png)
The pre-1.0 ZIP importer is no longer included in current Homarr releases. Keep the old installation available until you have manually verified the replacement. Compatible SQLite backups from current releases can still be restored from the onboarding welcome screen.
After that you can upload the zip file you downloaded in the first step.
:::
![Onboarding import dropzone](./img/old-homarr-import-dropzone.png)
After that you'll get a list with all your boards that can be imported. You can configure how it should be imported and can select which screensizes it should import.
![Onboarding import list](./img/old-homarr-import-choose-imported-data.png)
Once you have selected everything you can click on `Confirm import and continue` which will open the encryption key input dialog where you can input the secret key you got during the export.
![Onboarding import key input](./img/old-homarr-import-decryption-token.png)
After that you can continue the onboarding and you should see all your boards and data in the new installation.
See [After the installation](https://homarr.dev/docs/getting-started/after-the-installation) for more information.
## Remove the old installation
After you are happy with the new installation and everything is working as expected you can remove the old installation.
Please make sure to backup the data before you remove the old installation.
Also once the port 7575 is free again you can change the port mapping of the new installation to `7575:7575` if you want to.
Also once the port 7575 is free again you can change the port mapping of the new installation to `7575:7575` if you want to.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 418 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 618 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

@@ -1,167 +0,0 @@
---
authors:
- ajnart
tags: [homarr, update, version, assistant, custom widgets, dashboards, docker]
description: Homarr 2.0 introduces Custom Widgets and Workshop, rebuilt board editing, Assistant tooling, responsive layouts, onboarding, and more.
image: ./img/homarr-v2-dashboard.webp
draft: false
---
# Homarr 2.0.0 - Workshop, Custom Widgets and a rebuilt board
First, thank you.
Many of you used the v2 demo when it was still rough, found bugs and came back to test the next build. Others translated Homarr, answered questions, contributed code or simply kept using it. We would not be shipping this without you.
We are also planning a small batch of **free, limited-edition Homarr swag** for Workshop contributors and a few randomly picked community members. It is not for sale and it is not for profit. No Homarr merch empire, sorry.
Homarr 2.0 is an upgrade, not a brand-new app wearing the same logo. Here is what actually changed.
![A populated Homarr 2.0 dashboard with compact widgets, an Assistant, and an application rail](./img/homarr-v2-dashboard.webp)
<!-- truncate -->
:::warning Back up before upgrading
Database migrations run automatically when Homarr starts. Create a
[Homarr backup](/docs/management/backup) first. Do not test a prerelease image on your only production volume.
Backups are boring. Restores are worse.
:::
## Custom Widgets v2 and Workshop
### Build almost any widget
**This is the big one.** We think it is the most important Homarr update we have made.
Custom Widgets v2 lets you build widgets from JSX, API requests, actions and typed settings. Write one yourself or describe it to Assistant, then test it in the workbench. We think this is enough to make almost any dashboard widget—even that oddly specific component only three people need. Those three people no longer have to wait for us.
This was partly inspired by [Google's upcoming Create My Widget feature](https://blog.google/products-and-platforms/platforms/android/gemini-intelligence/): describe a widget in normal language and let AI make it. Coding knowledge is optional. Knowing what the API does still helps; AI has not fixed bad documentation. Yet.
![The Custom Widget v2 workbench after a live exchange-rate request successfully renders its preview](./img/custom-widget-workbench.webp)
A `homarr-custom-widget-v2` definition can use several fixed-origin API sources. Credentials are encrypted separately and stay out of exports and Workshop submissions. Actions support `GET`, `POST`, `PUT`, `PATCH` and `DELETE`; requests have host checks, timeouts, size limits, permissions and rate limits. Custom Widgets remain **Beta** in 2.0.
### Share widgets and Custom CSS
**Workshop is the other half.** Publish and install Custom Widgets, then update, vote on, comment on and report submissions. Workshop is not only for widgets: you can share your **Custom CSS** there too. Deployment URLs and credentials stay on your Homarr instance.
![The Community Workshop catalog inside Custom Widget management](./img/community-workshop.webp)
:::warning Workshop is moderated
AI makes widgets easy to produce. It does not make the first thing it coughs up worth publishing. **Do not dump untested slop into Workshop.** Test it, add a useful screenshot and explain its setup. Workshop is moderated.
:::
### The ultimate Homarr update
We have called this the **"ultimate Homarr update"**. Not because work stops, but because every new service or widget idea no longer needs a full Homarr release; the community can build and share it instead.
## Board editing, rebuilt
### Drag and drop
We threw out the old drag-and-drop system and rebuilt it on [`dnd-kit`](https://dndkit.com/). Moves and eight-direction resizes are transactional: Homarr previews the result, checks collisions and only saves a valid layout. Bad moves roll back instead of wrecking the board. In short: less fighting with your own dashboard.
![A Calendar tile moving across the rebuilt Homarr 2.0 board grid](./img/drag-and-drop.gif)
### Goodbye Groups and sections, bonjour Containers
Groups and dynamic sections did nearly the same job, so **Containers replace both**. They hold apps, widgets and other Containers. Hold <kbd>Ctrl</kbd> or <kbd>Command</kbd> to move several items at once.
### One board, several layouts
Boards also have Base and Mobile layouts, plus optional breakpoints. **Reset from Base** creates a Mobile starting point without duplicating the board.
## Homarr Assistant
### Work with live Homarr data
Assistant reads live Homarr data and has tools for boards, apps, integrations, widgets, Docker and supported services. It can answer questions, make approved changes and build Custom Widgets. It follows your permissions: reads can run automatically, while changes require approval by default.
![The Homarr Assistant expanded over a live dashboard with health, exploration, media, and board actions](./img/assistant-control-surface.webp)
Open it with <kbd>Shift</kbd> + <kbd>A</kbd>. <kbd>Cmd</kbd>/<kbd>Ctrl</kbd> + <kbd>/</kbd> searches it.
### The Homarr provider
:::note
Assistant ships with a **"Homarr provider"**. It is free, will never offer paid plans, and is paid for by [Ajnart](https://github.com/ajnart) using the ad revenue from this website.
It only uses zero-data-retention (ZDR) providers. [The provider is open source](https://github.com/homarr-labs/homarr/blob/release/v2/apps/workshop/homarr_provider.go), and neither prompts nor responses are logged. We are not selling your data. Pinky promise.
:::
### External assistants through MCP
External assistants can use the same permission-checked tools through `/api/mcp`, with an API key or OAuth 2.1 with PKCE.
## The rest (there is a lot)
### Brand-new onboarding
A six-stage setup studio handles the first admin, core settings, Docker discovery, apps, integrations and the first board.
![The Homarr 2.0 setup studio on its Essentials step, with all six setup stages shown](./img/onboarding-studio.webp)
### Board switcher
Boards now live in a searchable gallery with keyboard navigation. Open it from the header or press <kbd>Shift</kbd> + <kbd>C</kbd>.
![A visual board card inside the searchable Homarr board switcher](./img/board-switcher.webp)
### Command menu and keybinds
<kbd>Cmd</kbd>/<kbd>Ctrl</kbd> + <kbd>K</kbd> finds resources, pages, settings, users, groups and commands. More
keybinds cover Assistant, editing, selection and advanced widget views.
### Customizable header
Pick which buttons appear, put them on the left, center or right, and change their order.
![The Header studio showing left, center, and right zones plus available controls](./img/header-studio.webp)
### Instance branding
Change the name, logo, favicon, colors, corner styles and login page. Global CSS applies instance-wide; board CSS can override it. Only give that power to admins you trust. CSS remains CSS.
![Instance branding controls for application identity, palette, corner style, and global CSS](./img/instance-branding.webp)
### Widgets
Add content without leaving the board. Settings have live previews, and supported widgets have advanced views. Weather and Clock were redesigned; Air Quality, Countdown and Timer are new.
### Docker and Podman
`DOCKER_ENDPOINTS` supports several hosts. Assisted setup and container labels can create apps, integrations, Containers, widgets and health checks.
![Assisted Docker setup listing discovered services that can become apps or integrations](./img/docker-assisted-setup.webp)
Homepage-compatible labels work too.
### Permissions
Groups use a new Viewer, Editor and Admin matrix. The API, Assistant and MCP follow the same rules. LDAP can create users on first sign-in; OIDC can link verified accounts and synchronize groups.
### Performance
Board startup work runs in parallel, heavy screens load when opened, and identical integration requests share work instead of doing it twice.
## Before upgrading
- Keep a backup and use the final release image, not a prerelease tag.
- Legacy Custom JSX definitions enter a migration state. Homarr keeps encrypted credentials and an archived copy until the v2 definition validates.
- Mobile layouts place rail content in the main flow instead of displaying rails.
- Custom Widgets remain Beta.
- Assistant and MCP do not bypass permissions.
## Links
- Try the [Homarr 2.0 preview](https://app-v2.preview.homarr.dev).
- Follow the [v2 beta setup guide](/docs/getting-started/upgrade-v2).
- Follow the [v2 release pull request](https://github.com/homarr-labs/homarr/pull/6545) and [public beta thread](https://github.com/homarr-labs/homarr/issues/6600).
- Compare [v1.76.2 with the release/v2 branch](https://github.com/homarr-labs/homarr/compare/v1.76.2...release/v2).
- Read the guides for [Boards](/docs/management/boards), [Custom Widgets](/docs/management/custom-widgets), [Assistant](/docs/management/assistant), [MCP](/docs/management/mcp) and [Docker labels](/docs/integrations/docker-labels).
-24
View File
@@ -1,24 +0,0 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "base-nova",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/css/custom.css",
"baseColor": "mist",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "tabler",
"rtl": false,
"menuColor": "default",
"menuAccent": "subtle"
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"label": "Advanced",
"collapsed": true,
"position": 5,
"position": 4,
"link": {
"type": "generated-index",
"title": "Advanced Concepts or features"
@@ -1,4 +1,4 @@
{
"collapsed": true,
"label": "Command line interface"
}
}
@@ -1,180 +1,41 @@
# Developer Docker CLI
The host-side developer CLI builds and runs local Homarr images and pull-request previews with non-blocking background operations. Run it directly from a checkout with `pnpm dev:cli -- ...`; no shell-specific setup script is required. It is separate from the recovery CLI included in production images.
The host-side developer CLI builds and runs local Homarr images and pull-request previews. Run it directly from a checkout with `pnpm dev:cli -- ...`; no shell-specific setup script is required. It is separate from the recovery CLI included in production images.
To install an optional `homarr` binary, run `pnpm dev:cli:install` and make your Go bin directory available on `PATH`. GitHub CLI authentication (`gh auth login`) is only needed for pull-request features.
To install an optional `homarr` binary, run `pnpm dev:cli:install` and make your Go bin directory available on `PATH`. GitHub CLI authentication is only needed for pull-request features.
## Requirements
- Docker or OrbStack
- Go 1.25+ (when building the CLI from source)
- GitHub CLI authenticated with `gh auth login` (for pull-request and CI features)
- A [Nerd Font](https://www.nerdfonts.com/) enabled in your terminal (recommended for icons, status indicators, and TUI glyphs)
## Interactive Development Browser
## Browse development images
```bash
pnpm dev:cli --
# or with global binary:
homarr
```
The main browser combines local `homarr:*` images with open pull requests and their GHCR preview status. Select an image to start or stop it, use `R` to build a remote pull request locally in the background or rebuild a local image, or use `p` to force a remote preview pull. Press `d` (or `1`/`2`) to toggle between the image browser and the instance dashboard.
The main screen combines local `homarr:*` images with open pull requests and their GHCR preview status. Select an image to start or stop it, use `R` to build a remote pull request locally in the background or rebuild a local image, or use `p` to force a remote preview pull. Press `d` to switch to the instance dashboard and `d` again to return without losing the browser state. The explicit `dev` and `dash` subcommands start on either screen.
A locally built PR image is folded into its remote PR row only when its recorded revision matches the current GitHub head SHA (marked `◆ local`). When the matching GHCR image is also available, pressing `Enter` opens a local/remote selector with local preselected. Outdated local builds remain separate rows.
A locally built PR image is folded into its remote PR row only when its recorded revision exactly matches the current GitHub head SHA. The row is marked `◆ local`, and starting it prefers that local image without a registry pull. Outdated local builds remain separate rows. Navigation and dashboard switching remain available during background builds.
### Multi-Source Sidebar
Press `Tab` to cycle between sidebar tabs:
- **`[logs]`**: Live container output streams (press `f` to follow/pause, `L` to pin to the selected container, `Page Up`/`Page Down` to scroll).
- **`[build]`**: Real-time BuildKit step streaming for active local builds.
- **`[ci]`**: GitHub Actions CI check statuses, run duration, and workflow links for pull requests.
### Background Task Overlay
Pulls and builds run asynchronously in the background. Press `t` to open the tasks overlay, showing:
- Layer-by-layer download progress and transfer speeds (via Docker Engine API).
- Active build stages and logs.
- Task cancellation and status.
### Browser Shortcuts
| Key | Action |
| ---------------------- | --------------------------------------------------------------------------------------------- |
| `/` | Filter rows by PR number, title, author, branch, CI, image, state, or port |
| `↑` / `↓` or `j` / `k` | Navigate rows |
| `Enter` or `Space` | Start the available image; choose local or remote when both exist; stop if running |
| `R` | Build locally in background (or rebuild local image) |
| `p` | Pull remote PR image in background and start container |
| `m` | Manage mode (`b` rebuild, `i` delete image, `d` delete data, `c` remove container, `p` prune) |
| `t` | Toggle background tasks overlay |
| `Tab` | Cycle sidebar tabs (`[logs]`, `[build]`, `[ci]`) |
| `l` / `L` | Toggle sidebar visibility / Pin logs to selected instance |
| `f` | Follow or pause logs |
| `M` | Toggle demo mode for next launch |
| `b` | Toggle bot pull requests |
| `o` / `a` | Open PR on GitHub / Open running instance in browser |
| `c` | Copy instance URL to clipboard |
| `r` | Refresh data |
| `1` / `2` / `d` | Switch screen (development browser / instance dashboard) |
| `?` / `q` | Help overlay / Quit (cancels active background tasks) |
## Instance Dashboard
## Build images
```bash
homarr dash
# or
pnpm dev:cli -- dash
pnpm dev:cli -- build <name>
pnpm dev:cli -- build --pr <number> [name]
```
The instance dashboard displays all running and stopped Homarr containers, published ports, health states, and resource volumes.
The first command builds the current checkout as `homarr:<name>`. The pull-request form uses a depth-one clone in a temporary directory and defaults to `homarr:pr-<number>` when `name` is omitted.
| Key | Action |
| ------------------ | ---------------------------------------- |
| `Enter` or `Space` | Start or stop selected instance |
| `s` / `S` | Stop / Restart instance |
| `x` | Remove container (preserves data volume) |
| `m` | Manage mode for selected instance |
| `R` | Rebuild from recorded build provenance |
## CLI Commands
The commands below use the installed `homarr` binary. Without global installation, prefix each command with `pnpm dev:cli --` (for example, `pnpm dev:cli -- list`).
### Launch Instances
## Rebuild images
```bash
homarr run dev
homarr run --detach dev
homarr run --pr 6441
homarr run --pr 6441 --demo
homarr --pr 6441 --env WORKSHOP_WEB_URL=https://app-v2.preview.homarr.dev/
pnpm dev:cli -- rebuild <name>
```
- `--pr <number>`: Pull and run a PR image from GHCR (shorthand: `homarr --pr <number>`).
- `--demo` (`-m`): Start with mock integrations and sample data.
- `--detach`: Start container in background.
- `--env` (`-e`): Pass additional environment variables (repeatable: `--env KEY=VALUE`).
Images built by this CLI record their source checkout, revision, and optional pull-request number. Rebuild uses that provenance to select the original checkout or create a fresh temporary pull-request checkout.
### Build & Rebuild
## Run images
```bash
# Build current checkout as homarr:<name>
homarr build feature
# Build a pull request locally in a temporary checkout
homarr build --pr 6441
# Rebuild an existing image from recorded provenance
homarr rebuild feature
homarr run <name>
homarr run --pr <number>
```
Images built with the CLI record their source checkout, revision SHA, and optional PR number. `homarr rebuild` reads this provenance to reconstruct the build without manual configuration.
### GitHub CI Checks
```bash
# View CI checks for a specific PR
homarr ci 6441
# View CI checks for the current branch
homarr ci
# Watch CI checks until completion
homarr ci 6441 --watch
```
### Interactive Shell
```bash
# Open interactive shell in running container
homarr shell homarr-dev
# Run specific command in container
homarr shell homarr-dev pnpm db:migration:sqlite:run
```
### Image & Volume Management
```bash
# List local images with build provenance (source, revision, PR)
homarr images
# Delete local image
homarr images rm feature
# List data volumes with owner PR/tag and in-use status
homarr data
# Preview deleting a data volume and its owning container
homarr data rm homarr_feature_data
# Permanently delete the data after reviewing the preview
homarr data rm homarr_feature_data --force
# Remove stopped containers (prompts without --force)
homarr prune --force
```
Forced data deletion removes the derived owner container only when it mounts the exact selected volume. A same-named container that does not own that volume stops the operation.
### Instance Operations & Diagnostics
```bash
# List instances in a script-friendly table
homarr list
# Follow container logs
homarr logs homarr-dev
# Stop, restart, remove, or open container in browser
homarr stop homarr-dev
homarr restart homarr-dev
homarr remove homarr-dev
homarr open homarr-dev
# Verify Docker daemon, Engine API, GHCR access, and gh CLI authentication
homarr doctor
```
Add `--detach` to run in the background, `--demo` to enable demo data, or repeat `--env KEY=VALUE` to provide additional environment variables.
@@ -14,4 +14,4 @@ homarr fix-usernames
This will convert all credential usernames to lowercase and update the database accordingly.
![Terminal output confirming credential usernames were converted to lowercase](./img/screenshot-cli.png)
![](./img/screenshot-cli.png)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 768 KiB

+18 -204
View File
@@ -1,213 +1,27 @@
# Command line interface
The Homarr image includes a recovery CLI for operations that cannot be completed in the web interface. It is not an
application API; use the [API](/docs/management/api) or [MCP endpoint](/docs/management/mcp) for automation.
The Homarr image ships with a built-in recovery tool. This can be useful, if you do not have access to Homarr or if something is not working.
Please note that the CLI is an *emergency tool*, not an API tool.
Back up the database and stop Homarr before running a recovery command that changes database records directly.
Widget project commands and authenticated preview requests can run while Homarr is online.
Failed commands print an error and return a non-zero exit code, so shell scripts can stop safely.
:::caution
## Run the recovery CLI
Using the recovery CLi tool can cause problems when requests are being sent to Homarr while the tool is running. \
You should also create a backup of the database, in case you break something.
Open a shell in the container, then pass a command to `homarr`:
:::
```sh
docker exec -it <container-id> /bin/bash
homarr <command>
```
## Execute CLI with Docker
### QNAP Container Station
1. Execute ``docker ps | grep homarr`` on the root system with Docker to find the container ID of your Homarr container.
2. Run ``docker exec -it <container-id> /bin/bash homarr <your-command-goes-here>``. \
Replace ``<container-id>`` with your container ID from step 1. \
Replace ``<your-command-goes-here>`` with the command you want to execute (see sub-pages).
1. Open **Container Station**.
## Execute CLI with QNAP
![Container Station in the QNAP application menu](./img/334116069-e7ec4726-3df7-4dc6-a3e0-b745e7eef17f.png)
2. Select **Containers**.
![Containers menu in QNAP Container Station](./img/334116205-aa09ce40-0701-4f7d-b9f9-b431b9d207c5.png)
3. Select the Homarr container.
![Homarr container selected in QNAP Container Station](./img/334116303-714b5e07-b9de-4ec0-a2bc-def7ee38750f.png)
4. Select **Execute**.
![Execute action for the Homarr container in QNAP Container Station](./img/334116411-df1482f9-3aa9-4686-b77f-e0dca09e60b2.png)
5. Select `/bin/bash`, then select **Execute**.
![Bash selected in the QNAP execute console](./img/334116518-e1ddb515-99c7-4159-81bf-76e889d1e808.png)
6. Run `homarr <command>` in the console.
![Bash console open for Homarr in QNAP Container Station](./img/334116642-08c4f204-5cd0-4e4e-a785-0142e1b279ca.png)
See the pages in this section for available commands.
## Build and share custom widget packages
The `widgets` commands accept a project directory, a v3 package JSON file, or an offline archive. Scaffolding,
validation, compilation and packing do not need a running Homarr database.
Create a project to edit in your own IDE:
```sh
homarr widgets init my-clock --example clock --id example.my-clock --name "My clock"
homarr widgets validate my-clock
homarr widgets pack my-clock
```
`init` (also `scaffold`) creates a new directory with `homarr-widget.json` and normal TSX, TypeScript, CSS and asset
files. It refuses to overwrite an existing directory. Choose `clock`, `timer`, `beszel`, `checklist`, `downloads`,
`sonarr`, `rack`, `backup`, `household`, `overview`, `media`, `sqlite` or `mqtt` with `--example`; leaving out `--id` generates a unique
local package identity. Edit the manifest and files, then repeat validation and packing. The manifest carries entrypoints,
handlers, connection requirements, options and exact dependency versions; it does not embed the files map in this layout.
Directory loading skips hidden files, symlinks, `node_modules`, `dist`, `coverage`, generated archives and root-level
`package.json`, `package-lock.json` and `tsconfig.json`. Keep all widget source under supported TS/JS, JSON, CSS, SVG,
text, Markdown, image or font files. Build output defaults to `dist/widget.artifact.json`; packing defaults to
`dist/widget.homarr-widget.json`. Remove or rename an existing output before rebuilding, or pass a new `--output`.
The same commands also accept the single-file format:
```sh
homarr widgets validate widget.json
homarr widgets build widget.json --output widget.artifact.json
homarr widgets pack widget.json --output widget.homarr-widget.json
homarr widgets validate widget.homarr-widget.json
```
`validate` checks the package structure or an archive's source and artifact checksums. It does not execute widget code.
`build` compiles JSX/TSX, local modules, CSS and assets into browser and optional server output. `pack` includes the
source, compiled output and dependency lock in one JSON archive for offline installation. Output files must not already exist.
Packages with third-party dependencies need `--trust` to install their exact declared versions:
```sh
homarr widgets pack widget.json --trust
```
Dependency installations are cached in `.homarr-widgets` beside the input file. Installation scripts are disabled;
packages requiring native build steps are not supported by these commands. Compilation does not start the widget's
server handlers. Installed trusted widgets execute with Homarr's privileges; source and artifact checksums establish
integrity, not publisher authenticity. Local connection bindings, credentials and widget state are separate from the archive.
### Preview a local project in Homarr
Set `HOMARR_API_KEY` to an admin API key from your own Homarr instance, then run:
```sh
homarr widgets preview my-clock --url http://homarr.local:7575 --trust
```
The command creates a disabled installation on first use, compiles the current local source on that Homarr instance,
and prints a link to its exact preview session. Open the link while signed in as the API key owner. Repeating the command
replaces the previous preview; it does not activate or overwrite the installation's saved draft. Use `--save-draft` to
also save your local source. Use `--installation-id ID` to target an existing installation. The local
`.homarr-widget.local.json` file remembers installation and preview IDs and is excluded from scaffolded Git projects.
Pass `--bindings bindings.json` for a JSON object mapping declared connection names to existing Homarr connection IDs,
and `--options options.json` for preview option values. Credentials remain in Homarr. `--live-actions` explicitly allows
real mutations during this preview; queries and subscriptions can already execute trusted server code. `--trust` is
required on every invocation. Set `HOMARR_URL` instead of `--url`, or select a different secret variable with
`--api-key-env VARIABLE_NAME`. Preview compilation may install declared dependencies after this explicit trust choice.
Use `--scenario NAME` to select synthetic query data declared in the package's `previewScenarios`. This skips server
startup and configured service requests; unprovided query fixtures fail explicitly and subscriptions are unavailable.
SDK actions stay simulated, so `--scenario` cannot be combined with `--live-actions`. Trusted browser code and dependency
compilation keep their normal privileges. See [synthetic preview scenarios](../../management/custom-widgets/trusted-packages.mdx#synthetic-preview-scenarios).
### Publish the exact offline archive
Set `HOMARR_WORKSHOP_TOKEN` to an authenticated Workshop token, then publish the archive you reviewed:
```sh
homarr widgets publish my-clock/dist/widget.homarr-widget.json
homarr widgets publish next-release.homarr-widget.json --submission-id LISTING_ID --expected-revision 1
```
Use `--url` (or `WORKSHOP_API_URL`) for your own Workshop service, `--token-env` for another token variable,
`--changelog` for release notes, and `--forked-from RELEASE_ID` to attribute a new fork. Existing listings require their
current revision and a new package version. Forks need their own package identity. Published releases are immutable;
publication sends the archive without rebuilding it. You can also import the archive into Homarr and publish through
the package workspace.
### Run a package on another machine
Run an already packed archive on another machine with the optional widget runner:
```sh
export HOMARR_WIDGET_RUNNER_TOKEN="$(openssl rand -hex 32)"
homarr widgets serve widget.homarr-widget.json --trust --host 127.0.0.1 --port 8787
```
`serve` executes the installed package with that machine's operating system privileges. It never downloads packages
or accepts source uploads. Set a custom token variable with `--token-env VARIABLE_NAME`. The token must have at least
32 characters. Interrupting the command stops its worker and subscriptions. See [remote widget runners](../../management/custom-widgets/remote-runners.mdx)
for connection setup, local files, SQL, MQTT, and browser-service adapters.
### Share several widgets as a collection
A collection contains independent compiled widget packages and a shared connection blueprint. Write a collection
manifest with paths to already packed archives:
```json
{
"manifest": { "id": "example.home", "version": "1.0.0", "name": "Home dashboard" },
"connections": { "monitor": { "label": "Monitoring server", "kind": "integration" } },
"widgets": [
{ "key": "clock", "archive": "clock/dist/widget.homarr-widget.json" },
{
"key": "servers",
"archive": "beszel/dist/widget.homarr-widget.json",
"connections": { "beszel": "monitor" }
}
]
}
```
Pack and verify it without compiling or executing the member packages:
```sh
homarr widgets collection pack collection.json --output home.homarr-collection.json
homarr widgets collection validate home.homarr-collection.json
homarr widgets collection import home.homarr-collection.json --url http://homarr.local:7575
```
`import` uses `HOMARR_API_KEY`, or the variable selected with `--api-key-env`. Pass `--bindings bindings.json` to map
each collection slot to an existing local Homarr connection once; all members that reference that slot receive the
binding. Omitted connections can be configured later. Imports create every member as a disabled draft in one database
transaction and print their review links. Trust and activation are separate, explicit steps.
Collections support 1–32 members and up to 128 MiB. Package identities, compiled artifacts, dependency locks and
documented Workshop/fork attribution travel with the archive. The collection and each nested artifact have integrity
checksums. Local connection URLs, IDs and credentials, board placements, stored widget data and local configuration
are excluded. Authored `options` on a member are portable defaults: include only values you intend to share. Package
source is included verbatim, so embedded literals are still part of the archive.
## Build a local branch harness
The repository's host-side CLI can build the current checkout and run it as a named Docker harness:
![Homarr developer CLI showing pull requests, local images, and PR CI status](./img/homarr-dev-cli.png)
```sh
pnpm cli harness setup feature-board
```
This creates `homarr:feature-board`, starts it with persistent demo data, and prints the assigned URL. Query the port
again with:
```sh
pnpm cli harness port feature-board
```
Pass environment overrides with repeated `--env` options:
```sh
pnpm cli harness setup feature-board \
--env WORKSHOP_WEB_URL=https://example.test \
--env FEATURE_FLAG=true
```
Related commands are `harness build <name>`, `harness run <name>`, and `harness stop <name>`.
1. Open Container Station in the QNAP Web Interface ![](img/334116069-e7ec4726-3df7-4dc6-a3e0-b745e7eef17f.png)
2. Select Containers in the menu bar on the left ![](img/334116205-aa09ce40-0701-4f7d-b9f9-b431b9d207c5.png)
3. Select your Homarr Container ![](img/334116303-714b5e07-b9de-4ec0-a2bc-def7ee38750f.png)
4. Click "Execute" in the top right of the Window. ![](img/334116411-df1482f9-3aa9-4686-b77f-e0dca09e60b2.png)
5. Select /bin/bash and click Execute ![](img/334116518-e1ddb515-99c7-4159-81bf-76e889d1e808.png)
6. Run the Homarr command and press enter (eg. homarr reset-owner-password) ![](img/334116642-08c4f204-5cd0-4e4e-a785-0142e1b279ca.png)
@@ -7,7 +7,6 @@ This guide requires that you understand [how to access the CLI](../).
:::
Using these commands, you can manage the integrations from the CLI:
```bash
Group of commands to manage integrations
@@ -44,7 +43,7 @@ Flags:
## Delete Integration
- Requires a database connection
- Requires one parameter: `id` or `name`
- Requires one parameter: ``id`` or ``name``
- Deletes an integration permanently from the database
```bash
@@ -58,4 +57,4 @@ Flags:
-n, --name string Name of the integration
-h, --help help for integrations delete
-v, --version version for homarr-cli
```
```
@@ -10,10 +10,10 @@ Using this command, you can forcefully reset the password of a user.
It will be set to a random value and the hash for the user will be updated.
All sessions for that user will be terminated and the user must re-login.
![Reset-password command help showing that username is required](./img/screenshot-cli.png)
![](./img/screenshot-cli.png)
Using the `--username` parameter, you can specify who you want to reset the password for.
Using the ``--username`` parameter, you can specify who you want to reset the password for.
Executing the command will create a new random password, update the database and display the new password.
All sessions for this user will be terminated and they must re-login.
![Terminal output confirming a password reset and session deletion](./img/screenshot-cli-result.png)
![](./img/screenshot-cli-result.png)
@@ -7,7 +7,6 @@ This guide requires that you understand [how to access the CLI](../).
:::
Using these commands, you can manage the users from the CLI:
```bash
Group of commands to manage users
@@ -43,7 +42,7 @@ yj52ek5cyb6du4bp4osbkjry admin xxxx@example.com
## Delete User
- Requires a database connection
- Requires one parameter: `id` or `username`
- Requires one parameter: ``id`` or ``username``
```bash
Delete a user by id or username
@@ -77,4 +76,4 @@ Flags:
-p, --password New password
-h, --help help for users update-password
-v, --version version for homarr-cli
```
```
@@ -1,8 +1,9 @@
{
"label": "Developer guides",
"position": 6,
"collapsed": true,
"link": {
"type": "generated-index"
"label": "Developer guides",
"position": 6,
"collapsed": true,
"link": {
"type": "generated-index"
}
}
}
@@ -8,99 +8,74 @@ tags:
sidebar_position: 1
---
## Set up your development environment
## Setting up your development environment
This guide will help you set up your development environment to start contributing to homarr.
### Prerequisites
- **Node.js 24.18 or newer**: You can download it from the [official website](https://nodejs.org/).
- **Corepack**: Run `corepack enable` so the repository selects its pinned pnpm 11 version.
- **Corepack**: Run `corepack enable` so the repository selects its pinned pnpm version.
- **Git**: You can download it from the [official website](https://git-scm.com/).
- **Docker or Docker Desktop**: Required for the Redis service and the developer Docker CLI.
- **Go 1.25+**: Required only for the developer Docker CLI. You can download it from the [official website](https://go.dev/).
- **GitHub CLI**: Required only for pull-request image features. Install it from the [official website](https://cli.github.com/), then authenticate with `gh auth login`.
The versions in the root `package.json` are the supported baseline. Let Corepack select the repository version instead
of installing an unrelated global pnpm:
### Setting up the project
```sh
corepack enable
corepack install
pnpm install --frozen-lockfile
```
1. Fork the repository by clicking on the "Fork" button on the top right corner of the repository page.
### Run the Homarr application
![Fork button](./img/fork.png)
1. Copy `.env.example` to `.env`.
2. Set `DB_URL` to an absolute writable SQLite file path.
3. Keep Redis and the optional local databases running in one terminal.
4. Apply migrations and start Next.js in another terminal.
2. Clone the repository to your local machine with `git clone`. You can find the url to use by clicking on the "Code" button on the top right corner of your forks repository page.
```sh
cp .env.example .env
```
![Clone button](./img/clone.png)
Terminal 1:
3. Change directory to the project folder with `cd homarr`.
```sh
pnpm docker:dev
```
4. Install the dependencies with `pnpm install`.
Terminal 2:
5. Create `.env` from `.env.example`. In a POSIX shell, run `cp .env.example .env`; in PowerShell, run `Copy-Item .env.example .env`. Set `DB_URL` to an absolute path for your SQLite file.
```sh
pnpm db:migration:sqlite:run
pnpm dev
```
6. Start Redis in the background with `pnpm docker:dev:up` if your development workflow needs it.
The infrastructure command remains in the foreground. Open `http://127.0.0.1:3000`; the first user created through
Homarr becomes the instance administrator.
Use `pnpm docker:dev:up` instead when you only need Redis in the background. Running the migration also seeds the
default data; use `pnpm db:seed` to seed it explicitly later.
7. Create or update the database with `pnpm db:migration:sqlite:run`. This also seeds the default data. To run the seed explicitly later, use `pnpm db:seed`.
Workshop contributors can set up PocketBase and Docusaurus with the dedicated
[Workshop development guide](./workshop).
8. Start the development server with `pnpm dev` and wait for a message that says `✓ Ready in 10.7s`
### Useful commands
9. Open your browser and navigate to [http://localhost:3000](http://localhost:3000). (Loading the page might take up to 2 minutes the first time)
- `pnpm dev` — Next.js on port 3000.
- `pnpm dev:benchmark` — authenticated-board benchmark; readiness-only `--smoke` runs are not performance claims.
- `pnpm dev:cli -- dev` — browse and run local images and remote pull-request images.
- `pnpm dev:cli -- build <name>` — build the current checkout as `homarr:<name>`.
- `pnpm dev:cli -- build --pr <number>` — build a pull request from a temporary checkout.
- `pnpm dev:cli -- rebuild <name>` — rebuild an image from its recorded checkout or pull request.
- `pnpm dev:cli:install` — optionally install the developer CLI as a `homarr` binary.
- `pnpm dev:docs` — Docusaurus only on port 3003.
- `pnpm docker:dev:up` — Redis development service in the background.
- `pnpm cli` — Homarr operations CLI.
- `pnpm db:migration:sqlite:run` — create or update a SQLite database.
- `pnpm db:seed` — seed the default database data explicitly.
- `pnpm db:studio` — inspect the database.
- `pnpm format` / `pnpm format:fix` — check or apply oxfmt.
- `pnpm lint` / `pnpm lint:fix` — check or apply oxlint.
- `pnpm typecheck` — typecheck all workspaces.
- `pnpm test` — unit and contract suites.
- `pnpm build` — production monorepo build.
- `docker build -t homarr .` — build a production Docker image.
- `docker run -p 7575:7575 -e SECRET_ENCRYPTION_KEY='your_64_character_hex_string' homarr:<name>` — run it.
### Useful npm scripts
## CI and focused validation
#### During development
The main CI workflow runs lint, typechecking, affected workspace builds, workspace configuration checks, OpenAPI validation, and Custom Widget architecture and bundle checks. Unit and E2E suites do not run in CI. Preview images publish after the Fast gate and both architecture builds succeed; browser tests do not block publication. The container job builds an amd64 image when no preview image was built.
- `pnpm dev`: Start the Next.js development server.
- `pnpm dev:benchmark`: Run the local authenticated-board benchmark. Set `DEV_BENCHMARK_BOARD_URL` and
`DEV_BENCHMARK_STORAGE_STATE`; readiness-only measurements require the explicit `--smoke` flag and are not
suitable for performance claims.
- `pnpm dev:cli -- dev`: Browse and run local images and remote pull-request images.
- `pnpm dev:cli -- build <name>`: Build the current checkout as `homarr:<name>`.
- `pnpm dev:cli -- build --pr <number>`: Build a pull request locally from a temporary checkout.
- `pnpm dev:cli -- rebuild <name>`: Rebuild a local image from its recorded checkout or pull request.
- `pnpm dev:cli:install`: Optionally install the developer CLI as a `homarr` binary.
- `pnpm cli`: Run cli commands.
- `pnpm db:migration:sqlite:run`: Run the migrations to create or update the sqlite db file.
- `pnpm db:seed`: Explicitly seed the default database data.
- `pnpm docker:dev:up`: Start the Redis development service in the background.
- `pnpm db:migration:sqlite:generate`: Generate a new migration file for the sqlite db. Also works with `mysql`.
- `pnpm db:studio`: Open a visual studio for the database.
- `pnpm package:new`: Create a new package in the monorepo.
Workshop CI runs only when its workflow or Docker build inputs change. It validates Compose configuration and publishes production images on its configured release branches. Its Docker integration and image tests are manual; workspace typechecks run in the main Fast gate.
#### Checking code quality
Choose the smallest relevant suite when changing behavior:
- `pnpm lint`: Run the linter to check for code quality issues.
- `pnpm format:fix`: Run the formatter to fix code style issues.
- `pnpm test`: Run the tests. You can also use `pnpm test:ui` to get a UI for the tests.
- `pnpm typecheck`: Run the typescript type checker.
| Command | Scope | Requirements |
| ------------------------------------------------- | -------------------------------------------------------------- | --------------------------------------------------------------------- |
| `pnpm test <file>` | Focused unit or contract tests | Workspace dependencies |
| `pnpm test` | All unit and contract tests, without Docker integration suites | Workspace dependencies |
| `pnpm test:coverage` | Unit and contract tests with coverage | Workspace dependencies |
| `pnpm test:integration <file>` | Docker-backed service and database compatibility tests | Docker |
| `pnpm test:e2e <file>` | Browser and container scenarios | Docker; Playwright Chromium and system dependencies for browser tests |
| `pnpm test:docs-screenshots` | Regenerate Assistant documentation screenshots | Same browser prerequisites |
| `pnpm test:workshop` / `pnpm test:workshop-image` | Workshop integration / production-image validation | Docker |
#### Building the project
Omit `<file>` to run all tests in that command's scope. E2E and screenshot commands use a local image tagged `homarr-e2e`, or an image selected with `HOMARR_E2E_IMAGE`. The default commands run once without automatic retries; coverage is opt-in. The screenshot generator is excluded from both unit and E2E commands.
Keep behavioral coverage for authorization, migrations, persistence, and widget execution. Avoid tests that only search source code for particular component names, JSX, CSS, or documentation wording.
- `pnpm build`: Build the project for production.
- `pnpm dev:cli -- build <name>`: Build a `homarr:<name>` image and record its checkout for later rebuilds.
- `docker build -t homarr .`: Build a docker image for the project.
- `docker run -p 7575:7575 -e SECRET_ENCRYPTION_KEY='your_64_character_hex_string' homarr:<name>`: Run the created docker image.
@@ -132,4 +132,4 @@ This command should return CPU and memory usage statistics for each node.
## Conclusion
You have successfully set up a multi-node Kubernetes cluster using kind and installed the Metrics Server for monitoring resource usage. This setup is useful for local development and testing Kubernetes workloads before deploying them to a production environment.
You have successfully set up a multi-node Kubernetes cluster using kind and installed the Metrics Server for monitoring resource usage. This setup is useful for local development and testing Kubernetes workloads before deploying them to a production environment.
@@ -1,102 +0,0 @@
---
title: Widgets and integrations
tags:
- Developer
- Technical documentation
- Widgets
- Integrations
sidebar_position: 2
---
Homarr keeps feature registration explicit and typed. Feature registration has no generator or generated registry.
A new feature is available when its typed metadata and lazy loader are registered.
## Add a widget
Keep the UI, options, helpers, and tests together in `packages/widgets/src/<widget>/`. The usual folder only needs an
`index.ts` definition and a lazily imported `component.tsx`; complex widgets can add local files as needed.
1. Add the stable kind to `packages/definitions/src/widget.ts` and its documentation slug to the direct typed map in
`packages/definitions/src/docs/widget-doc-slugs.ts`.
2. Export `definition` and `componentLoader` from `packages/widgets/src/<widget>/index.ts` with
`createWidgetDefinition`. The local definition owns its icon, query keys, refresh interval, options, matchers, and
errors. Keep the component behind `withDynamicImport` so adding a widget does not load it on every dashboard.
3. For an integration-backed widget, add its integration kinds and selection rules once to
`packages/definitions/src/widget-integration-map.ts`, then spread `getWidgetIntegrationConfig(kind)` into the local
definition. This server-safe config is also the API authorization boundary, avoiding a package cycle without
duplicating widget metadata.
4. Register the widget in both explicit literal loader maps and the type-only module map in
`packages/widgets/src/registry.ts`, then add its icon to `packages/ui/src/widget-icons.ts`. The module and component
loaders must both use literal import paths. Keeping them separate lets a cold widget load request its definition
and component chunks in parallel; the type-only map preserves exact option and component inference. Keep all three
maps in the same order so drift is obvious in review.
5. If the widget needs server data, add its tRPC router under `packages/api/src/router/widgets/` and add its explicit
lazy import to `packages/api/src/router/widgets/index.ts`. Keep upstream requests and Redis response caching in
`@homarr/request-handler`. Use the widget integration middleware for integration-backed procedures.
6. Add canonical documentation in `apps/docs/docs/widgets/<slug>/`.
Weather is the small integration-free example. Downloads shows integration selection, polling, server caching,
mutations, partial upstream failures, and a larger options surface.
## Add an integration
Keep the HTTP client and its capability-specific types in `packages/integrations/src/<integration>/`.
1. Add the typed definition to `packages/definitions/src/integration.ts`, including credential alternatives,
categories, documentation slug, ports, Docker aliases, and onboarding metadata when relevant.
2. Add an explicit lazy creator to `packages/integrations/src/base/creator.ts`. The literal import keeps the
implementation out of the initial server bundle and makes the registration easy to find.
3. Implement only the interfaces required by the widgets that use the service.
4. Add canonical documentation in `apps/docs/docs/integrations/<slug>/`. Its `index.ts` must satisfy
`IntegrationDefinition` so invalid metadata fails type checking without widening the object. Use a type-only import
and `satisfies IntegrationDefinition`; do not cast the object.
Beszel is a compact example with several widgets sharing one integration client.
## Write user documentation
Write for technically capable self-hosters. Keep each page short and operational.
- Start with one sentence describing what the integration or widget adds to Homarr.
- Include prerequisites, credentials, permissions, destructive actions, and non-obvious limits only when they matter.
- Use numbered steps only when order matters. Prefer the exact UI labels a user must select.
- Do not add marketing, implementation history, architecture internals, or explanations of basic self-hosting concepts.
- End when the user can configure and verify the feature; do not repeat the same information in a summary.
The usual page needs only an introduction and a `Configuration` section. Add `Notes` or `Troubleshooting` only for
real caveats.
## Data and errors
- TanStack Query owns browser caching and refresh behavior. Redis owns shared server response caching for bounded key
spaces. Public handlers whose keys come from arbitrary URLs, domains, symbols, or coordinates stay in the bounded
process-local cache.
- Cache raw service responses, never authorization decisions or decrypted credentials. Integration-backed cache keys
include keyed credential fingerprints and the complete integration identity, and are invalidated after mutations.
The fingerprints change with credentials without exposing their values in Redis keys.
- Give upstream requests a finite deadline and pass the request handler's `AbortSignal` into clients that support it.
Admission is bounded per handler. Request deduplication and distributed lock ownership remain active until the
upstream settles or a bounded cleanup grace expires, while unresolved upstream work stays separately capped.
- Keep validation and authorization on the server. Client-side checks improve UX but do not replace tRPC guards.
- Use the shared logger with safe identifiers, operation names, duration, and the original error cause. Never log
secrets or full upstream payloads.
- Multi-integration reads should return successful providers when one service fails and surface a clear error when all
providers fail.
Registration files intentionally contain boring, explicit imports. This small amount of repetition preserves exact
TypeScript inference, gives Next.js discoverable lazy chunks, and keeps Ctrl+click navigation reliable.
## Registry imports
Use `@homarr/widgets/manifest` to load registered widgets. `loadWidgetDefinition` is the metadata-only path;
`loadWidgetComponent` loads only the UI module; and `loadWidgetResources` requests both paths in parallel. These
loaders cache in-flight and successful promises and remove rejected promises so a later request can retry.
The former root `widgetImports` and `loadWidgetDynamic` exports are intentionally not compatibility aliases.
`widgetImports` synchronously imported every widget module, and `loadWidgetDynamic` depended on that eager map.
Restoring them at the package root would defeat lazy definition chunks or add client-only runtime dependencies to
every root consumer. Migrate metadata consumers to `loadWidgetDefinition`, renderers to `loadWidgetResources`, and
type-only registry consumers from the removed internal `WidgetImportRecord` helper to the root `WidgetImports` type.
`@homarr/widgets/catalog` remains as a deprecated compatibility subpath for `widgetCatalogIcons`. New imports should
use `@homarr/ui/widget-icons`, which is the canonical server-safe icon catalog.
@@ -1,67 +0,0 @@
---
title: Workshop operator guide
sidebar_label: Operator guide
sidebar_position: 3
tags:
- Workshop
- Deployment
- Environment variables
- PocketBase
---
# Workshop operator guide
Workshop uses PocketBase. The production image serves the Workshop website and API from one origin and stores state in
`/pb_data`.
## Environment variables
| Variable | Purpose | Default |
| -------------------------------------- | -------------------------------------------- | ------------------------------ |
| `HOMARR_WEBSITE_URL` | Documentation/site base used by Homarr links | `https://homarr.dev` |
| `WORKSHOP_API_URL` | PocketBase API used by Homarr and docs | `HOMARR_WEBSITE_URL` |
| `WORKSHOP_WEB_URL` | Public Workshop URL | `HOMARR_WEBSITE_URL/workshop` |
| `WORKSHOP_PUBLIC_ORIGIN` | PocketBase-generated links and email | Required in production |
| `PB_ALLOWED_ORIGINS` | Allowed browser origins | `*` |
| `GITHUB_CLIENT_ID` | GitHub OAuth application ID | — |
| `GITHUB_CLIENT_SECRET` | GitHub OAuth application secret | — |
| `OPENROUTER_API_KEY` | Enables the Homarr Assistant provider | Provider disabled when empty |
| `HOMARR_AI_DAILY_REQUEST_LIMIT` | Per-user daily request allowance | `50` |
| `HOMARR_AI_GLOBAL_DAILY_REQUEST_LIMIT` | Shared daily request ceiling | `10000` |
| `HOMARR_AI_OPENROUTER_BASE_URL` | OpenRouter-compatible upstream | `https://openrouter.ai/api/v1` |
| `HOMARR_AI_OPENROUTER_MODEL` | Upstream model behind `homarr/model` | Selected by the Homarr team |
Public URL variables accept HTTP(S) origins without embedded credentials, query strings, or fragments. Restart the
container after changing runtime configuration.
## Authentication and email
Configure both GitHub OAuth variables and use `https://<workshop-host>/api/oauth2-redirect` as the callback. Supplying
only one OAuth variable is a startup error. Configure SMTP for comment, report, and removal notifications.
For the central community, `PB_ALLOWED_ORIGINS=*` permits requests from self-hosted Homarr origins. A private deployment
can use a comma-separated allowlist.
## Homarr provider
`OPENROUTER_API_KEY` enables the [Homarr provider](/docs/workshop/homarr-provider). The Workshop server chooses the upstream model and
enforces per-user and global daily limits. It requests zero-data-retention routing, disables data-collecting providers,
and does not retain prompts or responses.
Keep an OpenRouter account-level credit limit as the monetary backstop; request limits are not cost limits.
## Deployment
The v2 image is `ghcr.io/homarr-labs/workshop:v2`; immutable `sha-<commit>` tags are preferable for promotion. The
service listens on port 8090.
When using a reverse proxy, forward the complete hostname, including `/api`, `/_/`, and
`/workshop-runtime-config.js`. Use a dedicated hostname when Homarr already occupies the intended origin.
Verify `/api/health`, the Workshop page, a public listing request, GitHub sign-in, and one installation after deployment.
## Backup and restore
Stop writes and copy `/pb_data` for a consistent backup. A restore replaces the selected PocketBase data directory, so
preserve the current volume and verify that the backup contains `data.db` first. Rehearse the restore outside production
and verify sign-in and one installation afterward.
@@ -1,41 +0,0 @@
---
title: Workshop development
sidebar_position: 4
tags:
- Workshop
- Developer
- Docusaurus
- PocketBase
---
# Workshop development
See the [main development guide](./getting-started) for repository prerequisites.
Start PocketBase:
```sh
cp apps/workshop/.env.example apps/workshop/.env
docker compose --env-file apps/workshop/.env -f apps/workshop/docker-compose.yml up --build workshop
```
Start Docusaurus in another terminal:
```sh
WORKSHOP_API_URL=http://127.0.0.1:8090 pnpm dev:docs
```
PocketBase runs at `http://127.0.0.1:8090`; create the first superuser under `/_/`. Workshop is available at
`http://127.0.0.1:3003/workshop`.
To connect a local Homarr instance, set:
```dotenv
HOMARR_WEBSITE_URL=http://127.0.0.1:3003
WORKSHOP_API_URL=http://127.0.0.1:8090
WORKSHOP_WEB_URL=http://127.0.0.1:3003/workshop
```
Restart the affected development process after changing these variables. See
[`apps/workshop/README.md`](https://github.com/homarr-labs/homarr/blob/HEAD/apps/workshop/README.md) for OAuth,
moderation, tests, and service maintenance.
@@ -25,41 +25,14 @@ This is especially useful for [docker secrets](https://docs.docker.com/engine/sw
Using the `PUID` and `PGID` will require you to set the correct permissions on the mounted volumes and if used the docker socket. See more in the [Running as a different user](/docs/advanced/running-as-different-user) documentation.
:::
| Environment Variable | Description | Possible values | Default |
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------- | ----------------------------- |
| `PUID` | User ID to run the container as | Any valid user ID | `0` |
| `PGID` | Group ID to run the container as | Any valid group ID | `0` |
| `LOG_LEVEL` | Log level to use | `debug` / `info` / `warn` / `error` | `info` |
| `BASE_URL` | Public Homarr origin used by MCP OAuth metadata and redirects. Set this when a reverse proxy does not preserve the public host and protocol. | HTTP(S) origin without a path | - |
| `NO_EXTERNAL_CONNECTION` | Disables Homarr's internet-backed requests, including update checks, weather, location search, and RSS feeds | `true` or `false` | `false` |
| `ENABLE_DNS_CACHING` | Enables dns caching. Enabled by default, but if you experience issues with IPv6 or static IPs, see [#4006](https://github.com/homarr-labs/homarr/issues/4006) or disable this flag | `true` or `false` | `true` |
| `HOMARR_DISABLE_IPV6` | Disables the IPv6 listener of the internal nginx proxy, even when the host supports IPv6. IPv6 is normally disabled automatically when the host has no IPv6 configured. See [#4596](https://github.com/homarr-labs/homarr/issues/4596) | `true` or `false` | `false` |
| `HOMARR_WEBSITE_URL` | Main Homarr documentation/site base used by runtime links. Must be an absolute HTTP(S) URL. | HTTP(S) URL | `https://homarr.dev` |
| `WORKSHOP_API_URL` | PocketBase API used by Homarr and standalone documentation. | HTTP(S) URL | `HOMARR_WEBSITE_URL` |
| `WORKSHOP_WEB_URL` | Public Workshop website used by links in Homarr, Custom Widgets, onboarding, and assistant context. | HTTP(S) URL | `HOMARR_WEBSITE_URL/workshop` |
Workshop service operators can find its server-only configuration in the
[Workshop operator guide](/docs/advanced/development/workshop-operator#environment-variables).
### Workshop service
These variables configure the separate Workshop service, not the Homarr application container.
| Environment Variable | Description | Default |
| ------------------------------------------- | ------------------------------------------------------------------------- | --------------------------------- |
| `WORKSHOP_PUBLIC_ORIGIN` | Public origin used in PocketBase links and email | Required in production |
| `PB_ALLOWED_ORIGINS` | Browser origins allowed to call PocketBase | `*` for the public community |
| `WORKSHOP_IMAGE` | Workshop image used by the staging Compose file | `ghcr.io/homarr-labs/workshop:v2` |
| `WORKSHOP_EXPOSE_PORT` | Host port for the combined staging service | `8090` |
| `PB_EXPOSE_PORT` | Host port for local PocketBase development | `8090` |
| `DOCS_EXPOSE_PORT` | Host port for local documentation development | `3003` |
| `GITHUB_CLIENT_ID` / `GITHUB_CLIENT_SECRET` | Optional GitHub OAuth application credentials | - |
| `OPENROUTER_API_KEY` | Server-only upstream key that enables the Homarr Assistant provider | Provider disabled |
| `HOMARR_AI_DAILY_REQUEST_LIMIT` | Daily Assistant request units per user | `50` |
| `HOMARR_AI_GLOBAL_DAILY_REQUEST_LIMIT` | Daily Assistant request units shared by all users | `10000` |
| `HOMARR_AI_OPENROUTER_BASE_URL` | OpenRouter-compatible provider endpoint | `https://openrouter.ai/api/v1` |
| `HOMARR_AI_OPENROUTER_MODEL` | Upstream model exposed through the stable `homarr/model` alias | Selected by the Homarr team |
| `HOMARR_AI_ALLOW_INSECURE_UPSTREAM` | Allows an HTTP upstream for local provider tests; never use in production | `false` |
| Environment Variable | Description | Possible values | Default |
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------- | ------- |
| `PUID` | User ID to run the container as | Any valid user ID | `0` |
| `PGID` | Group ID to run the container as | Any valid group ID | `0` |
| `LOG_LEVEL` | Log level to use | `debug` / `info` / `warn` / `error` | `info` |
| `NO_EXTERNAL_CONNECTION` | Disables Homarr's internet-backed requests, including update checks, weather, and RSS feeds | `true` or `false` | `false` |
| `ENABLE_DNS_CACHING` | Enables dns caching. Enabled by default, but if you experience issues with IPv6 or static IPs, see [#4006](https://github.com/homarr-labs/homarr/issues/4006) or disable this flag | `true` or `false` | `true` |
| `HOMARR_DISABLE_IPV6` | Disables the IPv6 listener of the internal nginx proxy, even when the host supports IPv6. IPv6 is normally disabled automatically when the host has no IPv6 configured. See [#4596](https://github.com/homarr-labs/homarr/issues/4596) | `true` or `false` | `false` |
## Authentication
@@ -79,12 +52,11 @@ A random secret can be generated by using the following command: `openssl rand -
## Docker
| Environment Variable | Description | Possible values | Default |
| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | ------- |
| `DOCKER_SOCKET_PATHS` | Comma-separated mounted Docker-compatible socket paths. Homarr discovers each path as a selectable Docker environment | For example `/var/run/docker.sock,/var/run/podman.sock` | - |
| `DOCKER_HOSTNAMES` | Comma-separated plaintext TCP hostnames. Pair each hostname with the port in the same position in `DOCKER_PORTS` | For example `localhost,docker.example.com` | - |
| `DOCKER_PORTS` | Comma-separated plaintext TCP ports paired with `DOCKER_HOSTNAMES` | For example `2375,2376` | - |
| `DOCKER_ENDPOINTS` | Optional advanced JSON configuration for custom names, TLS, or restricted capabilities. It takes precedence over the simple variables above. See the [Docker and Podman connection guide](/docs/integrations/docker) | A JSON array of endpoint descriptors | - |
| Environment Variable | Description | Possible values | Default |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | ------- |
| `DOCKER_HOSTNAMES` | Comma-separated list of hostnames to connect to | For example `localhost,docker.example.com` | - |
| `DOCKER_PORTS` | Comma-separated list of ports to connect to | For example `2375,2376` | - |
| `DOCKER_SOCKET_PATHS` | Comma-separated list of mounted Docker-compatible socket paths. See the [Docker and Podman integration guide](/docs/integrations/docker) | For example `/var/run/docker.sock,/var/run/podman.sock` | - |
## Database
@@ -134,20 +106,6 @@ External redis is currently not supported with Proxmox Community Scripts
The advanced deployments environment variables should only be used if you know what you are doing.
:::
| Environment Variable | Description |
| ------------------------ | --------------------------------------------------- |
| `DB_MIGRATIONS_DISABLED` | Disable db migrations. For example for helm charts. |
All public URL variables reject non-HTTP protocols, embedded credentials, query strings, and fragments, and normalize
trailing slashes when Homarr or Docusaurus starts.
## Trusted widget packages
| Variable | Purpose | Default |
| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| `CUSTOM_WIDGET_PACKAGE_PATH` | Persistent build and runtime artifact cache. Activated browser/server artifacts and their source are also retained in the database. | `/appdata/custom-widgets` in production; `./appdata/custom-widgets` in development |
| `CUSTOM_WIDGET_MAX_SERVER_PROCESSES` | Maximum active trusted widget server artifacts per Homarr process, from 1 to 128. Source-free packages do not start a server process. | `16` |
Each replica materializes an authorized installed artifact from the database into its own cache. Dashboard requests do
not install npm dependencies. Package dependencies are resolved and bundled when an owner previews or activates a new
source draft; importing a complete compiled archive supports offline activation. See [trusted widget packages](/docs/management/custom-widgets/trusted-packages).
| Environment Variable | Description |
| ------------------------ | -------------------------------------------------- |
| `DB_MIGRATIONS_DISABLED` | Disable db migrations. For example for helm charts |
@@ -6,52 +6,11 @@ tags:
Homarr has the following keyboard shortcuts:
| Shortcut | Description |
| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `MOD` + `K` | Opens search, including local apps, boards, integrations, pages, commands, and preferences |
| `MOD` + `P` | Opens app search |
| `MOD` + `/` | Opens Homarr Assistant inside search |
| `MOD` + `Shift` + `/` (`MOD` + `?`) | Opens preference search |
| `Shift` + `A` | Opens the full Homarr Assistant |
| `MOD` + `J` | Toggles Dark Mode |
| `MOD` + `E` | Toggles Edit Mode |
| Hold `Shift` for 500 ms over a supported widget | Opens its temporary advanced view |
| Hold `Shift` + `Ctrl`/`Cmd` over a supported widget | Keeps its advanced view open until it is closed |
| `Esc` | Stops keyboard tile editing, cancels an active pointer or touch move/resize, or closes an open advanced view or board menu |
## Search and commands
Search is designed as the fastest route to Homarr. Start typing after `MOD` + `K` to find local apps, boards,
integrations, pages, commands, and preferences without entering a separate mode first. Local Homarr results appear
before configured search-engine actions and Assistant fallbacks. Commands include opening the board switcher without
leaving the keyboard. Type `!` to enter Web mode when you want to browse configured engine shortcuts or discover
DuckDuckGo bangs; bang discovery is excluded from ordinary Search results.
Use the dedicated shortcuts above to start in apps, Assistant, or preferences. Inside search, choose a mode to narrow
the same query when needed. Media request search is available only when a supported Seerr integration is configured.
## Widget advanced view
Supported widgets open a temporary advanced view when you hold `Shift` over them for 500 ms. Hold `Shift` plus `Ctrl`
on Windows/Linux or `Cmd` on macOS over the widget to keep the advanced view open, or use its menu.
## Board editing
`MOD` + `E` enters edit mode. Homarr keeps the current board visible while the editor prepares; movement and resizing become available when it is ready.
When a tile is focused in edit mode:
| Shortcut | Description |
| -------------------- | --------------------------------------------------------------------------- |
| `Enter` or `Space` | Starts or stops keyboard editing |
| Arrow keys | Moves the focused tile within its current canvas, rail, or Container |
| `Shift` + Arrow keys | Resizes from the tile's lower-right corner: Left/Up shrink; Right/Down grow |
| `Esc` | Stops keyboard editing without undoing its changes |
Keyboard moves and resizes apply immediately. To move an item between a canvas, rail, or Container, drag it there or use **Move / resize item** for precise numeric placement.
Commands at a grid boundary or at the tile's minimum size are ignored and announced instead of moving or shrinking the tile.
On touch screens, press and hold a tile before moving it; a quick tap does not start a drag. Resize handles accept direct touch input. During a pointer or touch move or resize, `Esc` cancels the interaction and restores the layout from before it started.
| Shortcut | Description |
| -------- | ----------- |
| `MOD` + `K` | Opens the search bar |
| `MOD` + `J` | Toggles Dark Mode |
| `MOD` + `E` | Toggles Edit Mode |
:::tip
+10 -14
View File
@@ -9,17 +9,16 @@ tags:
- Security
---
import Tabs from "@theme/Tabs";
import TabItem from "@theme/TabItem";
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
## Allowing self-signed certificates
Some users may come across a barrier, where they're unable to receive a 200 response from the Ping widget for some apps, while using self-signed certificates or a local certificate authory.
:::note What's going on?
Homarr is trying to communicate to your apps via the integrations.
It usually doesn't matter if Homarr is running on `http` or `https`.
It usually doesn't matter if Homarr is running on ``http`` or ``https``.
Your apps have a self-signed certificate - Homarr will recognize that the certificate was signed by an unknown authority and requests will be blocked.
:::
@@ -29,9 +28,8 @@ To allow self-signed certificates, you can configure them on the certificates ma
## Securing Homarr with Traefik
Copying the configuration straight from the docker-compose file won't work if you are running Homarr behind Traefik, such as a Portainer setup, or docker-swarm. In that case, you should use the following slightly modified configuration:
```yaml
version: "3"
version: '3'
services:
homarr:
container_name: homarr
@@ -40,7 +38,7 @@ services:
volumes:
- ./homarr/appdata:/appdata
environment:
- BASE_URL=https://your.internal.dns.address.here.com
- BASE_URL=your.internal.dns.address.here.com
- SECRET_ENCRYPTION_KEY=your_64_character_hex_string # <--- can be generated with `openssl rand -hex 32`
networks:
- proxy
@@ -55,12 +53,11 @@ networks:
external: true
```
<br />
<br/>
A sample Traefik docker-compose.yml using Cloudflare for certificate generation that works with the configuration above would be:
```yaml
version: "3"
version: '3'
apps:
traefik:
@@ -106,6 +103,7 @@ networks:
external: true
```
:::tip
Of particular note here is that both configurations explicitly define which network they are using, in this case "proxy", but it can be named anything. It just has to be the same across all apps for which Traefik is serving as a proxy. These are marked as external because the proxy network was manually created by running: `docker network create proxy` but this might be unnecessary depending on HOW exactly you are running Traefik. For example, if running [Traefik with Portainer](https://docs.portainer.io/advanced/reverse-proxy/traefik#deploying-in-a-docker-standalone-scenario), you can follow their official docs on how to set up Traefik and Portainer together, and you can just focus on the Homarr docker labels instead.
:::
@@ -116,6 +114,7 @@ If you are using Caddy as the reverse proxy for your setup, your `docker-compose
```yaml
services:
# <-- [Homarr installation]
caddy:
@@ -141,17 +140,14 @@ networks:
proxy:
external: true
```
:::important
Homarr and Caddy must share the same Docker network:
Homarr needs to be on the same network so you will need to add the following property to your homarr install:
```yaml
homarr:
...
networks:
- proxy
```
:::
Next, create a file named `Caddyfile` at the same level as your `docker-compose.yml` file with the following content:
@@ -8,7 +8,7 @@ tags:
- PUID
---
By default the container is running with user `root` and group `root`.
By default the container is running with user `root` and group `root`.
You can change the user and group by using the `PUID` and `PGID` environment variables.
Important to note is, that you'll also need to change the permissions for the mounted directories accordingly.
@@ -32,11 +32,11 @@ services:
volumes:
- ./homarr/appdata:/appdata
environment:
- SECRET_ENCRYPTION_KEY=your_64_character_hex_string # <--- can be generated with `openssl rand -hex 32`
- PUID=1000
- PGID=1000
- SECRET_ENCRYPTION_KEY=your_64_character_hex_string # <--- can be generated with `openssl rand -hex 32`
- PUID=1000
- PGID=1000
ports:
- "7575:7575"
- '7575:7575'
```
After that you'll need to create the appdata directory and set the correct permissions:
@@ -76,11 +76,11 @@ services:
- /var/run/docker.sock:/var/run/docker.sock
- ./homarr/appdata:/appdata
environment:
- SECRET_ENCRYPTION_KEY=your_64_character_hex_string # <--- can be generated with `openssl rand -hex 32`
- PUID=1000
- PGID=999
- SECRET_ENCRYPTION_KEY=your_64_character_hex_string # <--- can be generated with `openssl rand -hex 32`
- PUID=1000
- PGID=999
ports:
- "7575:7575"
- '7575:7575'
```
And set the permissions for the mounted directories:
@@ -90,4 +90,4 @@ mkdir -p ./homarr/appdata
chown -R 1000:999 ./homarr/appdata
```
Important here is that the group ID is set to `999` as we created the `docker` group with this ID.
Important here is that the group ID is set to `999` as we created the `docker` group with this ID.
@@ -10,21 +10,20 @@ tags:
- Active Directory
---
import Tabs from "@theme/Tabs";
import TabItem from "@theme/TabItem";
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
# Single Sign On
Homarr supports multiple authentication options, from internal userbase (credentials), to LDAP (with Active directory support), and OIDC.
### Auth configuration common variables
| Environment Variable | Description | Default Value |
| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- |
| AUTH_PROVIDERS | Select Which provider to use between _credentials_, _ldap_ and _oidc_. <br /> Multiple providers can be enabled with by separating them with `,`, (ex. `AUTH_PROVIDERS=credentials,oidc`, it is highly recommended to just enable one provider). | credentials |
| AUTH_LOGOUT_REDIRECT_URL | URL to redirect to after clicking logging out. | --- |
| AUTH_SESSION_EXPIRY_TIME | Time for the session to time out. Can be set as pure number, which will automatically be used in seconds, or followed by s, m, h or d for seconds, minutes, hours or days. (ex: "30m") | "30d" |
| AUTH_COOKIE_PREFIX | Prefix used for all authentication cookies (session, csrf, callback, pkce, state, nonce). Change this if you run another Auth.js/NextAuth app on the same hostname as Homarr (browsers scope cookies by host, not by port) to avoid cookie name collisions. Only letters, numbers, hyphens and underscores are allowed. | `homarr` |
| Environment Variable | Description | Default Value |
|---------------------------|-------------|---------------|
| AUTH_PROVIDERS | Select Which provider to use between *credentials*, *ldap* and *oidc*. <br /> Multiple providers can be enabled with by separating them with `,`, (ex. `AUTH_PROVIDERS=credentials,oidc`, it is highly recommended to just enable one provider). | credentials |
| AUTH_LOGOUT_REDIRECT_URL | URL to redirect to after clicking logging out. | --- |
| AUTH_SESSION_EXPIRY_TIME | Time for the session to time out. Can be set as pure number, which will automatically be used in seconds, or followed by s, m, h or d for seconds, minutes, hours or days. (ex: "30m") | "30d" |
| AUTH_COOKIE_PREFIX | Prefix used for all authentication cookies (session, csrf, callback, pkce, state, nonce). Change this if you run another Auth.js/NextAuth app on the same hostname as Homarr (browsers scope cookies by host, not by port) to avoid cookie name collisions. Only letters, numbers, hyphens and underscores are allowed. | `homarr` |
<Tabs>
<TabItem value="credentials" label="Credentials Provider" default>
@@ -202,9 +201,9 @@ Homarr supports multiple authentication options, from internal userbase (credent
| ``AUTH_OIDC_CLIENT_NAME`` | Display name of provider (in login screen) | OIDC |
| ``AUTH_OIDC_AUTO_LOGIN`` | Automatically redirect to OIDC login | false |
| ``AUTH_OIDC_SCOPE_OVERWRITE`` | Overwrite default scopes (openid, profile, email) | openid email profile groups |
| ``AUTH_OIDC_GROUPS_ATTRIBUTE`` | Attribute used for groups (roles) claim. Dot-separated paths such as `resource_access.homarr.roles` are supported. | groups |
| ``AUTH_OIDC_GROUPS_ATTRIBUTE`` | Attribute used for groups (roles) claim | groups |
| ``AUTH_OIDC_GROUPS_LOCAL_MANAGEMENT`` | Stop syncing group memberships of OIDC users from the groups claim and manage them locally via the group members page instead | false |
| ``AUTH_OIDC_NAME_ATTRIBUTE_OVERWRITE`` | Overwrite name attribute. Dot-separated paths are supported. By default it will use preferred_username if it does not contain a `@` and otherwise name. | --- |
| ``AUTH_OIDC_NAME_ATTRIBUTE_OVERWRITE`` | Overwrite name attribute. By default it will use preferred_username if it does not contain a `@` and otherwise name. | --- |
| ``AUTH_OIDC_FORCE_USERINFO`` | Force userinfo endpoint to be used for user information. | false |
| ``AUTH_OIDC_ENABLE_DANGEROUS_ACCOUNT_LINKING`` | Enable account linking for OIDC provider. This will link the OIDC accounts by email. Be sure that you have verified emails to prevent stealing accounts. | false |
| ``AUTH_OIDC_ENABLE_DANGEROUS_CREDENTIALS_LINKING`` | Link an OIDC identity to an existing credentials user with the same email. The credentials login and existing user data are preserved. Requires the OIDC profile to contain `email_verified: true`. | false |
@@ -244,21 +243,18 @@ Homarr supports multiple authentication options, from internal userbase (credent
- Create a group (e.g., `homarr-admins`) and assign users who require admin access.
**2. Example .env file:**
Generate `SECRET_ENCRYPTION_KEY` with `openssl rand -hex 32`; do not copy an example key between installs.
```env
OIDC_CLIENT_ID=<REPLACE_WITH_OIDC_CLIENT_ID>
OIDC_CLIENT_SECRET=<REPLACE_WITH_OIDC_CLIENT_SECRET>
OIDC_CLIENT_ID=identificationid #OIDC client ID
OIDC_CLIENT_SECRET=secretsecretsecret #OIDC client secret
OIDC_SLUG=homarr #Application slug in Authentik
AUTH_DOMAIN=auth.example.com #Authentik FQDN
ADMIN_GROUP=homarr-admins #Authentik group for Homarr admins
HOMARR_FQDN=homarr.example.com #Homarr FQDN
SECRET_ENCRYPTION_KEY=<REPLACE_WITH_64_HEX_CHARACTERS>
SECRET_ENCRYPTION_KEY=encryptencryptencrypt #Homarr encryption key
```
**3. Example Docker Compose configuration:**
```yaml {14-24}
```yaml {14-26}
services:
homarr:
image: ghcr.io/homarr-labs/homarr:latest
@@ -272,6 +268,8 @@ Homarr supports multiple authentication options, from internal userbase (credent
environment:
- TZ=America/Los_Angeles
- SECRET_ENCRYPTION_KEY=${SECRET_ENCRYPTION_KEY}
- BASE_URL=https://${HOMARR_FQDN}
- NEXTAUTH_URL=https://${HOMARR_FQDN}
- AUTH_PROVIDERS=oidc #(optional: include ',credentials' to keep local accounts as fallback)
- AUTH_OIDC_CLIENT_ID=${OIDC_CLIENT_ID}
- AUTH_OIDC_CLIENT_SECRET=${OIDC_CLIENT_SECRET}
@@ -323,18 +321,15 @@ Homarr supports multiple authentication options, from internal userbase (credent
- Record the `Client-ID` and `Client Key` (secret).
**2. Example .env file (Homarr):**
Generate `SECRET_ENCRYPTION_KEY` with `openssl rand -hex 32`; do not copy an example key between installs.
```env
OIDC_CLIENT_ID=<REPLACE_WITH_GOOGLE_CLIENT_ID>
OIDC_CLIENT_SECRET=<REPLACE_WITH_GOOGLE_CLIENT_SECRET>
OIDC_CLIENT_ID=identificationid #Google Client-ID
OIDC_CLIENT_SECRET=secretsecretsecret #Google Client Key (secret)
HOMARR_FQDN=homarr.example.com #Homarr FQDN
SECRET_ENCRYPTION_KEY=<REPLACE_WITH_64_HEX_CHARACTERS>
SECRET_ENCRYPTION_KEY=encryptencryptencrypt #Homarr encryption key
```
**3. Example Docker Compose configuration (Homarr):**
```yaml {14-20}
```yaml {16-22}
services:
homarr:
image: ghcr.io/homarr-labs/homarr:latest
@@ -348,6 +343,8 @@ Homarr supports multiple authentication options, from internal userbase (credent
environment:
- TZ=Europe/Berlin
- SECRET_ENCRYPTION_KEY=${SECRET_ENCRYPTION_KEY}
- BASE_URL=https://${HOMARR_FQDN}
- NEXTAUTH_URL=https://${HOMARR_FQDN}
- AUTH_PROVIDERS=oidc #(optional: include ',credentials' to keep local accounts as fallback)
- AUTH_OIDC_CLIENT_ID=${OIDC_CLIENT_ID}
- AUTH_OIDC_CLIENT_SECRET=${OIDC_CLIENT_SECRET}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 39 KiB

+79 -22
View File
@@ -1,37 +1,94 @@
# Styling
Homarr uses Mantine and supports custom CSS at two scopes:
:::tip
- **Global:** **Management → Settings → Instance branding → Custom CSS**.
- **Board:** **Board settings → Custom CSS**. Board CSS loads after the global stylesheet and can override it.
Both editors accept direct CSS and Workshop imports.
:::warning
Custom CSS can break after updates and is not an authorization boundary. Hidden controls remain accessible to users who
have permission to use them.
This page is directed at more experienced users with knowledge about CSS and browsers.
We recommend you to [look into the board customizations instead](/docs/management/boards#appearance),
if you do not have experience with writing custom styles or CSS.
:::
## Target elements
:::warning
Use browser developer tools to inspect the element and test a selector. Prefer stable semantic or Mantine class names;
avoid generated classes such as `mantine-Card-x2fske`, which change between builds.
Our support specifically excludes custom CSS, and we will not help you to write CSS.
However, we may help you by adding new CSS classes if required.
To target one widget, add a custom class under **Edit item → Advanced options** and use that class in the board CSS.
:::
Use `!important` only when the existing cascade requires it.
Homarr uses https://mantine.dev/ under the hood. You can change the look & feel of Homarr
using custom CSS, that extends or overwrites the styles from Mantine.
## Theme variables
## Getting into CSS
If you don't have any experience with CSS,
we strongly recommend you to go through the courses here: https://www.w3schools.com/css/.
Due to the infinite scenarios and setups possible with custom CSS,
our support team will not provide any support with your own written CSS.
Mantine exposes [CSS variables](https://mantine.dev/styles/css-variables-list/) that can be overridden globally or per
board:
## Limitations and dangers
When using custom CSS, you can break the page or certain functionalities.
Although CSS can improve the design & feel of the page, it **<ins>cannot do any of the following</ins>**:
- Implement new features, widgets or integrations
- Securely hide elements you don't want users to interact / have permission to
- Completely revamp the design of the page, only smaller changes are possible
Here are some dangers of using custom CSS:
- Updates may contain breaking changes in the structure, built-in styling or how styling is processed. Therefore,
custom CSS may break unexpectedly without any warning
- Custom CSS may make certain elements invisible or not possible to click / interact with
- Custom CSS could make readability and accessibility worse for impaired people
## How to inspect the page
When styling the page, you can use the ``Inspector`` to inspect elements on the page and try out styles.
This may vary depending on your browser and operating system, but the workflow usually goes like this:
- Open the Homarr page
- Press ``Ctrl`` + ``Shift`` + ``I`` at the same time.
Alternatively, right-click on the page and choose ``Inspect`` or ``Inspect element``.
- Open the tab ``Elements`` if it isn't open yet.
- Search the element you want to change in the DOM tree or use the element picker (usually at the top left)
to select it by clicking on the page.
- Experiment by changing the CSS code on the right side
When writing custom CSS, it is important to also remember or create a selector, which tells the browser
which element to apply the styles to. If you accidentally broke something or want to revert your changes in the inspector,
simply reload the page.
## Using Homarr's classes
Homarr's elements will already have a lot of classes and styles pre-applied.
You can reuse those classes to apply additional styles or overwrite them.
When overwriting, remember to add the ``!important`` flag to your CSS property to tell the browser to use our instruction instead.
Classes with random number suffixes, such as ``mantine-Card-x2fske``, should be avoided, since those are dynamic
CSS classes that will change with every build. This means, every time you update Homarr,
your styles will no longer apply to them
## Defining custom classes for widgets
If you want to apply the styles only for one widget, you can define custom class names only for that widget.
To do that, enter edit mode on your board, click the three dots of your widget, click ``Edit item`` and click ``Advanced options``.
Then, you can enter as many custom classes as you'd like:
![](./img/widget-custom-classes.png)
To save them, click `Save changes` on both modals and exit edit mode.
## Where to enter custom CSS
After you wrote your custom CSS, it is time to permanently save it, so you don't need to use the inspector anymore.
To do this, enter the board settings:
![](./img/enter-board-settings.png)
Scroll down a bit and click on custom CSS.
Here you can enter your CSS code, which will be applied on the respective board page.
![](./img/custom-css-setting.png)
## Using variables
You can also change the values of the theme variables, which are located here: https://mantine.dev/styles/css-variables-list/.
To do this, see the following example CSS:
```css
:root {
--mantine-primary-color-filled: #ffbb00;
--mantine-primary-color-filled: #ffbb00;
}
```
CSS changes presentation only; use widgets, integrations, permissions, or application code for behavior.
```
+1 -1
View File
@@ -1,6 +1,6 @@
{
"label": "Community",
"position": 6,
"position": 5,
"collapsed": true,
"link": {
"type": "generated-index"
+14 -3
View File
@@ -7,8 +7,19 @@ tags:
---
# Donate
## Why to donate
Our team of 4 developers is working on Homarr in their free time.
Homarr has become a big part of our daily things we work on - and therefore we invest a lot of time into maintaining Homarr,
providing support and implementing new features.
We also incur monetary costs while developing Homarr for the tools that we need.
A few examples include:
- Renting for a Cloud VPS for our cloud services (status page, monitoring, email, analytics, [demo instance](https://demo.homarr.dev/), ...)
- Paying for (IDE-) licenses
Donations fund Homarr's development infrastructure and community services.
Using a donation, you help us to cover these expenses and enable the development of Homarr.
Homarr is completely free to use. We'd highly appreciate if you consider donating, especially if you use Homarr in a commercial environment.
Support the project through [OpenCollective](https://opencollective.com/homarr). For sponsorships or donated services,
use the private contact listed on [Get in touch](/docs/community/get-in-touch).
## Where to donate
We accept donations on our official [OpenCollective](https://opencollective.com/homarr).
You may also sponsor us directly by providing a useful service / product to us other means.
You can get into contact with us [here](/docs/community/get-in-touch).
+67 -28
View File
@@ -1,51 +1,90 @@
---
title: Frequently Asked Questions
description: Answers to common Homarr questions about installation, customization, connectivity, support, and commercial use.
tags:
- Community
- Help
---
## Can Homarr run on a Raspberry Pi?
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
Yes, when the device uses a supported `linux/arm64` operating system. `linux/arm/v7` is not supported. See
[Prerequisites](/docs/getting-started).
### Can I install Homarr on a Raspberry Pi?
Yes, Homarr is compatible with the architecture of the Raspberry Pi. Have a look in our <a href="/docs/category/installation-1">installation guide</a> on how to do so.
## Can I use custom app icons?
### I have a suggestion / idea for improvement or want to report a bug
You can report issues, suggestions, or ideas for improvements <a href="https://github.com/homarr-labs/homarr/issues/new/choose">here</a>.
Yes. Use a direct image URL, one of Homarr's icon sources, or upload an image. See [Icons](/docs/advanced/icons).
### Can I use my own custom icons for apps?
Yes, check out our guide on <a href="/docs/advanced/customizations/icons">how to add your custom icons</a>.
## Can I create widgets?
### Some of my online icons are not working (anymore)
It seems the `cdn.jsdelivr.net` database of icon sometimes appear in the lists for DNS-Holes (PiHole, AdGuard Home...). <br />
So make sure to manually whitelist this address.
Yes. [Custom Widgets](/docs/management/custom-widgets) use server-side API requests and safe JSX. They can be written in
the workbench or created with an MCP-connected agent.
### Can I add my own widgets? Do I need programming skills?
We have decided, not to support custom widgets, until Homarr is in a stable and highly polished state.
Enabling users to write their own widgets is easy at the first glance, but there are some major issues with it:
## Can Homarr be exposed to the internet?
- There are mainly two options for adding plugins or custom widgets to Homarr. The first option is, to allow and inject custom code in Homarr. Apps like Nextcloud can do this easily, because they use the Runtime to render all components of the application (In reality, the process is still a bit more complicated). In contrast, Homarr is using Next.js - a framework that needs to be compiled in production. That means, that each time you would modify a custom widget or plugin, the app would have to be restarted. There are options to avoid this, but they are quite flakey and bring disadvantages in our experience. The second option is, to write widgets declaratively instead of imperatively. This way, we would be able to render and modify widgets without any restarting at all. However, we'd have to write our own interpreter for said declarative schema. This is totally possible, but requires a lot of planning and architecture design to function properly. The required work to implement either option is therefore quite substantial.
- Custom widgets would need to be able to request data, so they work properly. We use tRPC for this. Many APIs use different formats and we'd have to convert and read those.
- Using custom widgets, one could easily create harmful widgets, leak your personal data (potentially including your tokens) and trick your users. A proper implementation (eg. official plugin store, where we can ban harmful widgets from) would resolve this problem to a certain extent, but again requires additional work and time to be put in.
Homarr supports authenticated and public deployments. Terminate HTTPS at a reverse proxy, keep Homarr updated, and grant
public board access only when anonymous viewing is intended. See [Board access control](/docs/management/boards#access-control)
and [Single sign-on](/docs/advanced/single-sign-on).
If you still want to add a widget, please consider to contribute it directly to Homarr. For that, basic programming skills are required. Our [Developer Guides](/docs/advanced/development/getting-started) explain this even more detailed.
## Where are the logs?
### Is it secure to expose Homarr to the web?
Use **Management → Tools → Logs** for retained and live application logs. Container startup failures and older events are
available through the deployment platform, for example `docker container logs homarr`.
Yes, since v0.14.0, we are confident that Homarr is secure enough to be exposed to the web.
Make sure to enable the "allow anonymous access" [option in the settings](/docs/management/boards#access-control), if you want to allow people without an account to be able to see your dashboard (not edit)
Use the browser developer console only for client-side failures. Review logs before sharing them because URLs and other
deployment details may be present.
### How do I open the console / log?
There are two different sources of information:
- console: This is your client (browser). It can tell us, if there is an issue on the client (crash, bug, ...).
- log: This is your server. It will tell us, if there are issues with connections for the integrations, crashes and errors.
## Why can Homarr not reach a service behind Gluetun?
Both variants *can* contain personal data. It's recommended, that you don't post them publicly or remove personal data from it.
Gluetun's firewall blocks traffic that is not explicitly allowed. Configure its
[`FIREWALL_OUTBOUND_SUBNETS`](https://github.com/qdm12/gluetun-wiki/blob/main/setup/options/firewall.md) for the relevant
LAN or Docker subnet, and use an address reachable from the Homarr container.
<Tabs>
<TabItem value="apple" label="Read console" default>
## Where do I report a bug or request a feature?
> Note: This will not work on most mobile browsers.
Use the [GitHub issue templates](https://github.com/homarr-labs/homarr/issues/new/choose). For community support, see
[Get in touch](/docs/community/get-in-touch).
Open your browser and ensure, that Homarr is your current tab.
Press ``CTRL`` + ``SHIFT`` + ``I`` on your keyboard. If this shortcut doesn't work, please use one of the alternatives: https://developer.chrome.com/docs/devtools/open/.
Click on ``Console`` in the new window, that opened. There, you should see white, orange and sometimes red messages. You can use the Windows Snip & Sketch tool (or your respective tool on Linux / MacOS) to screenshot those messages.
## Can Homarr be used commercially?
> Please never execute any commands in the console. Scammers are using the console to let you execute harmful commands. If you want to resolve an issue in Homarr, **you'll never have to execute any commands here**
Yes. Homarr is licensed under the [Apache License 2.0](/docs/community/license). Commercial users can support development
through [OpenCollective](/docs/community/donate).
</TabItem>
<TabItem value="orange" label="Read log">
This depends on your installation method. If you use Unraid, you can simply click on the App and on ``Log``. If you have a bare Docker install, use ``docker container logs homarr``. If you used a different name than ``homarr`` for your container, you need to replace said argument. Here is some additional information about the command: https://geekflare.com/check-docker-logs/.
</TabItem>
</Tabs>
### How do I post logs?
Please check [the section above](#how-do-i-open-the-console--log), on how to obtain logs. Logs should always be posted to https://pastebin.com/ or https://rentry.co/.
### Can I use Homarr at my company?
Yes, you can! But please consider a donation, if our work has been useful to you.
### I think I found a bug. What can I do?
If you think, that you found a bug, please create an issue <a href="https://github.com/homarr-labs/homarr/issues/new/choose">here</a>.
### Why can't I use my custom icons?
You need to restart the Homarr Docker container after adding icons.
### Why can't homarr see/ping containers running locally behind GluetunVPN?
This is because GluetunVPN, and many other docker VPN services, secure their VPN connections with strict firewalls. These firewalls often block access to local containers, even on the same docker network. The workaround for <a href="https://github.com/qdm12/gluetun-wiki/blob/main/setup/options/firewall.md">GluetunVPN</a> is to add "- FIREWALL_OUTBOUND_SUBNETS=192.168.1.0/24" into the environmental variables of the docker compose file. If you are using a firewall on your system, you will need to add a rule that allows requests from ip's in the range of 192.168.xxx.xxx . If you are using UFW the command would be "sudo ufw allow from 192.168.0.0/16".
Tip: If you are running your torrent client behind a VPN, and running Sonarr (or something similar) normally, make sure to open your torrent client's port in your firewall. If you don't, Sonarr will not be able to see it. Furthermore, use your machines IP (192.168.x.x) rather then 127.0.0.1 or localhost.
### Can I get in touch with you?
Yes, go <a href="/docs/community/get-in-touch">here</a> to get with us in touch.
<hr/>
:::tip
Haven't found the solution to your problem / question? We are eager to help you on [our Discord](/docs/community/get-in-touch#discord) or in a [GitHub Issue](https://github.com/homarr-labs/homarr/issues/new/choose).
:::
+11 -10
View File
@@ -1,23 +1,24 @@
---
title: Get in touch
description: Community, support, and private contact channels.
title: Get In Touch
description: Still have a question? Click here!
tags:
- Help
- Community
---
# Get in touch
We're happy to talk with you! Shoot us a message at the following places:
## GitHub
## E-Mail
We are open to discuss sponsorships, security vulnerabilities and other private requests through our email.
Please do not report any bugs / feature requests here - they will be ignored and deleted.
Use the [issue templates](https://github.com/homarr-labs/homarr/issues/new/choose) for bugs and feature requests.
[homarr-labs@proton.me](mailto:homarr-labs@proton.me)
## Discord
We have a Discord Server, where we announce new releases, discuss features and bugs and show off our dashboards.
There are over 4'000 members and we're happy to answer your questions or concerns.
You're also open to discuss new / upcoming features and any offtopic selfhosting content.
Use the [Homarr Discord server](https://discord.gg/aCsmEV5RgA) for community support, release discussion, and dashboard
showcases.
[_![](https://discordapp.com/api/guilds/972958686051962910/widget.png?style=banner2)_](https://discord.gg/aCsmEV5RgA)
## Email
Use [homarr-labs@proton.me](mailto:homarr-labs@proton.me) for security reports, sponsorships, and other private matters.
Bug reports and feature requests belong on GitHub.
+2 -1
View File
@@ -7,7 +7,6 @@ tags:
Homarr is licensed under [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0).
## TL;DR
| Action | Allowed |
| -------------- | ------- |
| Copy | ✅ |
@@ -25,6 +24,8 @@ Please consider [making a donation](/docs/community/donate) to Homarr, if this p
:::
## Full License
```txt
+40 -15
View File
@@ -5,24 +5,49 @@ tags:
- Translations
---
# Translations
We want to make Homarr accessible for everyone.
But to do that, we need your help.
We are running a community program that enables everyone to translate Homarr into their language – no programming required.
We are using [Crowdin](https://crowdin.com/) to manage our translations.
You can find the translations [here](https://translate.homarr.dev/).
Homarr translations are managed on [Crowdin](https://translate.homarr.dev/). Select a language, translate strings in
context, and submit them for proofreading. Approved translations ship with a later Homarr release.
## How to translate
## Translate from Homarr
Select **Crowdin (live translations)** as the application language, connect a Crowdin account, and choose the target
language. Select text in Homarr or use Crowdin's in-context panel to edit a string.
### Using crowdin's online platform
Go to the [Crowdin translations](https://translate.homarr.dev/) and select a language you want to translate.
You can then enter the translation mode and start translating.
After you've submitted your translations, a Proofreader will review your changes.
After a translation has been approved, it will be merged to the language development branch.
We merge this one periodically to the development branch.
After the next update of Homarr, all changes from the development branch will be merged, including the changes in the translations.
Live translation submits changes to Crowdin; it does not update the installed Homarr image. Crowdin's preview option can
show pending translations before the next Homarr release.
### Using live translate
You can also help translate homarr directly from the app!
For this you can simply change your language to "Crowdin (live translations)"
![](./img/crowdin-language-select.png)
Then you'll be prompted to connect to your Crowdin account and choose the language in which you want to transalte.
You can then start translating directly from the app by selecting some text on the app.
If some text cannot be selected to be edited with the live translation tool, you can still translate it inside of the In-Context popup at the Home tab, it will be red if it's not translated yet.
![](./img/crowdin-live-translate.png)
:::caution
Although you are translating directly from the app, your translations will not be directly be available inside of your language, they will be in the next update of Homarr, because we have to push the new translations to a new docker image.
If you really want to use them you can enable the "preview transaltions" option in the live translate settings, but be aware that this is not recommended for performance reasons.
:::
:::tip
Do you want to become a proofreader? [Let us know on our Discord](/docs/community/get-in-touch#discord)! We will add you to the proofreader team. You can then review the translations and approve them. **Note:** You must speak the language fluently, have it as your native language or lots of experience with it.
:::
## Guidelines
- Preserve the meaning and context of the English source.
- Keep terminology consistent with existing translations.
- Do not add punctuation, formatting, or explanatory text that is absent from the source.
- Improve an existing translation only when the replacement is more accurate.
Contact the [Discord community](/docs/community/get-in-touch#discord) to help proofread a language.
1. Translate with context: Some translations can't be translated 1:1 - they need context. Ensure that your translations make sense in the context.
2. Keep things simple and original - no additional characters are needed.
3. Don't translate the same thing twice - if you see a translation that is already translated, don't translate it again unless you have a better translation.
5. Keep your translations as close to the original as possible.
@@ -6,76 +6,411 @@ tags:
- Configuration
---
On first launch, onboarding creates the administrator, connects optional services, and builds the first board. A
compatible SQLite backup can also be restored from the welcome screen.
import imageStartFromScratchStepServerSettings from "./img/onboarding-from-scratch-step-server-settings.png";
import imageStartFromScratchStepAdminUser from "./img/onboarding-from-scratch-step-admin-user.png";
import imageStartFromScratchStepExternalAdminGroup from "./img/onboarding-from-scratch-step-external-admin-group.png";
import imageStartFromScratchStepFinish from "./img/onboarding-from-scratch-step-finish.png";
import manageBoardHeaderEditMode from "./img/manage-board-header-edit-mode.png";
import manageBoardHeaderAddItem from "./img/manage-board-header-add-item-menu.png";
import manageBoardHeaderChooseItem from "./img/manage-board-header-choose-item.png";
import manageBoardNoApp from "./img/manage-board-no-app.png";
import manageExportOldHomarr from "./img/old-homarr-export-data.png";
import manageImportOldHomarrDropzone from "./img/old-homarr-import-dropzone.png";
import manageImportOldHomarrChooseData from "./img/old-homarr-import-choose-imported-data.png";
import manageImportDecryptionToken from "./img/old-homarr-import-decryption-token.png";
If onboarding returns after a restart, check that `/appdata` is writable and persisted. See the
[Docker installation guide](/docs/getting-started/installation/docker).
:::tip
## Core concepts
Read our [glossary](/docs/getting-started/glossary) if you are new to Homarr.
### Boards
:::
A **board** is a dashboard page containing apps, widgets, and Containers. Boards can be public or restricted to users
and groups. Each user can choose a home board.
Congratulations on installing Homarr. Now it's time to get yourself familiar with the concepts of Homarr and how to use it.
![Board management with board previews](./img/after-installation-boards.webp)
## Onboarding
See [Boards](/docs/management/boards) for responsive layouts, access control, appearance, and home-board settings.
The onboard will show the first time you start Homarr.
This process will help you with your first steps and adding some initial data, such as the administrator account.
### Apps
:::note
An **app** is a saved shortcut: a name, URL, icon, and optional status check. Add an App tile or a Bookmarks widget to
show apps on a board.
If you see this screen repeatitly after you restarted the Homarr container, your Docker mounting paths may be missing,
incorrect or read-only. Read [our installation instructions](/docs/getting-started/installation/docker) again to fix this issue.
![App management with saved shortcuts](./img/after-installation-apps.webp)
:::
See [Apps](/docs/management/apps) or [Icons](/docs/advanced/icons).
You can either import a previous dashboard using the ZIP archive or start from scratch:
### Integrations
![Screenshot of Step 1 onboarding](./img/onboarding-step-1.png)
An **integration** is a server-side connection from Homarr to a supported service such as Sonarr, Home Assistant, or
Docker. Integrations provide data and actions to widgets. They can also be linked to apps.
You may refresh anytime and continue from a different device - the onboarding is saved immediately on the server for your convenience.
![Integration catalog with compatible widgets](./img/after-installation-integrations.webp)
### Starting from scratch
See [Managing integrations](/docs/management/integrations) and the
[integration catalog](/docs/category/integrations).
Depending on your auth provider configuration you will either first see the admin user creation form or the external group creation form.
If the credentials provider was selected, you'll be prompted to create an admin user for Homarr.
This user cannot be deleted and has full control over Homarr and all of its boards.
You must choose a username and password (both can later be changed).
We enforce strict password requirements when you enter your password to protect your instance if you accidentally
expose it to the internet.
### Widgets
<img
src={imageStartFromScratchStepAdminUser}
alt={"Screenshot form to create admin user"}
width={300}
/>
A **widget** is a board tile. Some widgets work on their own; others require one or more integrations. Configuration,
supported integrations, and non-obvious limitations are documented on each widget page.
If you selected external providers (such as LDAP or OIDC), you'll be prompted to create an admin group.
This group will be used to synchronize users from your provider to Homarr.
When a user logs in through the provider, any of their groups that match groups in Homarr will be assigned to the user.
The name of the group must match exactly the name of the group in the provider.
![Board in edit mode with the add-content menu](./img/after-installation-widgets.webp)
<img
src={imageStartFromScratchStepExternalAdminGroup}
alt={"Screenshot form to create external admin group"}
width={300}
/>
See the [widget catalog](/docs/category/widgets).
Next, you can configure some basic settings for Homarr. We recommend to leave all settings at default for most users.
Here you can also disable analytics. Please note that our analytics are completly anonymous and is fully compliant with GDPR and PECR.
### Containers and rails
<img
src={imageStartFromScratchStepServerSettings}
alt={"Screenshot of analytics and indexing settings in the onboarding"}
width={300}
/>
A **Container** groups board items and can be nested. A **rail** reserves a fixed area at the left or right of a
responsive layout. Both are configured in board edit mode.
### Setting up integrations
![Board layout settings with fixed sidebars](./img/after-installation-containers-rails.webp)
After configuring server settings, the onboarding guides you through setting up your integrations.
See [Boards](/docs/management/boards#containers-and-rails).
#### Docker service discovery
### Users, groups, and permissions
If Docker is available (socket mounted), Homarr automatically detects running containers and matches them to known integration types.
Matching integrations are pre-selected in the integration list, saving you from manually picking each one.
Users inherit permissions and default boards from their groups. Resource-specific access can further restrict boards
and integrations.
#### URL templating
![Group permission settings](./img/after-installation-users-permissions.webp)
You can define a base URL template to auto-fill integration URLs in bulk.
Two modes are supported:
See [Users and groups](/docs/management/users) and [single sign-on](/docs/advanced/single-sign-on).
- **Subdomain mode** - e.g. `{app}.example.com` where `{app}` is replaced with the integration name
- **Port mode** - e.g. `192.168.1.1:{port}` where `{port}` is replaced with the integration's default port
## Explore further
#### API key helper links
- [Server settings](/docs/management/settings)
- [Assistant](/docs/management/assistant)
- [Custom Widgets](/docs/management/custom-widgets)
- [Community Workshop](/docs/workshop)
- [API](/docs/management/api) and [MCP](/docs/management/mcp)
- [Keyboard shortcuts](/docs/advanced/keyboard-shortcuts)
For integrations that require API keys (such as Sonarr, Radarr, SABnzbd, or Seerr), a helper link opens the relevant settings page in the target application so you can quickly copy your key.
See the [glossary](/docs/getting-started/glossary) for a compact list of Homarr terms.
#### Creating apps from Docker containers
You can optionally auto-create bookmark apps for all discovered Docker containers, making them immediately available on your board.
:::note
Release providers and Search.ch are configured directly in their widgets instead of the integrations page.
:::
After this step, you finished the onboarding.
The onboarding can no longer be started again.
From here you can click on one of the provided links to continue your journey.
We recommend you to continue with the section [creating your first board on this page](#creating-your-first-board).
<img
src={imageStartFromScratchStepFinish}
alt={"Screenshot of the finish onboarding process with links"}
width={300}
/>
### Importing a ZIP from version before 1.0.0
This is recommended when you used Homarr before and want to re-import your data.
It is important that you first migrate correctly using [the list of breaking changes for 1.0](/blog/2024/09/23/version-1.0).
<img
src={manageExportOldHomarr}
alt="Export from old Homarr"
style={{ maxWidth: 500 }}
/>
**How to export and import data**
1. Navigate to your old Homarr, open your management pages, open the tools submenu and open `Migrate to 1.0`.
2. Check the contents that you want to export. We recommend you to export all data.
3. Click on the export data button
4. Save the ZIP file, that is being downloaded, on your device. If you selected "integrations" or "users", you also get an encryption / decryption key.
Copy it and do not loose it!
5. Go to your new >1.0.0 Homarr instance and start the onboarding. Choose `Import from Homarr before 1.0`
6. Drag and drop your previously downloaded ZIP archive into the file upload: <br/><img src={manageImportOldHomarrDropzone} style={{ maxWidth: 500 }} />
7. Next, choose the data that you want to import and how to import it. Since screen sizes were removed in 1.0, we create a new board for every screen size by default. We recommend you to only keep the best layout and adjust it later in 1.0.0 - but you can also import all of them and pick later.<br/><img src={manageImportOldHomarrChooseData} style={{ maxWidth: 500 }} />
8. If you previously exported integrations or users, you will be asked to decrypt the data safely using your obtained encryption token. Paste into the modal and confirm your action.<br/><img src={manageImportDecryptionToken} style={{ maxWidth: 500 }} />
9. Next, you can [follow the same setup as when starting from scratch](#starting-from-scratch). After you completed the onboarding, all of your data will be migrated and available.
## Creating your first board
In Homarr, you organize your apps and widgets on boards.
Boards can be organized using the interactive drag and drop.
Let's create your first board by navigating here: `Manage` > `Boards` > `New board`:
![](./img/manage-create-board-button.png)
A board must always have a name and can either be public or private.
Public boards are also accessible for users, that are not logged in ([see access control for boards](/docs/management/boards/#access-control)).
![](./img/manage-create-board-modal.png)
Congratulations - you've just created your first board. You can see boards, that you have access to, on this page:
![](./img/manage-view-board.png)
## Add your first app to Homarr
To add links to your dashboard, you must first add them as an "app".
Navigate to this page to add an app:
![](./img/manage-navigate-apps.png)
Next, click on the `New app` button. You'll need to provide a name and an icon to create an app.
The name must be at least one character long but you can provide any image URL for the icon.
Homarr ships with over 11K icons - so you can just search by entering a search text instead.
![](./img/manage-add-app-form.png)
After you created the app, you'll see in on the apps page:
![](./img/manage-created-app.png)
## Arrange and organize your board
Let's add your freshly created app, from the previous step, to the board.
To do this, edit your previously created board by clicking on the big `Open board` button:
![](./img/manage-view-board.png)
Here you have the following options in the header:
![](./img/manage-board-header.png)
- **Search**: Here you can search for data. You can search for integrations, boards, actions, sites and much more. Some actions may be context dependent (eg. will only show when certain widgets are on your page)
- **Edit mode**: Clicking this button will toggle the edit mode. In this mode you can edit the contents of your board.
- **Settings**: Clicking this button will redirect you to a settings page where you can adjust the board settings (page title, colors, ...).
- **Profile**: This avatar will be visible on every page. Clicking it will show a menu with actions.
When entering the edit mode, you'll get some additional buttons to work with:
<img
src={manageBoardHeaderEditMode}
alt={"Screenshot of buttons in the edit mode"}
width={200}
/>
- **Add item**: Click this button to add new elements to your page.
- **Exit edit mode**: Click this btuton to save your changes and exit edit mode.
To continue, click the option "New Item":
<img
src={manageBoardHeaderAddItem}
alt={"Screenshot of the add item menu"}
width={200}
/>
Next, you can choose what item you'd like to add.
We will discuss the other options in the following sections - for now choose "App" to continue.
<img
src={manageBoardHeaderChooseItem}
alt={"Screenshot of choosing the app item in the modal"}
width={300}
/>
As soon as you chose the app, the modal will close and you'll get the following item on your board:
<img
src={manageBoardNoApp}
alt="Screenshot of the No app element on the board"
/>
This is expected, because you haven't told Homarr what app it should show in this item.
To configure this, click on the small 3 dots at the top of the item in the edit mode and choose "Edit item" in the menu.
![](./img/manage-board-edit-app.gif)
In the modal you can edit the app that you want to display:
![](./img/manage-board-choose-app.png)
As soon as you save your changes, the tile on the board will refresh and display your app.
Using the small bidirectional arrow at the bottom right you can resize the element.
Start pressing with your mouse button down, move your mouse to resize and release your finger as soon your you're happy with the size.
![](./img/resize-item.gif)
To move your item to a different position on the board, simply hold the mouse button anywhere on the item and move with the mouse to the desired position.
As soon as you release your finger, the item will snap in place in the grid.
![](./img/move-item.gif)
:::note
You cannot move items freely on the vertical axis.
We always align all items to the top of the page to simply organization.
To move items to the bottom, you must add more items or resize items.
:::
:::tip
We recommend you to experiment for a few minutes, before you start building your actual dashboard.
This helps you to explore the capabilities of Homarr before you build your board in a suboptimal way.
:::
Users on touch or mobile devices may not be able to drag and drop or move items.
They can use the "Move / resize item" option in the edit menu of items instead:
![](./img/manage-board-move-resize-item-menu.png)
Be sure to save your changes by exiting edit mode!
![](./img/exit-edit-mode.png)
## Adding your first integration
Now it's time to integrate some basic data into your board.
To do this, we first need to create an integration.
An integration is a connection between Homarr and a third party application - usually something that you run and deploy yourself.
Navigate to `Manage` > `Integrations` (for example via your user menu).
As an example, we are going to use the public demo instance of **Dash.** - a popular application to monitor CPU / hardware usage.
Choose the **Dash.** integration from the list of available integrations:
![](./img/choose-integration.gif)
Next, choose a name (or leave it at default), enter `https://dash.mauz.dev/` as the URL in the form and click on `Test connection and create`.
Pressing the save button will perform a test request to the integration, to confirm that the networking setup is working as expected.
Sometimes invalid configurations, firewalls and other mistakes can lead to connectivity problems - [follow this guide here](/docs/management/integrations/#troubleshooting-connection) if you experience issues.
If connection was successful, you will be redirect back to the list of integrations.
Now let's embed it to your board (see next section).
:::note
Some integrations may require additional configuration, such as a username, password or API tokens.
Depending on your networking setup and applications, the connection from Homarr might also get blocked by firewalls,
the target application or your router.
:::
## Embed integration data using widgets
Let's add your integration, that you have configured in the section above, as a widget to your board.
First, navigate back to your board that you want to add it to.
Next, enter edit mode again and add another item to your board:
![](./img/manage-board-header-add-item-menu.png)
Instead of choosing an app, let's choose `System Health Monitoring` this time.
Click on "Add to board":
![](./img/manage-board-header-choose-system-health.png)
Resize the item slightly and then edit it using the three dots menu at the top:
![](./img/edit-system-health-widget.gif)
Next, choose your newly created Dash. integration in the integrations dropdown:
![](./img/manage-board-edit-system-health.png)
:::note
Some widgets may support multiple integrations at the same time.
If they do not support multiple integrations, you will be unable to select more than one.
:::
Confirm your changes by pressing save.
Editing a widget will trigger a data update and data will soon appear:
![](./img/widget-system-health.png)
As with every change, don't forget to save your changes using the exit edit mode button:
![](./img/exit-edit-mode.png)
## Grouping your items
As the last step, let's organize your freshly created items using two different options.
You can choose either or both of them on your boards.
### Using dynamic sections
Dynamic sections are a simple, resizable visual group to group items.
![](./img/create-dynamic-section.png)
After adding a dynamic section, you can simply drag and drop items into the section:
![](./img/dynamic-section.gif)
### Using categories
Categories are more advanced and complicated than dynamic sections.
To create a category, open the add item modal and choose "new category":
![](./img/add-category.png)
Every category must have a name. After you click "Add category", you will get a new item on your board:
![](./img/category.gif)
You can drag and drop items in to this category in the edit mode.
Categories will have a big space before and after them in the edit mode to
enable you to drag and drop items outside the category.
You can also collapse or open categories in edit mode to facilitate your editing process.
:::note
A current limitation of categories is, that they will always have a width of 100%.
You cannot reduce their horizontal width.
:::
## Guided tours
After completing the onboarding, Homarr provides interactive guided tours to help you discover key features.
### Management tour
When you first visit the management area (`/manage`), Homarr walks you through the main pages: boards, apps, integrations, and users (admin only).
Each page highlights the list view and the create action so you know where to find and add things.
### Board tour
When you first open a board, a tour highlights the board header elements: search, edit mode, settings, board switcher, and user menu.
### Tour controls
- Press **Enter** (or click **Next**) to advance through tour steps
- Tours auto-start on first visit and do not repeat once completed
- Tours are skipped on mobile screens (below `sm` breakpoint)
- You can **reset tours** from the user settings page under **Manage > Users > [Your user] > General**
## Next steps
You're done with the basic setup 🔥. Now it's time to organize and customize even more.
We highly recommend you to experiment with different options, styles and configurations.
See these curated pages for your next steps:
- Create new apps using the [apps management](/docs/management/apps/)
- Customize your board or add more boards using the [boards management](/docs/management/boards/)
- Integrate software that you use using the [integrations management](/docs/management/integrations/)
- Choose from our beautiful and interactive [widgets](/docs/category/widgets)
- See [all supported integrations](/docs/category/integrations)
- Integrate with Homarr with your own application using [the API](/docs/management/api/)
:::danger
The Homarr Team will never ask for your credentials. Do not send people your credentials, as they manage the access to your applications and can cause harm to your device if they are abused.
:::
Loaded 100 of 2751 files, more files were not shown because too many files have changed in this diff. Show more