refactor: replace Biome with oxlint + oxfmt

Migrate the entire monorepo from Biome 2.4.7 to oxlint 1.56.0 (linter)
and oxfmt 0.41.0 (formatter) for faster lint/format and broader rule
coverage.

- Add `.oxlintrc.json` with React, TypeScript, unicorn, import plugins
  and correctness/suspicious categories
- Add `.oxfmtrc.json` with 2-space indent, import sorting, and Tailwind
  class sorting (all 30+ custom className attributes migrated)
- Add `docs/.oxlintrc.json` and `docs/.oxfmtrc.json` with Next.js plugin
- Update all 12 workspace package.json scripts: `oxlint`, `oxfmt`,
  `oxfmt --check`
- Add `format:check` turbo task and CI step
- Update VS Code settings/extensions to use `oxc.oxc-vscode`
- Update CI path triggers from `biome.json` to new config files
- Remove all `biome-ignore` comments and fix shadowed variables
- Delete `biome.json` and `docs/biome.json`
- Reformat entire codebase with oxfmt

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-18 13:26:50 -04:00
co-authored by Claude Opus 4.6
parent a80c8c9a0c
commit 2b0c683b7b
359 changed files with 3894 additions and 7208 deletions
+23
View File
@@ -0,0 +1,23 @@
{
"$schema": "./node_modules/oxfmt/configuration_schema.json",
"proseWrap": "never",
"sortImports": {
"groups": ["builtin", "external", "internal", "parent", "sibling", "index"],
"internalPattern": ["@/", "@sofa/", "collections/"],
"newlinesBetween": true,
"order": "asc"
},
"sortTailwindcss": {
"attributes": ["classList"],
"functions": ["cn", "clsx", "cva"]
},
"ignorePatterns": [
"node_modules",
".next",
"dist",
"build",
".source",
"public/openapi.json",
"content/docs/api/**/*.mdx"
]
}
+14
View File
@@ -0,0 +1,14 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"extends": ["../.oxlintrc.json"],
"plugins": ["eslint", "typescript", "unicorn", "oxc", "react", "nextjs", "import"],
"categories": {
"correctness": "error",
"suspicious": "warn"
},
"rules": {
"react/react-in-jsx-scope": "off",
"import/no-unassigned-import": "off"
},
"ignorePatterns": ["node_modules", ".next", "dist", "build", ".source", "public/openapi.json"]
}
+3 -6
View File
@@ -1,7 +1,6 @@
# sofa-docs
This is a Next.js application generated with
[Create Fumadocs](https://github.com/fuma-nama/fumadocs).
This is a Next.js application generated with [Create Fumadocs](https://github.com/fuma-nama/fumadocs).
Run development server:
@@ -36,10 +35,8 @@ Read the [Introduction](https://fumadocs.dev/docs/mdx) for further details.
## Learn More
To learn more about Next.js and Fumadocs, take a look at the following
resources:
To learn more about Next.js and Fumadocs, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js
features and API.
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
- [Fumadocs](https://fumadocs.dev) - learn about Fumadocs
-48
View File
@@ -1,48 +0,0 @@
{
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
},
"files": {
"ignoreUnknown": true,
"includes": [
"**",
"!node_modules",
"!.next",
"!dist",
"!build",
"!.source",
"!public/openapi.json"
]
},
"css": {
"parser": {
"cssModules": true,
"tailwindDirectives": true
}
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
},
"domains": {
"next": "recommended",
"react": "recommended"
}
},
"assist": {
"actions": {
"source": {
"organizeImports": "on"
}
}
}
}
+142 -77
View File
@@ -12,21 +12,22 @@
"fumadocs-openapi": "^10.3.18",
"fumadocs-ui": "^16.6.17",
"lucide-react": "^0.577.0",
"next": "16.1.6",
"next": "16.1.7",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"shiki": "^4.0.2",
"tailwind-merge": "^3.5.0",
},
"devDependencies": {
"@biomejs/biome": "^2.4.7",
"@tailwindcss/postcss": "^4.2.1",
"@tailwindcss/postcss": "^4.2.2",
"@types/mdx": "^2.0.13",
"@types/node": "^25.5.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"oxfmt": "0.41.0",
"oxlint": "1.56.0",
"postcss": "^8.5.8",
"tailwindcss": "^4.2.1",
"tailwindcss": "^4.2.2",
"typescript": "^5.9.3",
},
},
@@ -34,24 +35,6 @@
"packages": {
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
"@biomejs/biome": ["@biomejs/biome@2.4.7", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.7", "@biomejs/cli-darwin-x64": "2.4.7", "@biomejs/cli-linux-arm64": "2.4.7", "@biomejs/cli-linux-arm64-musl": "2.4.7", "@biomejs/cli-linux-x64": "2.4.7", "@biomejs/cli-linux-x64-musl": "2.4.7", "@biomejs/cli-win32-arm64": "2.4.7", "@biomejs/cli-win32-x64": "2.4.7" }, "bin": { "biome": "bin/biome" } }, "sha512-vXrgcmNGZ4lpdwZSpMf1hWw1aWS6B+SyeSYKTLrNsiUsAdSRN0J4d/7mF3ogJFbIwFFSOL3wT92Zzxia/d5/ng=="],
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Oo0cF5mHzmvDmTXw8XSjhCia8K6YrZnk7aCS54+/HxyMdZMruMO3nfpDsrlar/EQWe41r1qrwKiCa2QDYHDzWA=="],
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-I+cOG3sd/7HdFtvDSnF9QQPrWguUH7zrkIMMykM3PtfWU9soTcS2yRb9Myq6MHmzbeCT08D1UmY+BaiMl5CcoQ=="],
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-om6FugwmibzfP/6ALj5WRDVSND4H2G9X0nkI1HZpp2ySf9lW2j0X68oQSaHEnls6666oy4KDsc5RFjT4m0kV0w=="],
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-I2NvM9KPb09jWml93O2/5WMfNR7Lee5Latag1JThDRMURVhPX74p9UDnyTw3Ae6cE1DgXfw7sqQgX7rkvpc0vw=="],
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.7", "", { "os": "linux", "cpu": "x64" }, "sha512-bV8/uo2Tj+gumnk4sUdkerWyCPRabaZdv88IpbmDWARQQoA/Q0YaqPz1a+LSEDIL7OfrnPi9Hq1Llz4ZIGyIQQ=="],
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.7", "", { "os": "linux", "cpu": "x64" }, "sha512-00kx4YrBMU8374zd2wHuRV5wseh0rom5HqRND+vDldJPrWwQw+mzd/d8byI9hPx926CG+vWzq6AeiT7Yi5y59g=="],
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-hOUHBMlFCvDhu3WCq6vaBoG0dp0LkWxSEnEEsxxXvOa9TfT6ZBnbh72A/xBM7CBYB7WgwqboetzFEVDnMxelyw=="],
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.7", "", { "os": "win32", "cpu": "x64" }, "sha512-qEpGjSkPC3qX4ycbMUthXvi9CkRq7kZpkqMY1OyhmYlYLnANnooDQ7hDerM8+0NJ+DZKVnsIc07h30XOpt7LtQ=="],
"@emnapi/runtime": ["@emnapi/runtime@1.9.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q=="],
@@ -114,9 +97,9 @@
"@floating-ui/utils": ["@floating-ui/utils@0.2.11", "", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="],
"@formatjs/fast-memoize": ["@formatjs/fast-memoize@3.1.0", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-b5mvSWCI+XVKiz5WhnBCY3RJ4ZwfjAidU0yVlKa3d3MSgKmH1hC3tBGEAtYyN5mqL7N0G5x0BOUYyO8CEupWgg=="],
"@formatjs/fast-memoize": ["@formatjs/fast-memoize@3.1.1", "", {}, "sha512-CbNbf+tlJn1baRnPkNePnBqTLxGliG6DDgNa/UtV66abwIjwsliPMOt0172tzxABYzSuxZBZfcp//qI8AvBWPg=="],
"@formatjs/intl-localematcher": ["@formatjs/intl-localematcher@0.8.1", "", { "dependencies": { "@formatjs/fast-memoize": "3.1.0", "tslib": "^2.8.1" } }, "sha512-xwEuwQFdtSq1UKtQnyTZWC+eHdv7Uygoa+H2k/9uzBVQjDyp9r20LNDNKedWXll7FssT3GRHvqsdJGYSUWqYFA=="],
"@formatjs/intl-localematcher": ["@formatjs/intl-localematcher@0.8.2", "", { "dependencies": { "@formatjs/fast-memoize": "3.1.1" } }, "sha512-q05KMYGJLyqFNFtIb8NhWLF5X3aK/k0wYt7dnRFuy6aLQL+vUwQ1cg5cO4qawEiINybeCPXAWlprY2mSBjSXAQ=="],
"@fumadocs/tailwind": ["@fumadocs/tailwind@0.0.3", "", { "dependencies": { "postcss-selector-parser": "^7.1.1" }, "peerDependencies": { "tailwindcss": "^4.0.0" }, "optionalPeers": ["tailwindcss"] }, "sha512-/FWcggMz9BhoX+13xBoZLX+XX9mYvJ50dkTqy3IfocJqua65ExcsKfxwKH8hgTO3vA5KnWv4+4jU7LaW2AjAmQ=="],
@@ -186,26 +169,102 @@
"@mdx-js/mdx": ["@mdx-js/mdx@3.1.1", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="],
"@next/env": ["@next/env@16.1.6", "", {}, "sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ=="],
"@next/env": ["@next/env@16.1.7", "", {}, "sha512-rJJbIdJB/RQr2F1nylZr/PJzamvNNhfr3brdKP6s/GW850jbtR70QlSfFselvIBbcPUOlQwBakexjFzqLzF6pg=="],
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.1.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw=="],
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.1.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-b2wWIE8sABdyafc4IM8r5Y/dS6kD80JRtOGrUiKTsACFQfWWgUQ2NwoUX1yjFMXVsAwcQeNpnucF2ZrujsBBPg=="],
"@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.1.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ=="],
"@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.1.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-zcnVaaZulS1WL0Ss38R5Q6D2gz7MtBu8GZLPfK+73D/hp4GFMrC2sudLky1QibfV7h6RJBJs/gOFvYP0X7UVlQ=="],
"@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.1.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw=="],
"@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.1.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-2ant89Lux/Q3VyC8vNVg7uBaFVP9SwoK2jJOOR0L8TQnX8CAYnh4uctAScy2Hwj2dgjVHqHLORQZJ2wH6VxhSQ=="],
"@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.1.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ=="],
"@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.1.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-uufcze7LYv0FQg9GnNeZ3/whYfo+1Q3HnQpm16o6Uyi0OVzLlk2ZWoY7j07KADZFY8qwDbsmFnMQP3p3+Ftprw=="],
"@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.1.6", "", { "os": "linux", "cpu": "x64" }, "sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ=="],
"@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.1.7", "", { "os": "linux", "cpu": "x64" }, "sha512-KWVf2gxYvHtvuT+c4MBOGxuse5TD7DsMFYSxVxRBnOzok/xryNeQSjXgxSv9QpIVlaGzEn/pIuI6Koosx8CGWA=="],
"@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.1.6", "", { "os": "linux", "cpu": "x64" }, "sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg=="],
"@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.1.7", "", { "os": "linux", "cpu": "x64" }, "sha512-HguhaGwsGr1YAGs68uRKc4aGWxLET+NevJskOcCAwXbwj0fYX0RgZW2gsOCzr9S11CSQPIkxmoSbuVaBp4Z3dA=="],
"@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.1.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw=="],
"@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.1.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-S0n3KrDJokKTeFyM/vGGGR8+pCmXYrjNTk2ZozOL1C/JFdfUIL9O1ATaJOl5r2POe56iRChbsszrjMAdWSv7kQ=="],
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.1.6", "", { "os": "win32", "cpu": "x64" }, "sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A=="],
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.1.7", "", { "os": "win32", "cpu": "x64" }, "sha512-mwgtg8CNZGYm06LeEd+bNnOUfwOyNem/rOiP14Lsz+AnUY92Zq/LXwtebtUiaeVkhbroRCQ0c8GlR4UT1U+0yg=="],
"@orama/orama": ["@orama/orama@3.1.18", "", {}, "sha512-a61ljmRVVyG5MC/698C8/FfFDw5a8LOIvyOLW5fztgUXqUpc1jOfQzOitSCbge657OgXXThmY3Tk8fpiDb4UcA=="],
"@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.41.0", "", { "os": "android", "cpu": "arm" }, "sha512-REfrqeMKGkfMP+m/ScX4f5jJBSmVNYcpoDF8vP8f8eYPDuPGZmzp56NIUsYmx3h7f6NzC6cE3gqh8GDWrJHCKw=="],
"@oxfmt/binding-android-arm64": ["@oxfmt/binding-android-arm64@0.41.0", "", { "os": "android", "cpu": "arm64" }, "sha512-s0b1dxNgb2KomspFV2LfogC2XtSJB42POXF4bMCLJyvQmAGos4ZtjGPfQreToQEaY0FQFjz3030ggI36rF1q5g=="],
"@oxfmt/binding-darwin-arm64": ["@oxfmt/binding-darwin-arm64@0.41.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-EGXGualADbv/ZmamE7/2DbsrYmjoPlAmHEpTL4vapLF4EfVD6fr8/uQDFnPJkUBjiSWFJZtFNsGeN1B6V3owmA=="],
"@oxfmt/binding-darwin-x64": ["@oxfmt/binding-darwin-x64@0.41.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-WxySJEvdQQYMmyvISH3qDpTvoS0ebnIP63IMxLLWowJyPp/AAH0hdWtlo+iGNK5y3eVfa5jZguwNaQkDKWpGSw=="],
"@oxfmt/binding-freebsd-x64": ["@oxfmt/binding-freebsd-x64@0.41.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Y2kzMkv3U3oyuYaR4wTfGjOTYTXiFC/hXmG0yVASKkbh02BJkvD98Ij8bIevr45hNZ0DmZEgqiXF+9buD4yMYQ=="],
"@oxfmt/binding-linux-arm-gnueabihf": ["@oxfmt/binding-linux-arm-gnueabihf@0.41.0", "", { "os": "linux", "cpu": "arm" }, "sha512-ptazDjdUyhket01IjPTT6ULS1KFuBfTUU97osTP96X5y/0oso+AgAaJzuH81oP0+XXyrWIHbRzozSAuQm4p48g=="],
"@oxfmt/binding-linux-arm-musleabihf": ["@oxfmt/binding-linux-arm-musleabihf@0.41.0", "", { "os": "linux", "cpu": "arm" }, "sha512-UkoL2OKxFD+56bPEBcdGn+4juTW4HRv/T6w1dIDLnvKKWr6DbarB/mtHXlADKlFiJubJz8pRkttOR7qjYR6lTA=="],
"@oxfmt/binding-linux-arm64-gnu": ["@oxfmt/binding-linux-arm64-gnu@0.41.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-gofu0PuumSOHYczD8p62CPY4UF6ee+rSLZJdUXkpwxg6pILiwSDBIouPskjF/5nF3A7QZTz2O9KFNkNxxFN9tA=="],
"@oxfmt/binding-linux-arm64-musl": ["@oxfmt/binding-linux-arm64-musl@0.41.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-VfVZxL0+6RU86T8F8vKiDBa+iHsr8PAjQmKGBzSCAX70b6x+UOMFl+2dNihmKmUwqkCazCPfYjt6SuAPOeQJ3g=="],
"@oxfmt/binding-linux-ppc64-gnu": ["@oxfmt/binding-linux-ppc64-gnu@0.41.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-bwzokz2eGvdfJbc0i+zXMJ4BBjQPqg13jyWpEEZDOrBCQ91r8KeY2Mi2kUeuMTZNFXju+jcAbAbpyJxRGla0eg=="],
"@oxfmt/binding-linux-riscv64-gnu": ["@oxfmt/binding-linux-riscv64-gnu@0.41.0", "", { "os": "linux", "cpu": "none" }, "sha512-POLM//PCH9uqDeNDwWL3b3DkMmI3oI2cU6hwc2lnztD1o7dzrQs3R9nq555BZ6wI7t2lyhT9CS+CRaz5X0XqLA=="],
"@oxfmt/binding-linux-riscv64-musl": ["@oxfmt/binding-linux-riscv64-musl@0.41.0", "", { "os": "linux", "cpu": "none" }, "sha512-NNK7PzhFqLUwx/G12Xtm6scGv7UITvyGdAR5Y+TlqsG+essnuRWR4jRNODWRjzLZod0T3SayRbnkSIWMBov33w=="],
"@oxfmt/binding-linux-s390x-gnu": ["@oxfmt/binding-linux-s390x-gnu@0.41.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-qVf/zDC5cN9eKe4qI/O/m445er1IRl6swsSl7jHkqmOSVfknwCe5JXitYjZca+V/cNJSU/xPlC5EFMabMMFDpw=="],
"@oxfmt/binding-linux-x64-gnu": ["@oxfmt/binding-linux-x64-gnu@0.41.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ojxYWu7vUb6ysYqVCPHuAPVZHAI40gfZ0PDtZAMwVmh2f0V8ExpPIKoAKr7/8sNbAXJBBpZhs2coypIo2jJX4w=="],
"@oxfmt/binding-linux-x64-musl": ["@oxfmt/binding-linux-x64-musl@0.41.0", "", { "os": "linux", "cpu": "x64" }, "sha512-O2exZLBxoCMIv2vlvcbkdedazJPTdG0VSup+0QUCfYQtx751zCZNboX2ZUOiQ/gDTdhtXvSiot0h6GEGkOyalA=="],
"@oxfmt/binding-openharmony-arm64": ["@oxfmt/binding-openharmony-arm64@0.41.0", "", { "os": "none", "cpu": "arm64" }, "sha512-N+31/VoL+z+NNBt8viy3I4NaIdPbiYeOnB884LKqvXldaE2dRztdPv3q5ipfZYv0RwFp7JfqS4I27K/DSHCakg=="],
"@oxfmt/binding-win32-arm64-msvc": ["@oxfmt/binding-win32-arm64-msvc@0.41.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z7NAtu/RN8kjCQ1y5oDD0nTAeRswh3GJ93qwcW51srmidP7XPBmZbLlwERu1W5veCevQJtPS9xmkpcDTYsGIwQ=="],
"@oxfmt/binding-win32-ia32-msvc": ["@oxfmt/binding-win32-ia32-msvc@0.41.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-uNxxP3l4bJ6VyzIeRqCmBU2Q0SkCFgIhvx9/9dJ9V8t/v+jP1IBsuaLwCXGR8JPHtkj4tFp+RHtUmU2ZYAUpMA=="],
"@oxfmt/binding-win32-x64-msvc": ["@oxfmt/binding-win32-x64-msvc@0.41.0", "", { "os": "win32", "cpu": "x64" }, "sha512-49ZSpbZ1noozyPapE8SUOSm3IN0Ze4b5nkO+4+7fq6oEYQQJFhE0saj5k/Gg4oewVPdjn0L3ZFeWk2Vehjcw7A=="],
"@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.56.0", "", { "os": "android", "cpu": "arm" }, "sha512-IyfYPthZyiSKwAv/dLjeO18SaK8MxLI9Yss2JrRDyweQAkuL3LhEy7pwIwI7uA3KQc1Vdn20kdmj3q0oUIQL6A=="],
"@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.56.0", "", { "os": "android", "cpu": "arm64" }, "sha512-Ga5zYrzH6vc/VFxhn6MmyUnYEfy9vRpwTIks99mY3j6Nz30yYpIkWryI0QKPCgvGUtDSXVLEaMum5nA+WrNOSg=="],
"@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.56.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ogmbdJysnw/D4bDcpf1sPLpFThZ48lYp4aKYm10Z/6Nh1SON6NtnNhTNOlhEY296tDFItsZUz+2tgcSYqh8Eyw=="],
"@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.56.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-x8QE1h+RAtQ2g+3KPsP6Fk/tdz6zJQUv5c7fTrJxXV3GHOo+Ry5p/PsogU4U+iUZg0rj6hS+E4xi+mnwwlDCWQ=="],
"@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.56.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-6G+WMZvwJpMvY7my+/SHEjb7BTk/PFbePqLpmVmUJRIsJMy/UlyYqjpuh0RCgYYkPLcnXm1rUM04kbTk8yS1Yg=="],
"@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.56.0", "", { "os": "linux", "cpu": "arm" }, "sha512-YYHBsk/sl7fYwQOok+6W5lBPeUEvisznV/HZD2IfZmF3Bns6cPC3Z0vCtSEOaAWTjYWN3jVsdu55jMxKlsdlhg=="],
"@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.56.0", "", { "os": "linux", "cpu": "arm" }, "sha512-+AZK8rOUr78y8WT6XkDb04IbMRqauNV+vgT6f8ZLOH8wnpQ9i7Nol0XLxAu+Cq7Sb+J9wC0j6Km5hG8rj47/yQ=="],
"@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.56.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-urse2SnugwJRojUkGSSeH2LPMaje5Q50yQtvtL9HFckiyeqXzoFwOAZqD5TR29R2lq7UHidfFDM9EGcchcbb8A=="],
"@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.56.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-rkTZkBfJ4TYLjansjSzL6mgZOdN5IvUnSq3oNJSLwBcNvy3dlgQtpHPrRxrCEbbcp7oQ6If0tkNaqfOsphYZ9g=="],
"@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.56.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-uqL1kMH3u69/e1CH2EJhP3CP28jw2ExLsku4o8RVAZ7fySo9zOyI2fy9pVlTAp4voBLVgzndXi3SgtdyCTa2aA=="],
"@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.56.0", "", { "os": "linux", "cpu": "none" }, "sha512-j0CcMBOgV6KsRaBdsebIeiy7hCjEvq2KdEsiULf2LZqAq0v1M1lWjelhCV57LxsqaIGChXFuFJ0RiFrSRHPhSg=="],
"@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.56.0", "", { "os": "linux", "cpu": "none" }, "sha512-7VDOiL8cDG3DQ/CY3yKjbV1c4YPvc4vH8qW09Vv+5ukq3l/Kcyr6XGCd5NvxUmxqDb2vjMpM+eW/4JrEEsUetA=="],
"@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.56.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-JGRpX0M+ikD3WpwJ7vKcHKV6Kg0dT52BW2Eu2BupXotYeqGXBrbY+QPkAyKO6MNgKozyTNaRh3r7g+VWgyAQYQ=="],
"@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.56.0", "", { "os": "linux", "cpu": "x64" }, "sha512-dNaICPvtmuxFP/VbqdofrLqdS3bM/AKJN3LMJD52si44ea7Be1cBk6NpfIahaysG9Uo+L98QKddU9CD5L8UHnQ=="],
"@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.56.0", "", { "os": "linux", "cpu": "x64" }, "sha512-pF1vOtM+GuXmbklM1hV8WMsn6tCNPvkUzklj/Ej98JhlanbmA2RB1BILgOpwSuCTRTIYx2MXssmEyQQ90QF5aA=="],
"@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.56.0", "", { "os": "none", "cpu": "arm64" }, "sha512-bp8NQ4RE6fDIFLa4bdBiOA+TAvkNkg+rslR+AvvjlLTYXLy9/uKAYLQudaQouWihLD/hgkrXIKKzXi5IXOewwg=="],
"@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.56.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-PxT4OJDfMOQBzo3OlzFb9gkoSD+n8qSBxyVq2wQSZIHFQYGEqIRTo9M0ZStvZm5fdhMqaVYpOnJvH2hUMEDk/g=="],
"@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.56.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-PTRy6sIEPqy2x8PTP1baBNReN/BNEFmde0L+mYeHmjXE1Vlcc9+I5nsqENsB2yAm5wLkzPoTNCMY/7AnabT4/A=="],
"@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.56.0", "", { "os": "win32", "cpu": "x64" }, "sha512-ZHa0clocjLmIDr+1LwoWtxRcoYniAvERotvwKUYKhH41NVfl0Y4LNbyQkwMZzwDvKklKGvGZ5+DAG58/Ik47tQ=="],
"@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="],
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
@@ -276,13 +335,13 @@
"@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="],
"@scalar/helpers": ["@scalar/helpers@0.4.1", "", {}, "sha512-XfyYqSUA597wfzS8lmMY1xvYv4si2WytuoBWj7JDpx3E/lVq7YEsticXF/Q30ttFVRBfgQErg8MQI6b6IkZr2Q=="],
"@scalar/helpers": ["@scalar/helpers@0.4.2", "", {}, "sha512-IrgrGVSahCfYDNWITazz4Q1BOndp5eEzlimRkfxiYn++KqeWyLfALyym1omqcdKGYtiSx1KIbKaUJL9vkjaN7w=="],
"@scalar/json-magic": ["@scalar/json-magic@0.12.3", "", { "dependencies": { "@scalar/helpers": "0.4.1", "pathe": "^2.0.3", "yaml": "^2.8.0" } }, "sha512-SX4LbdCk/0JLP4t4oz1nzqfMzHsG9gY1jSgKB7Q2BsQHJ4V1zapQ+aFrMmbjaCrz8hueGG/3UCtgyWBNuGmT9g=="],
"@scalar/json-magic": ["@scalar/json-magic@0.12.4", "", { "dependencies": { "@scalar/helpers": "0.4.2", "pathe": "^2.0.3", "yaml": "^2.8.0" } }, "sha512-ZsJM+y9SOVjRL+g9Tr/jFEodcYPfHSt5ZRrpbeGDK71yTvSZw41A3UJ7Bg+RXM8JJWRYcJkYVkzrmbb/ig7qlA=="],
"@scalar/openapi-types": ["@scalar/openapi-types@0.6.0", "", { "dependencies": { "zod": "^4.3.5" } }, "sha512-OeaCrQmXGqO+ZRGgo/mAA8X1nTsdNWW2BxQjyDWTCSD1Qv5Cm58QvKzINKvJPFWrS0LXebRF6OXN+2H2hu9xpA=="],
"@scalar/openapi-types": ["@scalar/openapi-types@0.6.1", "", { "dependencies": { "zod": "^4.3.5" } }, "sha512-P1RvyTFN0vRSL136OqWjlZfSFjY9JoJfuD6LM1mIjoocfwmqX3WuzsFEFX6hAeeDlTh6gjbiy+OdhSee8GFfSA=="],
"@scalar/openapi-upgrader": ["@scalar/openapi-upgrader@0.2.0", "", { "dependencies": { "@scalar/openapi-types": "0.6.0" } }, "sha512-T8XKPaYBrxn+8THq8HgXjqpnqp71YZsPCamNYIYJrIM5pXSbBLyLoAh6uie7YfhcNvdQWc/KqahHQebEIcmeqA=="],
"@scalar/openapi-upgrader": ["@scalar/openapi-upgrader@0.2.1", "", { "dependencies": { "@scalar/openapi-types": "0.6.1" } }, "sha512-7KO1HZ5Uau7FVznu7lLRmoD9UwJIt73rAXVCgNx4hPe3dzdiCwxq+nnc5Z7xFY2BVlAklGuacgZhoZFJt8kbJw=="],
"@shikijs/core": ["@shikijs/core@4.0.2", "", { "dependencies": { "@shikijs/primitive": "4.0.2", "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-hxT0YF4ExEqB8G/qFdtJvpmHXBYJ2lWW7qTHDarVkIudPFE6iCIrqdgWxGn5s+ppkGXI0aEGlibI0PAyzP3zlw=="],
@@ -308,35 +367,35 @@
"@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
"@tailwindcss/node": ["@tailwindcss/node@4.2.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.31.1", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.1" } }, "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg=="],
"@tailwindcss/node": ["@tailwindcss/node@4.2.2", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.2" } }, "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA=="],
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.1", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.1", "@tailwindcss/oxide-darwin-arm64": "4.2.1", "@tailwindcss/oxide-darwin-x64": "4.2.1", "@tailwindcss/oxide-freebsd-x64": "4.2.1", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", "@tailwindcss/oxide-linux-x64-musl": "4.2.1", "@tailwindcss/oxide-wasm32-wasi": "4.2.1", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" } }, "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw=="],
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.2", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.2", "@tailwindcss/oxide-darwin-arm64": "4.2.2", "@tailwindcss/oxide-darwin-x64": "4.2.2", "@tailwindcss/oxide-freebsd-x64": "4.2.2", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2", "@tailwindcss/oxide-linux-arm64-musl": "4.2.2", "@tailwindcss/oxide-linux-x64-gnu": "4.2.2", "@tailwindcss/oxide-linux-x64-musl": "4.2.2", "@tailwindcss/oxide-wasm32-wasi": "4.2.2", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2", "@tailwindcss/oxide-win32-x64-msvc": "4.2.2" } }, "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg=="],
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.2.1", "", { "os": "android", "cpu": "arm64" }, "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg=="],
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.2.2", "", { "os": "android", "cpu": "arm64" }, "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg=="],
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.2.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw=="],
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.2.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg=="],
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.2.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw=="],
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.2.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw=="],
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.2.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA=="],
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.2.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ=="],
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1", "", { "os": "linux", "cpu": "arm" }, "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw=="],
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2", "", { "os": "linux", "cpu": "arm" }, "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ=="],
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ=="],
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.2.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw=="],
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ=="],
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.2.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag=="],
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g=="],
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.2.2", "", { "os": "linux", "cpu": "x64" }, "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg=="],
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g=="],
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.2.2", "", { "os": "linux", "cpu": "x64" }, "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ=="],
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.2.1", "", { "dependencies": { "@emnapi/core": "^1.8.1", "@emnapi/runtime": "^1.8.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.1", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q=="],
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.2.2", "", { "dependencies": { "@emnapi/core": "^1.8.1", "@emnapi/runtime": "^1.8.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.1", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q=="],
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.2.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA=="],
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.2.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ=="],
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.1", "", { "os": "win32", "cpu": "x64" }, "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ=="],
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.2", "", { "os": "win32", "cpu": "x64" }, "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA=="],
"@tailwindcss/postcss": ["@tailwindcss/postcss@4.2.1", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.2.1", "@tailwindcss/oxide": "4.2.1", "postcss": "^8.5.6", "tailwindcss": "4.2.1" } }, "sha512-OEwGIBnXnj7zJeonOh6ZG9woofIjGrd2BORfvE5p9USYKDCZoQmfqLcfNiRWoJlRWLdNPn2IgVZuWAOM4iTYMw=="],
"@tailwindcss/postcss": ["@tailwindcss/postcss@4.2.2", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.2.2", "@tailwindcss/oxide": "4.2.2", "postcss": "^8.5.6", "tailwindcss": "4.2.2" } }, "sha512-n4goKQbW8RVXIbNKRB/45LzyUqN451deQK0nzIeauVEqjlI49slUlgKYJM2QyUzap/PcpnS7kzSUmPb1sCRvYQ=="],
"@takumi-rs/core": ["@takumi-rs/core@0.73.1", "", { "dependencies": { "@takumi-rs/helpers": "0.73.1" }, "optionalDependencies": { "@takumi-rs/core-darwin-arm64": "0.73.1", "@takumi-rs/core-darwin-x64": "0.73.1", "@takumi-rs/core-linux-arm64-gnu": "0.73.1", "@takumi-rs/core-linux-arm64-musl": "0.73.1", "@takumi-rs/core-linux-x64-gnu": "0.73.1", "@takumi-rs/core-linux-x64-musl": "0.73.1", "@takumi-rs/core-win32-arm64-msvc": "0.73.1", "@takumi-rs/core-win32-x64-msvc": "0.73.1" } }, "sha512-+8GNu0O9lDNBjXx2uM3/15zIyTCt+vI3Ltmkudacybnvl6Kx2TMokRWD9/bymg19ZVcGbfOC1FsFIGQdVePCvQ=="],
@@ -386,7 +445,7 @@
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
"@typescript-eslint/types": ["@typescript-eslint/types@8.57.0", "", {}, "sha512-dTLI8PEXhjUC7B9Kre+u0XznO696BhXcTlOn0/6kf1fHaQW8+VjJAVHJ3eTI14ZapTxdkOmc80HblPQLaEeJdg=="],
"@typescript-eslint/types": ["@typescript-eslint/types@8.57.1", "", {}, "sha512-S29BOBPJSFUiblEl6RzPPjJt6w25A6XsBqRVDt53tA/tlL8q7ceQNZHTjPeONt/3S7KRI4quk+yP9jK2WjBiPQ=="],
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
@@ -406,9 +465,9 @@
"bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.7", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-1ghYO3HnxGec0TCGBXiDLVns4eCSx4zJpxnHrlqFQajmhfKMQBzUGDdkMK7fUW7PTHTeLf+j87aTuKuuwWzMGw=="],
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.8", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-PCLz/LXGBsNTErbtB6i5u4eLpHeMfi93aUv5duMmj6caNu6IphS4q6UevDnL36sZQv9lrP11dbPKGMaXPwMKfQ=="],
"caniuse-lite": ["caniuse-lite@1.0.30001778", "", {}, "sha512-PN7uxFL+ExFJO61aVmP1aIEG4i9whQd4eoSCebav62UwDyp5OHh06zN4jqKSMePVgxHifCw1QJxdRkA1Pisekg=="],
"caniuse-lite": ["caniuse-lite@1.0.30001780", "", {}, "sha512-llngX0E7nQci5BPJDqoZSbuZ5Bcs9F5db7EtgfwBerX9XGtkkiO4NwfDDIRzHTTwcYC8vC7bmeUEPGrKlR/TkQ=="],
"ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
@@ -452,7 +511,7 @@
"devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
"enhanced-resolve": ["enhanced-resolve@5.20.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ=="],
"enhanced-resolve": ["enhanced-resolve@5.20.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA=="],
"entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
@@ -488,15 +547,15 @@
"fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
"fast-xml-builder": ["fast-xml-builder@1.1.3", "", { "dependencies": { "path-expression-matcher": "^1.1.3" } }, "sha512-1o60KoFw2+LWKQu3IdcfcFlGTW4dpqEWmjhYec6H82AYZU2TVBXep6tMl8Z1Y+wM+ZrzCwe3BZ9Vyd9N2rIvmg=="],
"fast-xml-builder": ["fast-xml-builder@1.1.4", "", { "dependencies": { "path-expression-matcher": "^1.1.3" } }, "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg=="],
"fast-xml-parser": ["fast-xml-parser@5.5.5", "", { "dependencies": { "fast-xml-builder": "^1.1.3", "path-expression-matcher": "^1.1.3", "strnum": "^2.1.2" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-NLY+V5NNbdmiEszx9n14mZBseJTC50bRq1VHsaxOmR72JDuZt+5J1Co+dC/4JPnyq+WrIHNM69r0sqf7BMb3Mg=="],
"fast-xml-parser": ["fast-xml-parser@5.5.6", "", { "dependencies": { "fast-xml-builder": "^1.1.4", "path-expression-matcher": "^1.1.3", "strnum": "^2.1.2" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-3+fdZyBRVg29n4rXP0joHthhcHdPUHaIC16cuyyd1iLsuaO6Vea36MPrxgAzbZna8lhvZeRL8Bc9GP56/J9xEw=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"foreach": ["foreach@2.0.6", "", {}, "sha512-k6GAGDyqLe9JaebCsFCoudPPWfihKu8pylYXRlqP1J7ms39iPoTtk2fviNglIeQEwdh0bQeKJ01ZPyuyQvKzwg=="],
"framer-motion": ["framer-motion@12.36.0", "", { "dependencies": { "motion-dom": "^12.36.0", "motion-utils": "^12.36.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-4PqYHAT7gev0ke0wos+PyrcFxI0HScjm3asgU8nSYa8YzJFuwgIvdj3/s3ZaxLq0bUSboIn19A2WS/MHwLCvfw=="],
"framer-motion": ["framer-motion@12.38.0", "", { "dependencies": { "motion-dom": "^12.38.0", "motion-utils": "^12.36.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g=="],
"fumadocs-core": ["fumadocs-core@16.6.17", "", { "dependencies": { "@formatjs/intl-localematcher": "^0.8.1", "@orama/orama": "^3.1.18", "@shikijs/rehype": "^4.0.2", "@shikijs/transformers": "^4.0.2", "estree-util-value-to-estree": "^3.5.0", "github-slugger": "^2.0.0", "hast-util-to-estree": "^3.1.3", "hast-util-to-jsx-runtime": "^2.3.6", "image-size": "^2.0.2", "mdast-util-mdx": "^3.0.0", "mdast-util-to-markdown": "^2.1.2", "negotiator": "^1.0.0", "npm-to-yarn": "^3.0.1", "path-to-regexp": "^8.3.0", "remark": "^15.0.1", "remark-gfm": "^4.0.1", "remark-rehype": "^11.1.2", "scroll-into-view-if-needed": "^3.1.0", "shiki": "^4.0.2", "tinyglobby": "^0.2.15", "unified": "^11.0.5", "unist-util-visit": "^5.1.0", "vfile": "^6.0.3" }, "peerDependencies": { "@mdx-js/mdx": "*", "@mixedbread/sdk": "^0.46.0", "@orama/core": "1.x.x", "@oramacloud/client": "2.x.x", "@tanstack/react-router": "1.x.x", "@types/estree-jsx": "*", "@types/hast": "*", "@types/mdast": "*", "@types/react": "*", "algoliasearch": "5.x.x", "flexsearch": "*", "lucide-react": "*", "next": "16.x.x", "react": "^19.2.0", "react-dom": "^19.2.0", "react-router": "7.x.x", "waku": "^0.26.0 || ^0.27.0 || ^1.0.0", "zod": "4.x.x" }, "optionalPeers": ["@mdx-js/mdx", "@mixedbread/sdk", "@orama/core", "@oramacloud/client", "@tanstack/react-router", "@types/estree-jsx", "@types/hast", "@types/mdast", "@types/react", "algoliasearch", "flexsearch", "lucide-react", "next", "react", "react-dom", "react-router", "waku", "zod"] }, "sha512-ssHz9a7+ZZSkHjB4/sfHq9rO2fPW8jtw2fPeDVzkPJd34DqOPbxuaP0TQ6CEs1Pei99Fky9CzE8ENS3H8WFxnQ=="],
@@ -556,29 +615,29 @@
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"lightningcss": ["lightningcss@1.31.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.31.1", "lightningcss-darwin-arm64": "1.31.1", "lightningcss-darwin-x64": "1.31.1", "lightningcss-freebsd-x64": "1.31.1", "lightningcss-linux-arm-gnueabihf": "1.31.1", "lightningcss-linux-arm64-gnu": "1.31.1", "lightningcss-linux-arm64-musl": "1.31.1", "lightningcss-linux-x64-gnu": "1.31.1", "lightningcss-linux-x64-musl": "1.31.1", "lightningcss-win32-arm64-msvc": "1.31.1", "lightningcss-win32-x64-msvc": "1.31.1" } }, "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ=="],
"lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.31.1", "", { "os": "android", "cpu": "arm64" }, "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg=="],
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.31.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg=="],
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.31.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA=="],
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.31.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A=="],
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="],
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.31.1", "", { "os": "linux", "cpu": "arm" }, "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g=="],
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="],
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg=="],
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="],
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg=="],
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="],
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA=="],
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="],
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA=="],
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="],
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.31.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w=="],
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="],
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.31.1", "", { "os": "win32", "cpu": "x64" }, "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw=="],
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
"longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
@@ -692,9 +751,9 @@
"micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="],
"motion": ["motion@12.36.0", "", { "dependencies": { "framer-motion": "^12.36.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-5BMQuktYUX8aEByKWYx5tR4X3G08H2OMgp46wTxZ4o7CDDstyy4A0fe9RLNMjZiwvntCWGDvs16sC87/emz4Yw=="],
"motion": ["motion@12.38.0", "", { "dependencies": { "framer-motion": "^12.38.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-uYfXzeHlgThchzwz5Te47dlv5JOUC7OB4rjJ/7XTUgtBZD8CchMN8qEJ4ZVsUmTyYA44zjV0fBwsiktRuFnn+w=="],
"motion-dom": ["motion-dom@12.36.0", "", { "dependencies": { "motion-utils": "^12.36.0" } }, "sha512-Ep1pq8P88rGJ75om8lTCA13zqd7ywPGwCqwuWwin6BKc0hMLkVfcS6qKlRqEo2+t0DwoUcgGJfXwaiFn4AOcQA=="],
"motion-dom": ["motion-dom@12.38.0", "", { "dependencies": { "motion-utils": "^12.36.0" } }, "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA=="],
"motion-utils": ["motion-utils@12.36.0", "", {}, "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg=="],
@@ -704,7 +763,7 @@
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
"next": ["next@16.1.6", "", { "dependencies": { "@next/env": "16.1.6", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.1.6", "@next/swc-darwin-x64": "16.1.6", "@next/swc-linux-arm64-gnu": "16.1.6", "@next/swc-linux-arm64-musl": "16.1.6", "@next/swc-linux-x64-gnu": "16.1.6", "@next/swc-linux-x64-musl": "16.1.6", "@next/swc-win32-arm64-msvc": "16.1.6", "@next/swc-win32-x64-msvc": "16.1.6", "sharp": "^0.34.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw=="],
"next": ["next@16.1.7", "", { "dependencies": { "@next/env": "16.1.7", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.1.7", "@next/swc-darwin-x64": "16.1.7", "@next/swc-linux-arm64-gnu": "16.1.7", "@next/swc-linux-arm64-musl": "16.1.7", "@next/swc-linux-x64-gnu": "16.1.7", "@next/swc-linux-x64-musl": "16.1.7", "@next/swc-win32-arm64-msvc": "16.1.7", "@next/swc-win32-x64-msvc": "16.1.7", "sharp": "^0.34.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-WM0L7WrSvKwoLegLYr6V+mz+RIofqQgVAfHhMp9a88ms0cFX8iX9ew+snpWlSBwpkURJOUdvCEt3uLl3NNzvWg=="],
"next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="],
@@ -712,10 +771,14 @@
"oniguruma-parser": ["oniguruma-parser@0.12.1", "", {}, "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w=="],
"oniguruma-to-es": ["oniguruma-to-es@4.3.4", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, "sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA=="],
"oniguruma-to-es": ["oniguruma-to-es@4.3.5", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.1.0", "regex-recursion": "^6.0.2" } }, "sha512-Zjygswjpsewa0NLTsiizVuMQZbp0MDyM6lIt66OxsF21npUDlzpHi1Mgb/qhQdkb+dWFTzJmFbEWdvZgRho8eQ=="],
"openapi-sampler": ["openapi-sampler@1.7.2", "", { "dependencies": { "@types/json-schema": "^7.0.7", "fast-xml-parser": "^5.5.1", "json-pointer": "0.6.2" } }, "sha512-OKytvqB5XIaTgA9xtw8W8UTar+uymW2xPVpFN0NihMtuHPdPTGxBEhGnfFnJW5g/gOSIvkP+H0Xh3XhVI9/n7g=="],
"oxfmt": ["oxfmt@0.41.0", "", { "dependencies": { "tinypool": "2.1.0" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.41.0", "@oxfmt/binding-android-arm64": "0.41.0", "@oxfmt/binding-darwin-arm64": "0.41.0", "@oxfmt/binding-darwin-x64": "0.41.0", "@oxfmt/binding-freebsd-x64": "0.41.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.41.0", "@oxfmt/binding-linux-arm-musleabihf": "0.41.0", "@oxfmt/binding-linux-arm64-gnu": "0.41.0", "@oxfmt/binding-linux-arm64-musl": "0.41.0", "@oxfmt/binding-linux-ppc64-gnu": "0.41.0", "@oxfmt/binding-linux-riscv64-gnu": "0.41.0", "@oxfmt/binding-linux-riscv64-musl": "0.41.0", "@oxfmt/binding-linux-s390x-gnu": "0.41.0", "@oxfmt/binding-linux-x64-gnu": "0.41.0", "@oxfmt/binding-linux-x64-musl": "0.41.0", "@oxfmt/binding-openharmony-arm64": "0.41.0", "@oxfmt/binding-win32-arm64-msvc": "0.41.0", "@oxfmt/binding-win32-ia32-msvc": "0.41.0", "@oxfmt/binding-win32-x64-msvc": "0.41.0" }, "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-sKLdJZdQ3bw6x9qKiT7+eID4MNEXlDHf5ZacfIircrq6Qwjk0L6t2/JQlZZrVHTXJawK3KaMuBoJnEJPcqCEdg=="],
"oxlint": ["oxlint@1.56.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.56.0", "@oxlint/binding-android-arm64": "1.56.0", "@oxlint/binding-darwin-arm64": "1.56.0", "@oxlint/binding-darwin-x64": "1.56.0", "@oxlint/binding-freebsd-x64": "1.56.0", "@oxlint/binding-linux-arm-gnueabihf": "1.56.0", "@oxlint/binding-linux-arm-musleabihf": "1.56.0", "@oxlint/binding-linux-arm64-gnu": "1.56.0", "@oxlint/binding-linux-arm64-musl": "1.56.0", "@oxlint/binding-linux-ppc64-gnu": "1.56.0", "@oxlint/binding-linux-riscv64-gnu": "1.56.0", "@oxlint/binding-linux-riscv64-musl": "1.56.0", "@oxlint/binding-linux-s390x-gnu": "1.56.0", "@oxlint/binding-linux-x64-gnu": "1.56.0", "@oxlint/binding-linux-x64-musl": "1.56.0", "@oxlint/binding-openharmony-arm64": "1.56.0", "@oxlint/binding-win32-arm64-msvc": "1.56.0", "@oxlint/binding-win32-ia32-msvc": "1.56.0", "@oxlint/binding-win32-x64-msvc": "1.56.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.15.0" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-Q+5Mj5PVaH/R6/fhMMFzw4dT+KPB+kQW4kaL8FOIq7tfhlnEVp6+3lcWqFruuTNlUo9srZUW3qH7Id4pskeR6g=="],
"parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="],
"parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
@@ -784,7 +847,7 @@
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
"sax": ["sax@1.5.0", "", {}, "sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA=="],
"sax": ["sax@1.6.0", "", {}, "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA=="],
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
@@ -814,7 +877,7 @@
"tailwind-merge": ["tailwind-merge@3.5.0", "", {}, "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A=="],
"tailwindcss": ["tailwindcss@4.2.1", "", {}, "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw=="],
"tailwindcss": ["tailwindcss@4.2.2", "", {}, "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q=="],
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
@@ -822,6 +885,8 @@
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
"tinypool": ["tinypool@2.1.0", "", {}, "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw=="],
"trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
"trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
+10 -8
View File
@@ -17,7 +17,8 @@ There is currently no way to transfer the admin role to another user.
Toggle user registration open or closed in **Settings → Security → Open Registration**.
<Callout type="info">
Registration is always open when no users exist — this is how the initial admin account is created. After the first account is registered, it closes automatically.
Registration is always open when no users exist — this is how the initial admin account is
created. After the first account is registered, it closes automatically.
</Callout>
When registration is closed, new users cannot create accounts unless they authenticate via [OIDC/SSO](/docs/configuration#oidc--sso) with `OIDC_AUTO_REGISTER` enabled.
@@ -58,12 +59,12 @@ Click **Create backup** to generate an immediate database backup. Each backup is
Enable scheduled backups with the toggle and configure:
| Setting | Default | Options |
|---|---|---|
| Frequency | Every day | `6h`, `12h`, `1d`, `7d` |
| Time | 02:00 | Any `HH:MM` (UTC) |
| Day of week | Sunday | For weekly backups only |
| Retention | 7 | Max number of scheduled backups to keep |
| Setting | Default | Options |
| ----------- | --------- | --------------------------------------- |
| Frequency | Every day | `6h`, `12h`, `1d`, `7d` |
| Time | 02:00 | Any `HH:MM` (UTC) |
| Day of week | Sunday | For weekly backups only |
| Retention | 7 | Max number of scheduled backups to keep |
Old backups beyond the retention count are automatically pruned. Scheduled backups are named `sofa-scheduled-*.db`.
@@ -76,7 +77,8 @@ Upload a backup file or select one from the existing backup list to restore. Bef
3. After the database is replaced, **migrations run automatically** to bring older backups up to the current schema.
<Callout type="warn">
Restoring a backup replaces the entire database. All data since the backup was created will be lost. The pre-restore snapshot lets you undo the restore if something goes wrong.
Restoring a backup replaces the entire database. All data since the backup was created will be
lost. The pre-restore snapshot lets you undo the restore if something goes wrong.
</Callout>
## Background Jobs
+46 -46
View File
@@ -9,7 +9,7 @@ icon: Settings
These three environment variables must be set for Sofa to start:
| Variable | Description | Example |
|---|---|---|
| --- | --- | --- |
| `TMDB_API_READ_ACCESS_TOKEN` | TMDB API authentication token | [Get one here](https://www.themoviedb.org/settings/api) |
| `BETTER_AUTH_SECRET` | Session encryption secret (min 32 chars) | `openssl rand -base64 32` |
| `BETTER_AUTH_URL` | Public URL of your instance | `https://sofa.example.com` |
@@ -18,11 +18,11 @@ These three environment variables must be set for Sofa to start:
## Data & Storage
| Variable | Default | Description |
|---|---|---|
| `DATA_DIR` | `/data` (Docker) or `./data` (dev) | Root directory for all persistent data |
| `DATABASE_URL` | `${DATA_DIR}/sqlite.db` | SQLite database file path |
| `CACHE_DIR` | `${DATA_DIR}/images` | Image cache directory |
| Variable | Default | Description |
| -------------- | ---------------------------------- | -------------------------------------- |
| `DATA_DIR` | `/data` (Docker) or `./data` (dev) | Root directory for all persistent data |
| `DATABASE_URL` | `${DATA_DIR}/sqlite.db` | SQLite database file path |
| `CACHE_DIR` | `${DATA_DIR}/images` | Image cache directory |
`DATABASE_URL` and `CACHE_DIR` are derived from `DATA_DIR` by default but can be overridden independently.
@@ -30,9 +30,9 @@ The database uses SQLite with WAL (Write-Ahead Logging) mode for concurrent read
## Image Caching
| Variable | Default | Description |
|---|---|---|
| `IMAGE_CACHE_ENABLED` | `true` | Cache TMDB images locally |
| Variable | Default | Description |
| --------------------- | ------- | ------------------------- |
| `IMAGE_CACHE_ENABLED` | `true` | Cache TMDB images locally |
When enabled, Sofa downloads poster, backdrop, and other images from TMDB and serves them from disk. This improves load times and reduces external requests but uses disk space.
@@ -51,9 +51,9 @@ Set `IMAGE_CACHE_ENABLED=false` to serve images directly from TMDB's CDN instead
## Logging
| Variable | Default | Options |
|---|---|---|
| `LOG_LEVEL` | `info` | `error`, `warn`, `info`, `debug`, `trace` |
| Variable | Default | Options |
| ----------- | ------- | ----------------------------------------- |
| `LOG_LEVEL` | `info` | `error`, `warn`, `info`, `debug`, `trace` |
Sofa uses structured JSON logging via [Pino](https://getpino.io/). Set to `debug` for troubleshooting.
@@ -61,13 +61,13 @@ Sofa uses structured JSON logging via [Pino](https://getpino.io/). Set to `debug
Sofa supports any OpenID Connect provider (Authentik, Keycloak, Authelia, Google Workspace, etc.). OIDC is enabled when all three of `OIDC_CLIENT_ID`, `OIDC_CLIENT_SECRET`, and `OIDC_ISSUER_URL` are set.
| Variable | Default | Description |
|---|---|---|
| `OIDC_CLIENT_ID` | — | Client ID from your identity provider |
| `OIDC_CLIENT_SECRET` | — | Client secret from your identity provider |
| `OIDC_ISSUER_URL` | — | Issuer discovery URL |
| `OIDC_PROVIDER_NAME` | `SSO` | Label shown on the login button |
| `OIDC_AUTO_REGISTER` | `true` | Auto-create accounts on first OIDC login |
| Variable | Default | Description |
| ------------------------ | ------- | -------------------------------------------- |
| `OIDC_CLIENT_ID` | — | Client ID from your identity provider |
| `OIDC_CLIENT_SECRET` | — | Client secret from your identity provider |
| `OIDC_ISSUER_URL` | — | Issuer discovery URL |
| `OIDC_PROVIDER_NAME` | `SSO` | Label shown on the login button |
| `OIDC_AUTO_REGISTER` | `true` | Auto-create accounts on first OIDC login |
| `DISABLE_PASSWORD_LOGIN` | `false` | Hide the email/password form (requires OIDC) |
### Callback URL
@@ -120,7 +120,7 @@ When OIDC is enabled, account linking is active by default. If a user logs in vi
`OIDC_AUTO_REGISTER` and `DISABLE_PASSWORD_LOGIN` interact to control how users can access your instance:
| `OIDC_AUTO_REGISTER` | `DISABLE_PASSWORD_LOGIN` | Behavior |
|---|---|---|
| --- | --- | --- |
| `true` (default) | `false` (default) | Both login methods available. New OIDC users get accounts created automatically. |
| `true` | `true` | OIDC only. Accounts are auto-created on first login. Password form is hidden. |
| `false` | `false` | Both login methods available. OIDC users must have a pre-existing account with a matching email. |
@@ -128,19 +128,19 @@ When OIDC is enabled, account linking is active by default. If a user logs in vi
## TMDB Overrides
| Variable | Default | Description |
|---|---|---|
| `TMDB_API_BASE_URL` | `https://api.themoviedb.org/3` | Override TMDB API endpoint |
| `TMDB_IMAGE_BASE_URL` | `https://image.tmdb.org/t/p` | Override TMDB image CDN |
| Variable | Default | Description |
| --------------------- | ------------------------------ | -------------------------- |
| `TMDB_API_BASE_URL` | `https://api.themoviedb.org/3` | Override TMDB API endpoint |
| `TMDB_IMAGE_BASE_URL` | `https://image.tmdb.org/t/p` | Override TMDB image CDN |
These are only needed if you're proxying TMDB requests or using an alternative endpoint.
## Network
| Variable | Default | Description |
|---|---|---|
| `CORS_ORIGIN` | `http://localhost:3000` | Allowed CORS origin |
| `PORT` | `3000` | Server listening port |
| Variable | Default | Description |
| ------------- | ----------------------- | --------------------- |
| `CORS_ORIGIN` | `http://localhost:3000` | Allowed CORS origin |
| `PORT` | `3000` | Server listening port |
Set `CORS_ORIGIN` to your public URL when using a reverse proxy.
@@ -148,8 +148,8 @@ Set `CORS_ORIGIN` to your public URL when using a reverse proxy.
Sofa checks for new versions every 6 hours by contacting the public API.
| Variable | Default | Description |
|---|---|---|
| Variable | Default | Description |
| ---------------- | ------------------------------- | --------------------------- |
| `PUBLIC_API_URL` | `https://public-api.sofa.watch` | Endpoint for version checks |
Update checks can be disabled in **Settings → Admin → Update Checks**. When a new version is available, a notification appears in the admin settings.
@@ -162,12 +162,12 @@ Sofa includes opt-in anonymous telemetry. It is **disabled by default**. See the
Automatic backups can be enabled in **Settings → Admin → Backups**. Configuration options:
| Setting | Default | Options |
|---|---|---|
| Frequency | Every day | `6h`, `12h`, `1d`, `7d` |
| Time | 02:00 | Any `HH:MM` |
| Day of week | Sunday | (for weekly backups) |
| Retention | 7 | Number of scheduled backups to keep |
| Setting | Default | Options |
| ----------- | --------- | ----------------------------------- |
| Frequency | Every day | `6h`, `12h`, `1d`, `7d` |
| Time | 02:00 | Any `HH:MM` |
| Day of week | Sunday | (for weekly backups) |
| Retention | 7 | Number of scheduled backups to keep |
Backups are SQLite database dumps stored in the `backups/` directory inside `DATA_DIR`. Old backups beyond the retention count are automatically deleted.
@@ -175,15 +175,15 @@ Backups are SQLite database dumps stored in the `backups/` directory inside `DAT
Sofa runs several background jobs automatically:
| Job | Schedule | Description |
|---|---|---|
| Library refresh | Daily at 3:00 AM | Refreshes metadata for tracked titles |
| Streaming availability | Every 6 hours | Updates where titles are available to stream |
| Recommendations | Every 12 hours | Refreshes personalized recommendations |
| New episodes | Every 12 hours | Checks for new episodes of tracked TV shows |
| Image caching | Twice daily | Caches images for tracked titles |
| Credits refresh | Daily at 2:00 AM | Updates cast and crew information |
| Update check | Every 6 hours | Checks for new Sofa versions |
| Telemetry | Daily at 12:30 AM | Sends anonymized usage data (if enabled) |
| Job | Schedule | Description |
| ---------------------- | ----------------- | -------------------------------------------- |
| Library refresh | Daily at 3:00 AM | Refreshes metadata for tracked titles |
| Streaming availability | Every 6 hours | Updates where titles are available to stream |
| Recommendations | Every 12 hours | Refreshes personalized recommendations |
| New episodes | Every 12 hours | Checks for new episodes of tracked TV shows |
| Image caching | Twice daily | Caches images for tracked titles |
| Credits refresh | Daily at 2:00 AM | Updates cast and crew information |
| Update check | Every 6 hours | Checks for new Sofa versions |
| Telemetry | Daily at 12:30 AM | Sends anonymized usage data (if enabled) |
All schedules are in UTC. TMDB API calls are rate-limited with a 300ms delay between requests per title to stay within API limits.
+10 -10
View File
@@ -7,11 +7,11 @@ Sofa can import your existing watch history, watchlist, and ratings from other t
## Supported Services
| Service | OAuth Connect | File Upload | Data Imported |
|---|---|---|---|
| **Trakt** | Yes | JSON export | Watches, watchlist, ratings |
| **Simkl** | Yes | JSON export | Watches, watchlist, ratings, anime (as TV) |
| **Letterboxd** | No | ZIP export | Watches, watchlist, ratings (movies only) |
| Service | OAuth Connect | File Upload | Data Imported |
| -------------- | ------------- | ----------- | ------------------------------------------ |
| **Trakt** | Yes | JSON export | Watches, watchlist, ratings |
| **Simkl** | Yes | JSON export | Watches, watchlist, ratings, anime (as TV) |
| **Letterboxd** | No | ZIP export | Watches, watchlist, ratings (movies only) |
## Importing via OAuth (Trakt & Simkl)
@@ -71,11 +71,11 @@ Imports are safe to re-run. Sofa checks for existing data before writing:
Different services use different rating scales. Sofa converts to its 15 star scale:
| Source | Scale | Conversion |
|---|---|---|
| Trakt | 110 | `round(rating / 2)`, clamped to 15 |
| Simkl | 110 | `round(rating / 2)`, clamped to 15 |
| Letterboxd | 0.55 (half-stars) | `round(rating)`, clamped to 15 |
| Source | Scale | Conversion |
| ---------- | ------------------ | ----------------------------------- |
| Trakt | 110 | `round(rating / 2)`, clamped to 15 |
| Simkl | 110 | `round(rating / 2)`, clamped to 15 |
| Letterboxd | 0.55 (half-stars) | `round(rating)`, clamped to 15 |
### Timestamps
+1 -1
View File
@@ -4,7 +4,7 @@ description: Connect Sofa to your media stack.
icon: Puzzle
---
Sofa integrates with popular media servers and the *arr stack to automate your tracking workflow.
Sofa integrates with popular media servers and the \*arr stack to automate your tracking workflow.
## Webhook Integrations
+2 -1
View File
@@ -7,7 +7,8 @@ icon: Smartphone
Sofa has a native mobile app for iOS and Android built with [Expo](https://expo.dev/) and React Native. It connects to your self-hosted Sofa server and provides the same core features as the web app.
<Callout type="info">
The mobile app is not currently available on the App Store or Google Play. You'll need to build it from source.
The mobile app is not currently available on the App Store or Google Play. You'll need to build it
from source.
</Callout>
## Features
+6 -6
View File
@@ -10,11 +10,11 @@ Sofa includes optional, privacy-focused telemetry to help understand how the pro
Sofa has two separate telemetry systems depending on the platform:
| | Server | Mobile App | Web App |
|---|---|---|---|
| **System** | Custom reporting | PostHog | None |
| **Default** | Disabled | Disabled | N/A |
| **Opt-in** | Admin toggle | User toggle + ATT | N/A |
| | Server | Mobile App | Web App |
| ----------- | ---------------- | ----------------- | ------- |
| **System** | Custom reporting | PostHog | None |
| **Default** | Disabled | Disabled | N/A |
| **Opt-in** | Admin toggle | User toggle + ATT | N/A |
The web app has **zero analytics** — no tracking scripts, no third-party services.
@@ -48,7 +48,7 @@ A single JSON payload is sent to `https://public-api.sofa.watch/v1/telemetry`:
```
| Field | Description |
|---|---|
| --- | --- |
| `instanceId` | A random UUID generated once per server instance. Not tied to any user or IP. |
| `version` | The Sofa version running on your server. |
| `arch` | OS and CPU architecture (e.g. `linux-x64`, `darwin-arm64`). |
+18 -16
View File
@@ -21,11 +21,11 @@ If your library is empty, the dashboard shows an invitation to start exploring.
Every title in your library has one of three statuses:
| Status | Meaning |
|---|---|
| **Watchlist** | You plan to watch this |
| Status | Meaning |
| --------------- | ------------------------------ |
| **Watchlist** | You plan to watch this |
| **In Progress** | You're currently watching this |
| **Completed** | You've finished watching this |
| **Completed** | You've finished watching this |
Set a status from any title's detail page by clicking the status button, or press <kbd>W</kbd> to cycle through statuses.
@@ -51,7 +51,8 @@ These same transitions apply when watches are logged automatically via [webhook
- **Catch up** — When you mark an episode as watched while earlier episodes are unwatched, a toast notification offers a "Catch up" button to batch-mark all prior episodes.
<Callout type="info">
Duplicate watches within 5 minutes are automatically ignored. This prevents double-counting when webhook integrations send the same event multiple times.
Duplicate watches within 5 minutes are automatically ignored. This prevents double-counting when
webhook integrations send the same event multiple times.
</Callout>
## Rating Titles
@@ -95,7 +96,8 @@ Title detail pages include a **Where to Watch** section showing which streaming
Click a provider's logo to open their page for that title in a new tab.
<Callout type="info">
Streaming availability data is currently limited to the **United States**. Provider information is refreshed automatically every 6 hours for titles in your library.
Streaming availability data is currently limited to the **United States**. Provider information is
refreshed automatically every 6 hours for titles in your library.
</Callout>
## Account Management
@@ -110,13 +112,13 @@ Manage your profile in **Settings**:
Press <kbd>?</kbd> anywhere in the app to see the full shortcut reference. Here's a summary:
| Scope | Keys | Action |
|---|---|---|
| Global | `/` | Open search |
| Global | `?` | Show keyboard shortcuts |
| Navigation | `g` then `h` | Go to dashboard |
| Navigation | `g` then `e` | Go to explore |
| Title page | `w` | Cycle watch status |
| Title page | `m` | Mark movie as watched |
| Title page | `Escape` | Go back |
| Title page | `1``5` | Rate 15 stars |
| Scope | Keys | Action |
| ---------- | ------------ | ----------------------- |
| Global | `/` | Open search |
| Global | `?` | Show keyboard shortcuts |
| Navigation | `g` then `h` | Go to dashboard |
| Navigation | `g` then `e` | Go to explore |
| Title page | `w` | Cycle watch status |
| Title page | `m` | Mark movie as watched |
| Title page | `Escape` | Go back |
| Title page | `1``5` | Rate 15 stars |
+10 -7
View File
@@ -6,8 +6,9 @@
"build": "next build",
"dev": "next dev",
"start": "next start",
"lint": "biome check",
"format": "biome format --write",
"lint": "oxlint",
"format": "oxfmt",
"format:check": "oxfmt --check",
"check-types": "fumadocs-mdx && next typegen && tsc --noEmit",
"generate:api-docs": "bun scripts/generate-api-docs.ts",
"postinstall": "fumadocs-mdx"
@@ -20,21 +21,23 @@
"fumadocs-openapi": "^10.3.18",
"fumadocs-ui": "^16.6.17",
"lucide-react": "^0.577.0",
"next": "16.1.6",
"next": "16.1.7",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"shiki": "^4.0.2",
"tailwind-merge": "^3.5.0"
},
"devDependencies": {
"@biomejs/biome": "^2.4.7",
"@tailwindcss/postcss": "^4.2.1",
"@tailwindcss/postcss": "^4.2.2",
"@types/mdx": "^2.0.13",
"@types/node": "^25.5.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"oxfmt": "0.41.0",
"oxlint": "1.56.0",
"postcss": "^8.5.8",
"tailwindcss": "^4.2.1",
"tailwindcss": "^4.2.2",
"typescript": "^5.9.3"
}
},
"packageManager": "bun@1.3.10"
}
+13 -2
View File
@@ -4190,13 +4190,24 @@
"type": "string",
"default": "TMDB API key is not configured"
},
"data": {}
"data": {
"type": "object",
"properties": {
"code": {
"const": "TMDB_NOT_CONFIGURED"
}
},
"required": [
"code"
]
}
},
"required": [
"defined",
"code",
"status",
"message"
"message",
"data"
]
},
{
+1
View File
@@ -1,4 +1,5 @@
import { generateFiles } from "fumadocs-openapi";
import { openapi } from "../src/lib/openapi";
void generateFiles({
+5 -5
View File
@@ -1,5 +1,6 @@
import { HomeLayout } from "fumadocs-ui/layouts/home";
import Link from "next/link";
import { TmdbLogo } from "@/components/tmdb-logo";
import { baseOptions } from "@/lib/layout.shared";
@@ -7,8 +8,8 @@ export default function Layout({ children }: LayoutProps<"/">) {
return (
<HomeLayout {...baseOptions()}>
{children}
<footer className="border-t border-fd-border px-6 py-8">
<div className="mx-auto flex max-w-5xl flex-col items-center gap-4 text-center text-xs text-fd-muted-foreground">
<footer className="border-fd-border border-t px-6 py-8">
<div className="text-fd-muted-foreground mx-auto flex max-w-5xl flex-col items-center gap-4 text-center text-xs">
<div className="flex flex-wrap items-center justify-center gap-x-2 gap-y-1">
<a
href="https://github.com/jakejarvis/sofa"
@@ -36,9 +37,8 @@ export default function Layout({ children }: LayoutProps<"/">) {
>
<TmdbLogo className="h-3" />
</a>
<p className="max-w-md text-fd-muted-foreground/80">
This product uses the TMDB API but is not endorsed or certified by
TMDB.
<p className="text-fd-muted-foreground/80 max-w-md">
This product uses the TMDB API but is not endorsed or certified by TMDB.
</p>
</div>
</div>
+26 -30
View File
@@ -9,6 +9,7 @@ import {
Smartphone,
} from "lucide-react";
import Link from "next/link";
import { ScrollIndicator } from "@/components/scroll-indicator";
import { SofaLogo } from "@/components/sofa-logo";
@@ -21,8 +22,7 @@ const features = [
},
{
title: "Media Server Sync",
description:
"Automatically log watches from Plex, Jellyfin, or Emby via webhooks.",
description: "Automatically log watches from Plex, Jellyfin, or Emby via webhooks.",
icon: MonitorPlay,
},
{
@@ -44,8 +44,7 @@ const features = [
},
{
title: "One Container",
description:
"Deploy with a single Docker image. No external database, no sidecar services.",
description: "Deploy with a single Docker image. No external database, no sidecar services.",
icon: Container,
},
];
@@ -55,16 +54,16 @@ const steps = [
step: "1",
title: "Get a TMDB API token",
content: (
<p className="text-sm text-fd-muted-foreground">
<p className="text-fd-muted-foreground text-sm">
Create a free account at{" "}
<a
href="https://www.themoviedb.org/settings/api"
target="_blank"
rel="noopener noreferrer"
className="text-fd-primary underline underline-offset-4 hover:text-fd-primary/80"
className="text-fd-primary hover:text-fd-primary/80 underline underline-offset-4"
>
themoviedb.org
<ExternalLinkIcon className="size-3 inline-block translate-y-[-2px] text-fd-muted-foreground ml-1" />
<ExternalLinkIcon className="text-fd-muted-foreground ml-1 inline-block size-3 translate-y-[-2px]" />
</a>
</p>
),
@@ -73,7 +72,7 @@ const steps = [
step: "2",
title: "Set three environment variables",
content: (
<pre className="overflow-x-auto mt-2 rounded-lg bg-fd-background/80 p-3 text-xs text-fd-muted-foreground ring-1 ring-white/[0.06]">
<pre className="bg-fd-background/80 text-fd-muted-foreground mt-2 overflow-x-auto rounded-lg p-3 text-xs ring-1 ring-white/[0.06]">
<code>{`TMDB_API_READ_ACCESS_TOKEN=...
BETTER_AUTH_SECRET=...
BETTER_AUTH_URL=https://sofa.example.com`}</code>
@@ -84,7 +83,7 @@ BETTER_AUTH_URL=https://sofa.example.com`}</code>
step: "3",
title: "Start the container",
content: (
<pre className="overflow-x-auto mt-2 rounded-lg bg-fd-background/80 p-3 text-xs text-fd-muted-foreground ring-1 ring-white/[0.06]">
<pre className="bg-fd-background/80 text-fd-muted-foreground mt-2 overflow-x-auto rounded-lg p-3 text-xs ring-1 ring-white/[0.06]">
<code>docker compose up -d</code>
</pre>
),
@@ -99,10 +98,10 @@ export default function HomePage() {
<div className="pointer-events-none absolute top-[60vh] -right-24 h-[400px] w-[400px] rounded-full bg-[oklch(0.8_0.14_65)] opacity-[0.04] blur-[100px]" />
{/* ── Hero ── */}
<section className="relative flex min-h-[calc(100vh-3.5rem)] flex-col items-center justify-center px-6 pb-16 pt-8 text-center">
<section className="relative flex min-h-[calc(100vh-3.5rem)] flex-col items-center justify-center px-6 pt-8 pb-16 text-center">
<div className="relative flex max-w-2xl flex-col items-center gap-8">
<p
className="animate-stagger-item text-xs font-medium tracking-[0.3em] text-fd-primary uppercase"
className="animate-stagger-item text-fd-primary text-xs font-medium tracking-[0.3em] uppercase"
style={{ "--stagger-index": 0 } as React.CSSProperties}
>
Self-hosted movie & TV tracker
@@ -112,7 +111,7 @@ export default function HomePage() {
className="animate-stagger-item"
style={{ "--stagger-index": 1 } as React.CSSProperties}
>
<SofaLogo className="size-20 text-fd-primary md:size-28" />
<SofaLogo className="text-fd-primary size-20 md:size-28" />
</div>
<h1
@@ -123,11 +122,10 @@ export default function HomePage() {
</h1>
<p
className="animate-stagger-item max-w-md text-base leading-relaxed text-fd-muted-foreground md:text-lg"
className="animate-stagger-item text-fd-muted-foreground max-w-md text-base leading-relaxed md:text-lg"
style={{ "--stagger-index": 3 } as React.CSSProperties}
>
Sofa lives on your own server and seamlessly plugs into your
existing home media stack.
Sofa lives on your own server and seamlessly plugs into your existing home media stack.
</p>
<div
@@ -136,7 +134,7 @@ export default function HomePage() {
>
<Link
href="/docs"
className="inline-flex h-11 items-center rounded-lg bg-fd-primary px-7 text-sm font-medium text-fd-primary-foreground transition-shadow hover:shadow-lg hover:shadow-fd-primary/20"
className="bg-fd-primary text-fd-primary-foreground hover:shadow-fd-primary/20 inline-flex h-11 items-center rounded-lg px-7 text-sm font-medium transition-shadow hover:shadow-lg"
>
Get Started
</Link>
@@ -144,7 +142,7 @@ export default function HomePage() {
href="https://github.com/jakejarvis/sofa"
target="_blank"
rel="noopener noreferrer"
className="inline-flex h-11 items-center rounded-lg px-7 text-sm font-medium ring-1 ring-fd-border transition-colors hover:bg-fd-accent hover:ring-fd-primary/30"
className="ring-fd-border hover:bg-fd-accent hover:ring-fd-primary/30 inline-flex h-11 items-center rounded-lg px-7 text-sm font-medium ring-1 transition-colors"
>
GitHub
</a>
@@ -160,12 +158,12 @@ export default function HomePage() {
{features.map((feature, i) => (
<div
key={feature.title}
className="animate-stagger-item rounded-xl bg-fd-card/50 p-6 ring-1 ring-white/[0.06]"
className="animate-stagger-item bg-fd-card/50 rounded-xl p-6 ring-1 ring-white/[0.06]"
style={{ "--stagger-index": i } as React.CSSProperties}
>
<feature.icon className="mb-3 size-5 text-fd-primary" />
<h3 className="mb-1.5 font-display text-lg">{feature.title}</h3>
<p className="text-sm leading-relaxed text-fd-muted-foreground">
<feature.icon className="text-fd-primary mb-3 size-5" />
<h3 className="font-display mb-1.5 text-lg">{feature.title}</h3>
<p className="text-fd-muted-foreground text-sm leading-relaxed">
{feature.description}
</p>
</div>
@@ -176,7 +174,7 @@ export default function HomePage() {
{/* ── Quick Start ── */}
<section className="mx-auto w-full max-w-3xl px-6 pb-24">
<h2
className="animate-stagger-item mb-8 text-center font-display text-2xl md:text-3xl"
className="animate-stagger-item font-display mb-8 text-center text-2xl md:text-3xl"
style={{ "--stagger-index": 0 } as React.CSSProperties}
>
Deploy in minutes
@@ -186,13 +184,11 @@ export default function HomePage() {
{steps.map((s) => (
<div
key={s.step}
className="animate-stagger-item rounded-xl bg-fd-card/50 p-5 ring-1 ring-white/[0.06]"
style={
{ "--stagger-index": Number(s.step) } as React.CSSProperties
}
className="animate-stagger-item bg-fd-card/50 rounded-xl p-5 ring-1 ring-white/[0.06]"
style={{ "--stagger-index": Number(s.step) } as React.CSSProperties}
>
<div className="flex items-baseline gap-3">
<span className="flex size-6 shrink-0 items-center justify-center rounded-full bg-fd-primary text-xs font-semibold text-fd-primary-foreground">
<span className="bg-fd-primary text-fd-primary-foreground flex size-6 shrink-0 items-center justify-center rounded-full text-xs font-semibold">
{s.step}
</span>
<h3 className="font-display text-base">{s.title}</h3>
@@ -202,15 +198,15 @@ export default function HomePage() {
))}
</div>
<p className="mt-4 text-xs text-fd-muted-foreground flex items-center gap-1 mx-auto w-fit">
<InfoIcon className="size-3 inline-block" />
<p className="text-fd-muted-foreground mx-auto mt-4 flex w-fit items-center gap-1 text-xs">
<InfoIcon className="inline-block size-3" />
The first registered account becomes admin.
</p>
<div className="mt-6 text-center">
<Link
href="/docs"
className="text-sm text-fd-primary underline underline-offset-4 hover:text-fd-primary/80"
className="text-fd-primary hover:text-fd-primary/80 text-sm underline underline-offset-4"
>
Read the full setup guide &rarr;
</Link>
+62 -99
View File
@@ -8,95 +8,75 @@ export const metadata: Metadata = {
export default function PrivacyPolicyPage() {
return (
<article className="prose prose-sm prose-invert mx-auto max-w-3xl px-6 py-16 [&_a]:text-fd-primary [&_a]:underline [&_a]:underline-offset-4 hover:[&_a]:text-fd-primary/80 [&_h2]:font-display [&_h2]:text-2xl [&_h2]:mt-10 [&_h2]:mb-4 [&_h2]:border-b [&_h2]:pb-2 [&_h3]:font-display [&_h3]:text-xl [&_h3]:mt-8 [&_h3]:mb-3 [&_p]:text-fd-muted-foreground [&_p]:text-[15px] [&_p]:leading-relaxed [&_li]:text-fd-muted-foreground [&_li]:text-[15px] [&_li]:leading-relaxed [&_ul]:space-y-1 [&_strong]:text-fd-foreground [&_strong]:font-medium">
<h1 className="font-display text-3xl tracking-tight mb-2">
Privacy Policy
</h1>
<p className="text-sm text-fd-muted-foreground/60 !mt-0 mb-8">
Last updated: March 16, 2026
</p>
<article className="prose prose-sm prose-invert [&_a]:text-fd-primary hover:[&_a]:text-fd-primary/80 [&_h2]:font-display [&_h3]:font-display [&_p]:text-fd-muted-foreground [&_li]:text-fd-muted-foreground [&_strong]:text-fd-foreground mx-auto max-w-3xl px-6 py-16 [&_a]:underline [&_a]:underline-offset-4 [&_h2]:mt-10 [&_h2]:mb-4 [&_h2]:border-b [&_h2]:pb-2 [&_h2]:text-2xl [&_h3]:mt-8 [&_h3]:mb-3 [&_h3]:text-xl [&_li]:text-[15px] [&_li]:leading-relaxed [&_p]:text-[15px] [&_p]:leading-relaxed [&_strong]:font-medium [&_ul]:space-y-1">
<h1 className="font-display mb-2 text-3xl tracking-tight">Privacy Policy</h1>
<p className="text-fd-muted-foreground/60 !mt-0 mb-8 text-sm">Last updated: March 16, 2026</p>
<p>
Sofa is a self-hosted application. When you run Sofa, your data lives on
your own server and is under your control. This policy describes what
data Sofa stores, what it sends externally, and how you can control it.
Sofa is a self-hosted application. When you run Sofa, your data lives on your own server and
is under your control. This policy describes what data Sofa stores, what it sends
externally, and how you can control it.
</p>
<h2>Data Stored on Your Server</h2>
<p>
All of the following data is stored locally in a single SQLite database
file on the machine running Sofa.
All of the following data is stored locally in a single SQLite database file on the machine
running Sofa.
</p>
<h3>Account Information</h3>
<ul>
<li>Email address, display name, and hashed password</li>
<li>Session tokens, IP address, and user agent string of active sessions</li>
<li>
Session tokens, IP address, and user agent string of active sessions
</li>
<li>
If using OIDC/SSO: provider account IDs and OAuth tokens from your
identity provider
If using OIDC/SSO: provider account IDs and OAuth tokens from your identity provider
</li>
</ul>
<h3>Viewing Activity</h3>
<ul>
<li>
Watchlist status (e.g. &ldquo;watching&rdquo;,
&ldquo;completed&rdquo;) for movies and TV shows
Watchlist status (e.g. &ldquo;watching&rdquo;, &ldquo;completed&rdquo;) for movies and TV
shows
</li>
<li>Individual movie and episode watch timestamps</li>
<li>Personal ratings</li>
<li>
Source of each watch event (manual entry, or imported from Plex,
Jellyfin, or Emby)
</li>
<li>Source of each watch event (manual entry, or imported from Plex, Jellyfin, or Emby)</li>
</ul>
<h3>Media Metadata</h3>
<p>
Movie, TV show, season, episode, and cast/crew metadata is fetched from{" "}
<a
href="https://www.themoviedb.org/"
target="_blank"
rel="noopener noreferrer"
>
<a href="https://www.themoviedb.org/" target="_blank" rel="noopener noreferrer">
The Movie Database (TMDB)
</a>{" "}
and cached locally. This includes titles, descriptions, images, genres,
streaming availability, and related recommendations. No personal data is
included in this metadata.
and cached locally. This includes titles, descriptions, images, genres, streaming
availability, and related recommendations. No personal data is included in this metadata.
</p>
<h3>Backups</h3>
<p>
Manual and scheduled backups are full copies of the database stored on
your server. They contain all of the data described above. Backup
management (creation, download, deletion) requires admin authentication.
Manual and scheduled backups are full copies of the database stored on your server. They
contain all of the data described above. Backup management (creation, download, deletion)
requires admin authentication.
</p>
<h2>External Services</h2>
<p>
Sofa communicates with the following external services during normal
operation.
</p>
<p>Sofa communicates with the following external services during normal operation.</p>
<h3>The Movie Database (TMDB)</h3>
<p>
Your server makes API requests to TMDB to fetch and refresh movie and TV
metadata. These requests include your TMDB API key and the IDs or search
queries for titles being looked up. A single API key is used for the
entire instance &mdash; TMDB cannot identify individual users. No
personal data (watch history, ratings, etc.) is ever sent to TMDB.
Your server makes API requests to TMDB to fetch and refresh movie and TV metadata. These
requests include your TMDB API key and the IDs or search queries for titles being looked up.
A single API key is used for the entire instance &mdash; TMDB cannot identify individual
users. No personal data (watch history, ratings, etc.) is ever sent to TMDB.
</p>
<p>
When the local image cache is enabled (the default), poster and backdrop
images are downloaded from the TMDB CDN and served from your server.
When disabled, images are loaded directly from TMDB&rsquo;s CDN by the
client.
When the local image cache is enabled (the default), poster and backdrop images are
downloaded from the TMDB CDN and served from your server. When disabled, images are loaded
directly from TMDB&rsquo;s CDN by the client.
</p>
<h3>Sofa Public API</h3>
@@ -104,78 +84,67 @@ export default function PrivacyPolicyPage() {
<ul>
<li>
<strong>Update checks</strong> &mdash; a periodic request to{" "}
<code>public-api.sofa.watch</code> to check for new releases. Only a
user agent string is sent; no instance or user data is included. Can
be disabled in admin settings.
<code>public-api.sofa.watch</code> to check for new releases. Only a user agent string is
sent; no instance or user data is included. Can be disabled in admin settings.
</li>
<li>
<strong>Telemetry</strong> &mdash; an optional, anonymous report sent
at most once every 24 hours. It includes a random instance ID, the
Sofa version, CPU architecture, bucketed user and title counts (e.g.
&ldquo;2-5&rdquo;), and which optional features are enabled. No
personal data, watch history, or exact counts are included. Telemetry
is <strong>disabled by default</strong> and must be explicitly enabled
by an admin. See the{" "}
<Link href="/docs/telemetry">telemetry documentation</Link> for full
details.
<strong>Telemetry</strong> &mdash; an optional, anonymous report sent at most once every
24 hours. It includes a random instance ID, the Sofa version, CPU architecture, bucketed
user and title counts (e.g. &ldquo;2-5&rdquo;), and which optional features are enabled.
No personal data, watch history, or exact counts are included. Telemetry is{" "}
<strong>disabled by default</strong> and must be explicitly enabled by an admin. See the{" "}
<Link href="/docs/telemetry">telemetry documentation</Link> for full details.
</li>
</ul>
<h3>PostHog (Mobile App Only)</h3>
<p>
The native iOS and Android app includes optional, anonymous analytics
powered by PostHog. This tracks screen views and app lifecycle events
&mdash; no personal data, search queries, or watch history is collected.
Analytics is <strong>disabled by default</strong> and requires explicit
opt-in. On iOS, Apple&rsquo;s App Tracking Transparency prompt is shown
first. You can change your preference at any time in the app&rsquo;s
settings.
The native iOS and Android app includes optional, anonymous analytics powered by PostHog.
This tracks screen views and app lifecycle events &mdash; no personal data, search queries,
or watch history is collected. Analytics is <strong>disabled by default</strong> and
requires explicit opt-in. On iOS, Apple&rsquo;s App Tracking Transparency prompt is shown
first. You can change your preference at any time in the app&rsquo;s settings.
</p>
<h3>Media Server Integrations</h3>
<p>
If you connect Plex, Jellyfin, or Emby, those services send webhook
events <em>to</em> your Sofa server when you finish watching something.
This data is processed and stored locally. Sofa does not send data back
to your media servers.
If you connect Plex, Jellyfin, or Emby, those services send webhook events <em>to</em> your
Sofa server when you finish watching something. This data is processed and stored locally.
Sofa does not send data back to your media servers.
</p>
<h3>Sonarr & Radarr</h3>
<p>
If you use Sonarr or Radarr integration, those services pull your
watchlist from Sofa via authenticated API requests. Sofa does not push
data to them.
If you use Sonarr or Radarr integration, those services pull your watchlist from Sofa via
authenticated API requests. Sofa does not push data to them.
</p>
<h2>Cookies</h2>
<p>
Sofa uses a session cookie to keep you logged in. This cookie is
HTTP-only, same-site, and contains only a session token. No third-party
cookies are set by the web app.
Sofa uses a session cookie to keep you logged in. This cookie is HTTP-only, same-site, and
contains only a session token. No third-party cookies are set by the web app.
</p>
<h2>Data Sharing</h2>
<p>
Because Sofa is self-hosted, there is no central service that has access
to your data. The project maintainers have no access to your database,
your watch history, or your account information. The only data that
leaves your server is described in the{" "}
Because Sofa is self-hosted, there is no central service that has access to your data. The
project maintainers have no access to your database, your watch history, or your account
information. The only data that leaves your server is described in the{" "}
<a href="#external-services">External Services</a> section above.
</p>
<h2>Data Retention & Deletion</h2>
<p>
All data is stored in a single SQLite file on your server. You have full
control over it:
All data is stored in a single SQLite file on your server. You have full control over it:
</p>
<ul>
<li>
Admin users can delete other user accounts, which cascades to all
associated viewing history and ratings
Admin users can delete other user accounts, which cascades to all associated viewing
history and ratings
</li>
<li>
Scheduled backups are automatically pruned after a configurable
retention limit (default: 7)
Scheduled backups are automatically pruned after a configurable retention limit (default:
7)
</li>
<li>Sessions and verification tokens expire automatically</li>
<li>Deleting the database file removes all data entirely</li>
@@ -183,20 +152,15 @@ export default function PrivacyPolicyPage() {
<h2>Children</h2>
<p>
Sofa is not directed at children under 13. Since the application is
self-hosted, account creation is controlled entirely by the server
administrator.
Sofa is not directed at children under 13. Since the application is self-hosted, account
creation is controlled entirely by the server administrator.
</p>
<h2>Open Source</h2>
<p>
Sofa is open source under the MIT License. You can audit exactly what
data is collected and how it is handled by reviewing the{" "}
<a
href="https://github.com/jakejarvis/sofa"
target="_blank"
rel="noopener noreferrer"
>
Sofa is open source under the MIT License. You can audit exactly what data is collected and
how it is handled by reviewing the{" "}
<a href="https://github.com/jakejarvis/sofa" target="_blank" rel="noopener noreferrer">
source code
</a>
.
@@ -204,9 +168,8 @@ export default function PrivacyPolicyPage() {
<h2>Changes</h2>
<p>
This policy may be updated as new features are added. Changes will be
reflected in the &ldquo;Last updated&rdquo; date above and committed to
the repository.
This policy may be updated as new features are added. Changes will be reflected in the
&ldquo;Last updated&rdquo; date above and committed to the repository.
</p>
<h2>Contact</h2>
+1
View File
@@ -1,4 +1,5 @@
import { createFromSource } from "fumadocs-core/search/server";
import { source } from "@/lib/source";
export const { GET } = createFromSource(source, {
+4 -7
View File
@@ -9,6 +9,7 @@ import {
import { createRelativeLink } from "fumadocs-ui/mdx";
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { getMDXComponents } from "@/components/mdx";
import { gitConfig } from "@/lib/layout.shared";
import { getPageImage, source } from "@/lib/source";
@@ -23,10 +24,8 @@ export default async function Page(props: PageProps<"/docs/[[...slug]]">) {
return (
<DocsPage toc={page.data.toc} full={page.data.full}>
<DocsTitle>{page.data.title}</DocsTitle>
<DocsDescription className="mb-0">
{page.data.description}
</DocsDescription>
<div className="flex flex-row gap-2 items-center border-b pb-6">
<DocsDescription className="mb-0">{page.data.description}</DocsDescription>
<div className="flex flex-row items-center gap-2 border-b pb-6">
<MarkdownCopyButton markdownUrl={`${page.url}.mdx`} />
<ViewOptionsPopover
markdownUrl={`${page.url}.mdx`}
@@ -49,9 +48,7 @@ export async function generateStaticParams() {
return source.generateParams();
}
export async function generateMetadata(
props: PageProps<"/docs/[[...slug]]">,
): Promise<Metadata> {
export async function generateMetadata(props: PageProps<"/docs/[[...slug]]">): Promise<Metadata> {
const params = await props.params;
const page = source.getPage(params.slug);
if (!page) notFound();
+1
View File
@@ -1,4 +1,5 @@
import { DocsLayout } from "fumadocs-ui/layouts/docs";
import { baseOptions } from "@/lib/layout.shared";
import { source } from "@/lib/source";
@@ -1,12 +1,10 @@
import { notFound } from "next/navigation";
import { getLLMText, source } from "@/lib/source";
export const revalidate = false;
export async function GET(
_req: Request,
{ params }: RouteContext<"/llms.mdx/docs/[[...slug]]">,
) {
export async function GET(_req: Request, { params }: RouteContext<"/llms.mdx/docs/[[...slug]]">) {
const { slug } = await params;
const page = source.getPage(slug);
if (!page) notFound();
+1
View File
@@ -1,4 +1,5 @@
import { llms } from "fumadocs-core/source";
import { source } from "@/lib/source";
export const revalidate = false;
+3 -7
View File
@@ -1,5 +1,6 @@
import { ImageResponse } from "@takumi-rs/image-response";
import { notFound } from "next/navigation";
import { getPageImage, source } from "@/lib/source";
export const revalidate = false;
@@ -37,10 +38,7 @@ const PRIMARY = "#fba952";
// oklch(0.63 0.02 80) → muted foreground
const MUTED = "#90887c";
export async function GET(
_req: Request,
{ params }: RouteContext<"/og/docs/[...slug]">,
) {
export async function GET(_req: Request, { params }: RouteContext<"/og/docs/[...slug]">) {
const { slug } = await params;
const page = source.getPage(slug.slice(0, -1));
if (!page) notFound();
@@ -110,9 +108,7 @@ export async function GET(
{title}
</div>
{description ? (
<div style={{ fontSize: "24px", lineHeight: 1.5, color: MUTED }}>
{description}
</div>
<div style={{ fontSize: "24px", lineHeight: 1.5, color: MUTED }}>{description}</div>
) : null}
</div>
+2
View File
@@ -1,5 +1,7 @@
import { createAPIPage } from "fumadocs-openapi/ui";
import { openapi } from "@/lib/openapi";
import client from "./api-page.client";
export const APIPage = createAPIPage(openapi, { client });
+1
View File
@@ -1,5 +1,6 @@
import defaultMdxComponents from "fumadocs-ui/mdx";
import type { MDXComponents } from "mdx/types";
import { APIPage } from "@/components/api-page";
export function getMDXComponents(components?: MDXComponents) {
+2 -5
View File
@@ -43,13 +43,10 @@ export function ScrollIndicator() {
return (
<div
ref={ref}
className="absolute bottom-9 flex flex-col items-center gap-1.5 text-fd-muted-foreground transition-opacity duration-500"
className="text-fd-muted-foreground absolute bottom-9 flex flex-col items-center gap-1.5 transition-opacity duration-500"
style={{ opacity: shown && !scrolled && fits ? 1 : 0 }}
>
<ArrowDown
className="size-7 animate-scroll-bounce stroke-2"
aria-hidden="true"
/>
<ArrowDown className="animate-scroll-bounce size-7 stroke-2" aria-hidden="true" />
</div>
);
}
+1
View File
@@ -1,4 +1,5 @@
import type { BaseLayoutProps } from "fumadocs-ui/layouts/shared";
import { SofaLogo } from "@/components/sofa-logo";
export const gitConfig = {
+2 -1
View File
@@ -1,8 +1,9 @@
import { docs } from "collections/server";
import { type InferPageType, loader } from "fumadocs-core/source";
import { lucideIconsPlugin } from "fumadocs-core/source/lucide-icons";
import { openapiPlugin } from "fumadocs-openapi/server";
import { docs } from "collections/server";
// See https://fumadocs.dev/docs/headless/source-api for more info
export const source = loader({
baseUrl: "/docs",
+1 -4
View File
@@ -1,10 +1,7 @@
import { isMarkdownPreferred, rewritePath } from "fumadocs-core/negotiation";
import { type NextRequest, NextResponse } from "next/server";
const { rewrite: rewriteLLM } = rewritePath(
"/docs{/*path}",
"/llms.mdx/docs{/*path}",
);
const { rewrite: rewriteLLM } = rewritePath("/docs{/*path}", "/llms.mdx/docs{/*path}");
export default function proxy(request: NextRequest) {
if (isMarkdownPreferred(request)) {