---
title: "Ring File Cabinet"
description: "Member-gated personal file manager, trustee shares, gallery, product research alt folders, and Image/Video chat sibling saves via Generative Gallery"
locale: "en"
---
# Ring File Cabinet

> **Info**
> Use **Founder** / **Developer** tabs in the docs sidebar to filter this page. Uploads ride [RingFileBase](/docs/integrations/ring-filebase.md); public gallery bytes ride [Ring CDN](/docs/integrations/ring-cdn.md) `/files/{uuid}`.

**Ring File Cabinet** is the personal file manager for Ring clones: nested folders (max depth **3**), a three-column desktop workspace, `owner` / `trustee` ACL (trustee = view + download only), a **shared-with-me** desktop for every **subscriber+**, private gallery curation, and a public `/{username}/img` lightbox for items marked `visibility=public`.

### Access gates (verified)

| Surface | Who | Behavior |
|---------|-----|----------|
| `/file-cabinet` | Auth; **member+** | Own manager — `FileCabinetWrapper` → `FileCabinetDesktop`. Non-members see `MemberUpgradeGate` (same pattern as add-entity). |
| `/profile/cabinet` | — | **Redirects** to `/file-cabinet` (`ROUTES.PROFILE_CABINET` aliases `FILE_CABINET`). |
| `/profile/shared` | **subscriber+** | Full drag desktop of trustee shares. Subscriber-only users also see upgrade CTA `membersUpgrade` → membership with `returnTo=/profile/shared`. |
| `/profile/gallery` | Auth; **member+** | Curate image/video → `private` \| `unlisted` \| `public` (`MemberUpgradeGate` for non-members). |
| `/{username}/img` | Public | Lightbox for `visibility=public` items. |
| `/{username}/player` | Public | Mood player (renamed from `/songs`; no legacy redirect). Private manage stays `/profile/songs`. |

ACL grants are **immediate** (no invite-accept). Max upload **25MB** via `getCabinetStorageConfig()`. **Publications** use a separate co-author model — not File Cabinet ACL.

  **Own** File Cabinet (`/file-cabinet`) and gallery manage (`/profile/gallery`) require **member** privileges (`hasMemberPrivileges` + `MemberUpgradeGate`). **All subscribers** (and above) open `/profile/shared` for trustee shares — they do **not** get an own desktop or upload surface until they upgrade to member.

### For founders

## Why this matters for your clone

Members need a place for docs, scans, and media that is **not** chat attachment chaos and **not** the Admin Wiki vault. File Cabinet is personal storage with trusted viewers, a shared inbox for collaborators, and a public gallery face — useful for marketplaces, service rings, and any clone where trust includes “show my work.”

### Who gets what (operator view)

```mermaid
flowchart LR
  sub[Subscriber+] --> shared["/profile/shared\ntrustee desktop"]
  mem[Member+] --> own["/file-cabinet\nown manager"]
  mem --> gal["/profile/gallery"]
  own --> trust[Share as trustee]
  trust --> shared
  gal --> img["/{username}/img\npublic items"]
```

  
- **[Own file manager (member+)](/docs/features/subscriptions.md)** — Members open `/file-cabinet` — upload, folders, trustees via ContactPicker, expandable tree + options rail. Non-members hit the membership upgrade gate.

  
- **[Shared for all subscribers](/docs/features/subscriptions.md)** — Every **subscriber+** uses `/profile/shared` for trustee shares (view/download desktop). An in-rail upgrade CTA points to membership for an own manager.

  
- **[Public gallery](/docs/integrations/ring-cdn.md)** — Curate at `/profile/gallery` (member+); public items appear on `/{username}/img` via CDN URLs.

  
- **[Storage plane](/docs/integrations/ring-filebase.md)** — Objects live in RingFileBase / MinIO; gallery images can request `derivativesProfile: gallery`.

  
- **[Generative Gallery from cabinet](/docs/features/generative-media.md)** — Detail-panel Image/Video chat opens the generative fs-modal (`scope: cabinet`); saves siblings under the source parent.

### Typical scenarios

