Skip to content

Per-user root folders (multi-user): wire owner_user_id + user-aware import paths #1220

Description

@vavallee

Context

docs/multi-user.md advertised per-user root folders, but they were never implemented — corrected in the docs by #1219. This issue tracks actually building the feature. Surfaced by a user who enabled BINDERY_ENFORCE_TENANCY expecting their own root folders and found none (no UI as admin or user).

Current state (the gap)

  • root_folders has an owner_user_id column (migration 025), but:
    • RootFolderRepo.Create (internal/db/root_folders.go) never sets it — every folder is created owner=0 (global).
    • RootFolderRepo.List never filters by user — every user sees the whole shared pool.
  • The management UI (web/src/pages/settings/RootFoldersTab.tsx) is admin-only (ADMIN_TABS in SettingsPage.tsx); regular users can't see or manage root folders at all.
  • Import destinations are not user-aware. Scanner.effectiveLibraryDir (internal/importer/scanner.go) resolves the target as: author's RootFolderID → global library.defaultRootFolderId setting → BINDERY_LIBRARY_DIR. No user dimension anywhere, so even with per-user folders, files wouldn't land in a per-user tree.

So tenancy today isolates what each user sees (authors/books/profiles/queue/history), not where files live. All users share one physical library tree.

Scope to actually implement

This is more than CRUD scoping — the load-bearing piece is the import-path resolver.

  1. DB layerRootFolderRepo.Create stamp owner_user_id; List scope via QueryScopeFor (admin / userID==0 passthrough, mirroring authors/books repos); ownership checks on Get/Update/Delete (Delete already calls CheckOwnership, but it's inert while everything is owner=0).
  2. API — thread the requesting user's ID through the root-folder handlers (internal/api/root_folders.go), CreateForUser-style, like authors.
  3. Import path resolver (critical) — make effectiveLibraryDir user-aware: resolve the destination from the owning user's root folder / per-user default, not just per-author + global default. Decide how this composes with the existing per-author RootFolderID.
  4. Per-user defaultlibrary.defaultRootFolderId is a single global setting; needs a per-user default (or per-user fallback) so each user's adds route correctly.
  5. UI — surface root folder management to regular users (it's currently admin-only). Decide the model: shared admin pool plus per-user folders, or fully per-user.
  6. Back-compat / migration — existing owner=0 folders stay shared; define semantics when tenancy is on (treat owner=0 as shared-to-all vs admin-only).

Open design questions

  • Do admins keep a shared global pool and users get their own, or is it fully per-user under EnforceTenancy?
  • With tenancy off (default), behaviour must stay exactly as today (single shared pool).
  • How does a per-user root folder interact with the per-author RootFolderID already selectable in the Add/Edit Author modals?

Acceptance

  • With BINDERY_ENFORCE_TENANCY=true, a non-admin user can create/list/manage only their own root folders, and their imports land in their own tree.
  • Tenancy off → unchanged single-pool behaviour.
  • The capability matrix in docs/multi-user.md can truthfully list root folders as per-user again.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions