Libroo is a private, physical-first library management system for home and small-library inventories. It tracks books, shelf locations, reading state, lending logistics, borrower links, and admin-managed access.
- Nuxt 4, Nuxt UI v4, Pinia, and Nuxt Image.
- Effect services and repositories for server-side business logic.
- Drizzle ORM with SQLite-compatible storage.
- Better Auth for accounts, sessions, roles, bans, password reset, and email verification.
- NuxtHub on Cloudflare for hosted D1 database and R2/blob storage.
- Self-hosted Docker profile with local libSQL/SQLite, local blob storage, and Sharp WebP cover conversion.
Database migrations live under server/db/migrations/sqlite.
- Email/password authentication through Better Auth.
- First-user admin promotion for empty installs.
- Admin user management, role changes, bans, invites, and audit log.
- Optional public registration or invite-only registration.
- Manual book creation and ISBN lookup with Open Library metadata.
- Library list/detail views, authors, locations, tags, ratings, notes, and reading progress.
- Borrowing and lending workflows, including public borrower invite links.
- Versioned CSV import/export with optional background metadata and cover enrichment.
- Local and hosted cover/blob storage.
- Optional SMTP or Plunk email delivery for verification, password reset, invites, and security notifications.
- Self-hosted Docker deployment and hosted Cloudflare/NuxtHub deployment.
Run Libroo on your own machine or server with Docker and Docker Compose. Your library, uploads, and cover images are kept in a persistent Docker volume.
Clone the repository and create .env. The following creates a new auth secret
only when .env does not already exist, so it is safe to run again.
git clone https://github.com/niklhut/libroo.git
cd libroo
if [ -e .env ] || [ -L .env ]; then
echo '.env already exists; leaving it unchanged.'
else
umask 077
secret="$(openssl rand -hex 32)"
sed '/^NUXT_BETTER_AUTH_SECRET=/d' .env.example > .env
printf '\nNUXT_BETTER_AUTH_SECRET=%s\n' "$secret" >> .env
fiIf people will access Libroo through a domain or reverse proxy, set
NUXT_BETTER_AUTH_URL in .env to that public address before starting. The
default http://localhost:3000 is suitable when running it on your own machine.
docker compose up -dOpen http://localhost:3000/register and create the first account. It becomes
the administrator for the new library.
Normal installs follow the newest published release. If you prefer to choose
when to upgrade, set LIBROO_IMAGE in your untracked .env to a full release
tag, such as:
LIBROO_IMAGE=ghcr.io/niklhut/libroo:0.5.0Use an image digest instead when you need to run one exact image. See .env.example for the other available settings.
If you are changing Libroo or want to run an image built from this checkout:
docker build -t libroo:local .
LIBROO_IMAGE=libroo:local docker compose up -dYou can instead set LIBROO_IMAGE=libroo:local in your untracked .env while
developing. Do not change the repository's Compose file.
Back up before upgrading. The libroo-data Docker volume contains the database
at /data/db/sqlite.db and uploads at /data/blob.
Keep your existing .env; do not replace it with .env.example. In particular,
keep NUXT_BETTER_AUTH_SECRET unchanged because changing it signs everyone out.
To update to the newest published release:
docker compose pull --policy always
docker compose up -dLibroo applies database migrations as it starts. Migrations move forward only unless a release provides a rollback plan.
You can also use the included backup and restore commands:
pnpm backup:selfhost -- --output-dir /backups/libroo
pnpm restore:selfhost -- /backups/libroo/libroo-selfhost-backup-YYYY-MM-DDTHH-MM-SSZ.tar.gzSee docs/backup-restore.md for self-hosted archives, hosted D1/R2 exports, manifests, verification, and retention guidance. The hosted-service backup retention target is 30 days before public launch.
Better Auth documentation refers to BETTER_AUTH_SECRET and BETTER_AUTH_URL. In this Nuxt app, set them with the Nuxt runtime-config prefix:
| Variable | Required | Notes |
|---|---|---|
NUXT_BETTER_AUTH_SECRET |
Production yes | Stable secret used to sign Better Auth sessions. Generate with openssl rand -base64 32 and keep it unchanged across restarts. |
NUXT_BETTER_AUTH_URL |
Production yes | Public origin of the app, for example https://libroo.example.com or http://localhost:3000. |
NUXT_LIBROO_RUNTIME_PROFILE |
Optional | selfhost by default. Use cloudflare for NuxtHub/D1/R2 builds. |
NUXT_DATABASE_URL |
Self-host | libSQL/SQLite URL. Local default is file:.data/db/sqlite.db; Docker uses file:/data/db/sqlite.db. |
NUXT_LOCAL_STORAGE_DIR |
Self-host | Local blob directory. Local default is .data/blob; Docker uses /data/blob. |
NUXT_PUBLIC_REGISTRATION_ENABLED |
Optional | true by default. Controls new-user creation, not sign-in. Set false after bootstrap to require an invite for password signup and block new OIDC users. |
NUXT_EMAIL_PASSWORD_ENABLED |
Optional | true by default. Set false to disable local password and passkey authentication, password signup (including invite-backed signup), and password reset. |
NUXT_PUBLIC_PASSKEYS_ENABLED |
Optional | false by default. Enables passkey sign-in and enrollment on a secure origin while local authentication is enabled; passkeys do not create users. |
NUXT_PUBLIC_OIDC_ENABLED |
Optional | false by default. Enables the configured OIDC provider; see OAuth / OIDC Sign-In. |
NUXT_OIDC_TRUST_PROVIDER |
Optional | false by default. When true, matching-email OIDC accounts may be linked automatically for an existing locally verified user even without a provider email_verified claim. Use only with a fully trusted IdP. |
NUXT_PUBLIC_OPEN_LIBRARY_LINKS_ENABLED |
Optional | true in development and false in production unless explicitly set. |
Registration and OIDC are independent switches. Passkeys are part of local
authentication and additionally require NUXT_EMAIL_PASSWORD_ENABLED=true.
After the first account has been created, the combinations behave as follows
(assuming OIDC and passkeys are otherwise configured where mentioned):
| Public registration | Email/password | New users | Existing users |
|---|---|---|---|
true |
true |
Public password signup and OIDC just-in-time creation are allowed. | Password, passkey, and already-linked OIDC sign-in remain available. |
false |
true |
Password signup requires a valid invite; OIDC cannot create a user. | Password, passkey, and already-linked OIDC sign-in remain available. |
true |
false |
OIDC can create users; password signup is unavailable even with an invite. | Already-linked OIDC sign-in remains available; password and passkey sign-in are disabled. |
false |
false |
No password or OIDC user creation; invites cannot create password accounts. | Only already-linked OIDC sign-in remains available. |
An empty installation has one bootstrap exception: its first account may be
created by password or OIDC even when public registration is disabled, and that
account is promoted to admin. NUXT_PUBLIC_OIDC_ENABLED only exposes the OIDC
provider; it does not override the registration policy. Passkeys are an
authentication method for existing users, not a signup method, and are disabled
alongside local password authentication.
With NUXT_OIDC_TRUST_PROVIDER=false, an OIDC sign-in whose email already
belongs to a Libroo user is not linked implicitly and returns
account_not_linked; the provider must be linked explicitly from an
authenticated account. Setting it to true makes the configured provider a
trusted identity source: an account with the same verified local email is linked
automatically even if the provider does not return email_verified=true.
Better Auth still requires the existing Libroo user's emailVerified field to
be true; disabling Libroo's email-verification feature does not mark existing
users as verified. A signed-in user can instead connect the configured provider
explicitly under Settings → Account & sign-in; this proves control of both
accounts without requiring email verification. Connect OIDC before disabling
the user's existing password sign-in method. Different-email accounts are never
merged. Enable trust only when the IdP reliably verifies ownership of every
email address it asserts.
For an existing installation, keep NUXT_PUBLIC_REGISTRATION_ENABLED=false
and NUXT_EMAIL_PASSWORD_ENABLED=true during the OIDC transition. That keeps
public password signup closed (invite-backed signup still works) while existing
users sign in with their current method and connect OIDC under Settings →
Account & sign-in. Once every user has linked and tested OIDC, local password
and passkey authentication can be disabled without affecting those linked
identities. See the
OIDC rollout guide for the full flow.
Email is optional, but password reset, invite emails, security notifications, and verification emails require a provider.
SMTP is available in the self-hosted profile:
NUXT_EMAIL_PROVIDER=smtp
NUXT_EMAIL_FROM="Libroo <no-reply@your-libroo.example.com>"
NUXT_SMTP_HOST=smtp.example.com
NUXT_SMTP_PORT=587
NUXT_SMTP_SECURE=false
NUXT_SMTP_USER=your-smtp-user
NUXT_SMTP_PASSWORD=your-smtp-passwordPlunk is available in both profiles and is the hosted Cloudflare profile's email provider:
NUXT_EMAIL_PROVIDER=plunk
NUXT_EMAIL_FROM=no-reply@your-libroo.example.com
NUXT_EMAIL_REPLY_TO=support@your-libroo.example.com
NUXT_PLUNK_API_KEY=sk_your_secret_key
NUXT_PLUNK_BASE_URL=https://next-api.useplunk.comSet NUXT_EMAIL_VERIFICATION_ENABLED=true when users should verify email ownership before normal app access. When it is false or unset, registration and sign-in work without verification; email changes apply after current-password confirmation.
The current baseline migration is server/db/migrations/sqlite/0000_initial_beta.sql. Fresh local and self-hosted installs should apply migrations to an empty SQLite database with:
pnpm exec node scripts/migrate-selfhost.mjsThe Docker image runs that script automatically before starting Nuxt. It creates the database directory, checks writable space, and applies migrations from server/db/migrations/sqlite.
Hosted Cloudflare deployments apply the same SQLite migration files to D1 after pnpm build:cloudflare:
pnpm build:cloudflare
pnpm exec wrangler d1 migrations apply DB --remote --config .output/server/wrangler.jsonAfter the beta release, add new Drizzle migrations linearly. Do not rewrite existing migration history for installations with live data.
For local development, install Node.js 22 or newer and pnpm 11.8.0, or enable
Corepack for the package manager version in package.json.
git clone https://github.com/niklhut/libroo
cd libroo
pnpm install
cp .env.example .env
openssl rand -base64 32Put the generated secret in .env:
NUXT_BETTER_AUTH_SECRET=<output from openssl rand -base64 32>
NUXT_BETTER_AUTH_URL=http://localhost:3000
NUXT_LIBROO_RUNTIME_PROFILE=selfhost
NUXT_DATABASE_URL=file:.data/db/sqlite.db
NUXT_LOCAL_STORAGE_DIR=.data/blobApply the SQLite baseline migration, then start Nuxt:
pnpm exec node scripts/migrate-selfhost.mjs
pnpm devOpen http://localhost:3000/register and create the first account. The Better Auth policy plugin in server/utils/libroo-admin-auth-plugin.ts promotes the first created user in an empty database to admin.
Useful checks before shipping changes:
pnpm lint:fix
pnpm typecheck
pnpm test
pnpm test:unit
pnpm test:d1
pnpm test:integration
pnpm test:scripts
pnpm test:e2epnpm testruns all Vitest projects.pnpm test:unitruns pure logic tests.pnpm test:d1runs Cloudflare D1/Miniflare-bound tests.pnpm test:integrationruns cross-service and booted-server flows.pnpm test:scriptsruns backup, restore, and migration script tests.pnpm test:e2eruns Playwright browser flows and builds the self-host runtime first.
CI runs these same Vitest projects through a per-project matrix, so a passing
pnpm test locally mirrors CI coverage. E2E runs separately because it depends
on its own Playwright build/runtime; CI still reports each Vitest project's
status individually.
The full first-release manual QA pass lives in docs/first-release-qa.md.
The hosted profile uses NuxtHub with Cloudflare D1 and R2-compatible blob storage. It requires Wrangler and Cloudflare credentials.
NUXT_LIBROO_RUNTIME_PROFILE=cloudflare pnpm build:cloudflareHosted deployment secrets and environment variables:
CLOUDFLARE_API_TOKENCLOUDFLARE_ACCOUNT_IDNUXT_HUB_CLOUDFLARE_DATABASE_IDNUXT_HUB_CLOUDFLARE_BUCKET_NAMENUXT_BETTER_AUTH_SECRETNUXT_BETTER_AUTH_URLNUXT_PLUNK_API_KEY, when email delivery is enabled
Recommended hosted variables:
NUXT_EMAIL_PROVIDER=plunkNUXT_EMAIL_FROM=no-reply@your-domain.exampleNUXT_EMAIL_VERIFICATION_ENABLED=trueNUXT_PUBLIC_REGISTRATION_ENABLED=false, after the first admin account existsNUXT_PUBLIC_OPEN_LIBRARY_LINKS_ENABLED=false
Deployments should apply D1 migrations immediately before wrangler deploy. The repository's deployment notes include the CI promotion policy, preview caveats, rollback expectations, and full variable tables in docs/deployment.md.
Start every new install with an empty database and public registration enabled:
NUXT_PUBLIC_REGISTRATION_ENABLED=trueOpen /register and create the first account, or use the configured OIDC button.
The librooAdminPolicyPlugin runs after Better Auth creates the user and
atomically assigns user.role = 'admin' if no admin user exists. After
confirming the account can access /admin/users, set
NUXT_PUBLIC_REGISTRATION_ENABLED=false for private or hosted invite-only
operation and restart/redeploy. The empty-database bootstrap is also permitted
when registration is already disabled, but enabling it during setup makes the
intended state clearer.
The policy also prevents common lockouts: admins cannot demote or ban themselves, and Libroo rejects demoting or banning the last active admin.
Libroo uses Better Auth as the source of truth for user authentication and account state.
- Roles are stored on Better Auth's
user.rolefield. Libroo treats any role token containingadminas admin. - Bans are stored on Better Auth's
user.banned,user.ban_reason, anduser.ban_expiresfields. - There is no separate Libroo user-auth table. Do not create or update a parallel user table for auth state.
- Manage users from
/admin/userswhenever possible. That UI uses Better Auth admin APIs and Libroo policy/audit plugins. - Admin invite links live in
signup_invitesand create normal Better Auth users when accepted. - Account deletion removes Better Auth records and the user's Libroo library data. See docs/account-deletion.md for retention and support operations.
Libroo is licensed under the GNU AGPLv3. You may use, modify, and self-host it. If you run a modified version as a network service for other users, the AGPL expects you to provide the corresponding source code for that modified version.
The app does not ship legally sufficient privacy-policy or imprint text. Operators can configure public legal URLs or Markdown sources with:
NUXT_PUBLIC_LEGAL_PRIVACY_POLICY_URL=
NUXT_PUBLIC_LEGAL_IMPRINT_URL=
NUXT_LEGAL_PRIVACY_POLICY_MARKDOWN_URL=
NUXT_LEGAL_IMPRINT_MARKDOWN_URL=Available in the beta release:
- Core private-library catalog, locations, reading progress, ratings, notes, and tags.
- ISBN lookup and manual entry.
- Lending, borrower links, and loan return/cancel flows.
- Better Auth-backed registration, login, password reset, email verification, first-admin promotion, invites, role management, bans, and audit log.
- SQLite self-hosting and Cloudflare/NuxtHub hosted deployment.
- CSV import/export and account deletion.
- Backup/restore tooling for self-hosted operators and documented hosted D1/R2 backup procedures.
Future work toward v1:
- Richer shelf mapping and physical inventory workflows.
- Reminders and notification scheduling for loans.
- Barcode scanner polish and bulk inventory flows.
- More import sources and metadata reconciliation tools.
- Hardened preview environments for Cloudflare deployments.