1. **Vendor media kit** — Member uploads PDFs and product photos on `/file-cabinet`; shares a folder with a **trustee** (view/download only, immediate grant).
2. **Store Agent Research alt folder** — Product **Research** writes `research-{runId}.md` and citation-backed images under `store/[storename]/[product-slug]/alt` (depth ≤3). Vendors open those images in the detail panel → **Image chat** / **Video chat** to enhance or enlive, then save a sibling next to the source.
3. **Subscriber collaborator** — A subscriber without membership still opens `/profile/shared`, arranges trusted files on their desktop, and downloads — without owning a personal cabinet.
4. **Public portfolio strip** — Owner marks selected images `public` → visitors open `/{username}/img`.
5. **Confidential vs shared** — Empty trustee list = **confidential**; Share `FsModal` shows a live Trustees row (or “confidential”) and **Make confidential** to revoke all trustees.

### Operator checklist

- Confirm migration `042_file_cabinet.sql` applied (tables `file_cabinet_*`) and the app image includes File Cabinet UI.
- Env: `NEXT_PUBLIC_STORAGE_PROVIDER=ring_filebase` (or equivalent), `RINGBASE_API_URL`, `RINGBASE_PUBLIC_URL`, `RINGBASE_API_TOKEN` — see [RingFileBase](/docs/integrations/ring-filebase.md).
- Smoke as **member**: upload on `/file-cabinet` → Share trustees → second user (subscriber+) sees the item on `/profile/shared`.
- Smoke as **subscriber** (non-member): `/file-cabinet` shows upgrade gate; `/profile/shared` still works for shared files; rail shows “Members get their own file manager”.
- Smoke gallery: add image → set `public` → open `/{username}/img`.
- Do not expect HMAC/TTL signed URLs — public delivery is stable CDN `/files/{fileId}` today.

### For developers

## Architecture

```mermaid
flowchart TB
  UI["/file-cabinet · /profile/shared · gallery"] --> Wrap["FileCabinetWrapper"]
  Wrap --> Desk["FileCabinetDesktop\nRingRightRailLayout"]
  Desk --> Actions["app/_actions/file-cabinet.ts"]
  Actions --> Svc["features/file-cabinet/service.ts"]
  Actions --> File["file().upload"]
  File --> RFB["RingFileBase / RingBaseAdapter"]
  Svc --> Nodes["file_cabinet_nodes"]
  Svc --> Acl["file_cabinet_acl"]
  Svc --> DeskDb["file_cabinet_desktop\nicons + treeExpandedIds"]
  Svc --> Gal["file_cabinet_gallery_items"]
  Svc --> Tunnel["publishToUserTunnel\nfile-cabinet:desktop-icons"]
  Dl["/api/file-cabinet/download"] --> Svc
  Img["/{username}/img"] --> Gal
  CDN["RINGBASE_PUBLIC_URL/files/{id}"] --> RFB
```

### Role gates in code (verified)

| Surface | Gate |
|---------|------|
| `app/.../file-cabinet/page.tsx` | `auth` → `hasMemberPrivileges` else `MemberUpgradeGate` (`returnTo` = `ROUTES.FILE_CABINET`) |
| `app/.../profile/shared/page.tsx` | `auth` → `hasRoleAtLeast(..., subscriber)` else unauthorized |
| `app/.../profile/gallery/page.tsx` | `auth` → `hasMemberPrivileges` else `MemberUpgradeGate` |
| `app/.../profile/cabinet/page.tsx` | Redirect → `ROUTES.FILE_CABINET` |
| Actions (`listOwnCabinetAction`, upload, own desktop) | `requireMember()` |
| Actions (`listSharedCabinetAction`, shared desktop) | `requireSubscriber()`; `scope: 'own'` still requires member |

### UX shell (verified)

`FileCabinetWrapper` is a thin client boundary (`components/wrappers/file-cabinet-wrapper.tsx`) that renders `FileCabinetDesktop`. The three-column shell (`RingRightRailLayout` + Davinci center pane) lives **inside** the desktop component.

| Piece | Role |
|-------|------|
| Center pane | Breadcrumbs + New folder / Upload (right) + icon workspace (no bordered desktop card) |
| Right rail | Title → expandable **folders-only** tree → options (`DavinciGlassPanel`, title = visible filename) → non-scrolling info panel |
| Tree | `+/−` expand; selected dir uses fully-rounded (`rounded-[99px]`) active surface; expand state in `treeExpandedIds` on the desktop document |
| Desktop icons | Visible name SSOT in `icons[].meta.filename` (rename overwrites; no history); image icons use `sync_thumb` via download `?variant=` |
| Empty folder | Delete control only when the folder has no children |
| Trustees | Options row + Share `FsModal` (live Trustees / **confidential**, inline ContactPicker, **Make confidential** when clearing trustees) |
| Loading | Fixed-height skeletons for trustees row, info meta, and image preview (no layout jump) |
| Shared upgrade | Subscriber-only + `scope === 'shared'` → `t('membersUpgrade')` CTA → `/membership?returnTo=/profile/shared` |
| Image / Video chat | Detail panel (`file-cabinet-detail-panel.tsx`) on image files → `GenerativeMediaEditorFsModal` (`scope="cabinet"`) |
| Sibling save | `saveGeneratedMediaToDesktopAction` under source `parentId` as `{base}-enhanced-{ts}.{ext}` or `{base}-enlive-{ts}.{ext}` |

### Product research alt path (store Agent Knowledge)

When vendors run **Research** on a product, `features/store/lib/product-cabinet-media.ts` ensures three nested dirs under the vendor owner’s cabinet (conceptual `files/` root + `MAX_FOLDER_DEPTH = 3`):

`store/[storename]/[product-slug]/alt`

Artifacts: `research-{runId}.md` plus downloaded `research-{runId}-N.{ext}` images (SSRF-checked HTTP(S) fetch → RingBase → `createFileNode`). Storefront gallery enablement stays on the product form (**Use in gallery**) — see [Multi-Vendor Store](/docs/features/store.md). Pipeline: `cabinet.genmedia.sibling_save` in `scripts/PIPELINES.md`.

### Module map

| Path | Role |
|------|------|
| `features/file-cabinet/service.ts` | `server-only` CRUD, ACL (incl. ancestor walk for inherited trustees), desktop, gallery |
| `features/file-cabinet/acl.ts` | `owner` \| `trustee` (legacy `editor` → `trustee`) |
| `features/file-cabinet/constants.ts` | `FILE_CABINET_DESKTOP_CHANNEL`, `FILE_CABINET_DOWNLOAD_PATH`, `MAX_FOLDER_DEPTH = 3` |
| `features/file-cabinet/desktop-filename.ts` | Visible filename helpers |
| `features/file-cabinet/media-urls.ts` | Same-origin download URL helpers (`?variant=`) |
| `features/file-cabinet/components/*` | Desktop, tree, options, detail (Image/Video chat), share/rename FsModals, skeletons, gallery, public img |
| `app/_actions/file-cabinet.ts` | Server Actions + role gates + `saveGeneratedMediaToDesktopAction` |
| `features/store/lib/product-cabinet-media.ts` | Store research → `store/…/alt` writer (SSRF-safe) |
| `app/api/file-cabinet/download/route.ts` | ACL check → byte stream (`inline=1`, optional `variant=` for thumbs/webp) |
| `lib/storage/storage-config.ts` | `getCabinetStorageConfig()` — 25MB + MIME allowlist |
| `components/file-tree/tree-helpers.ts` | Shared tree helpers (Admin Wiki adapters) |
| `data/migrations/042_file_cabinet.sql` | Schema (+ mirrored in `data/schema.sql`) |

### ACL model

| Role | Capabilities |
|------|----------------|
| `owner` | Full CRUD, set trustees via ContactPicker (`setCabinetTrusteesAction`) |
| `trustee` | View / list / download; shared desktop layout; **cannot** mutate or upload |

Grants write to `file_cabinet_acl` immediately — no invite-accept flow. Listing ACL for the options rail **walks ancestors** so folder shares surface on nested files.

### Schema (JSONB collections)

Tables (`id` + `data` JSONB): `file_cabinet_nodes`, `file_cabinet_acl`, `file_cabinet_desktop`, `file_cabinet_gallery_items`.

Desktop document stores icon positions and `treeExpandedIds` for the folders-only expand/collapse tree. Per-folder layout cache keeps drag positions when navigating nested folders.

  Pass document id as `createDoc(collection, data, { id })`. Putting `id` only inside `data` lets `PostgreSQLAdapter` `generateId()` diverge from `data.id`.

### Storage upload (verified)

Cabinet uploads use `file().upload(...)` with `access: 'private'`, `derivativesProfile: 'gallery'` for images, and MIME/size checks from `getCabinetStorageConfig()` (images, PDF, Office, text/csv/md, zip, plus `video/mp4` / `video/webm`).

Public gallery items store the CDN URL from upload (`storageUrl`). There are **no** HMAC/TTL signed URLs in the current `RingBaseAdapter` path — delivery is `{RINGBASE_PUBLIC_URL}/files/{fileId}` (+ `_v_*` derivatives). Private downloads use the ACL proxy (`/api/file-cabinet/download`) with `Content-Disposition` from `node.name` / desktop `meta.filename`. Direct CDN `/files/{uuid}` still lacks `Content-Disposition` — see backlog.

### Desktop tunnel

`saveDesktop` publishes to the member’s personal tunnel:

{`// features/file-cabinet/constants.ts
export const FILE_CABINET_DESKTOP_CHANNEL = 'file-cabinet:desktop-icons'
// service: publishToUserTunnel(userId, FILE_CABINET_DESKTOP_CHANNEL, { scope, icons, treeExpandedIds, updatedAt })`}

Client listens via `useTunnelChannel` in `file-cabinet-desktop.tsx`.

### Wire-up steps

### Apply migration

Ensure `042_file_cabinet.sql` (and schema.sql parity) on the clone DB. Fresh installs pick up tables from `data/schema.sql`.

### Confirm storage env

Reference [RingFileBase](/docs/integrations/ring-filebase.md): `NEXT_PUBLIC_STORAGE_PROVIDER=ring_filebase`, `RINGBASE_API_URL`, `RINGBASE_PUBLIC_URL`, `RINGBASE_API_TOKEN`.

### Call through Server Actions

Prefer `app/_actions/file-cabinet.ts` from client UI — do not bypass ACL helpers in `service.ts`. Own list/upload requires **member**; shared desktop requires **subscriber+**; `getDesktop` / `saveDesktop` for `scope: 'own'` still require member privileges.

### Public img page

`app/[locale]/[username]/img/page.tsx` loads `listPublicGalleryByOwner(ownerId)` and renders `PublicProfileImgGallery`. Only `visibility=public` items appear.

### Routes (constants)

| Constant | Path |
|----------|------|
| `ROUTES.FILE_CABINET` | `/file-cabinet` (canonical own manager) |
| `ROUTES.PROFILE_CABINET` | `/file-cabinet` (deprecated alias; `/profile/cabinet` redirects) |
| `ROUTES.PROFILE_SHARED` | `/profile/shared` |
| `ROUTES.PROFILE_GALLERY` | `/profile/gallery` |
| `ROUTES.PUBLIC_PROFILE_IMG(username)` | `/{username}/img` |
| `ROUTES.PUBLIC_PROFILE_PLAYER(username)` | `/{username}/player` |

Private mood-player manage remains `/profile/songs` (`ROUTES.PROFILE_SONGS`). Peer `/games` is a sibling public-profile surface — not File Cabinet storage.

## Related documentation

- [integrations/ring-filebase](/docs/integrations/ring-filebase.md) — Depends-on: cabinet uploads go through file() / RingBaseAdapter and getCabinetStorageConfig.

- [integrations/ring-cdn](/docs/integrations/ring-cdn.md) — Depends-on: public gallery items use stable CDN /files/{uuid} (+ _v_* derivatives).

- [features/public-profile](/docs/features/public-profile.md) — Next-step: public /img, /player, and /games surfaces hang off /{username}.

- [features/peer-games](/docs/features/peer-games.md) — See-also: public /games availability is a sibling profile surface (not File Cabinet ACL).

- [features/subscriptions](/docs/features/subscriptions.md) — Same-workflow: member gate for /file-cabinet vs subscriber+ shared desktop.

- [features/tunnel-protocol](/docs/features/tunnel-protocol.md) — Deep-dive: desktop icon sync uses publishToUserTunnel on channel file-cabinet:desktop-icons.

- [features/admin-wiki](/docs/features/admin-wiki.md) — See-also: shared FileTree helpers and JSONB id+data collection pattern.

- [features/store](/docs/features/store.md) — Same-workflow: Agent Knowledge Research writes markdown + images into store/product/alt.

- [features/generative-media](/docs/features/generative-media.md) — Depends-on: cabinet Image/Video chat reuses GenerativeMediaEditorFsModal with scope cabinet.
