---
name: Drive Suite Build
overview: "Build UltiDrive as a Google Drive–like frontend in `drive-suite`, with all backend/infra in `ulti-backend` (ultid API, Nextcloud, OnlyOffice, Authentik, deploy). Mail stays frontend-only in `gmail-interface-clone`. Production routing: same domain at `/drive`."
todos:
  - id: phase0-provisioning
    content: Fix drive EnsurePrincipal + extend drive API (quota, shares CRUD, restore, favorite, create blank file)
    status: completed
  - id: phase0-onlyoffice
    content: Deploy OnlyOffice Document Server, enable NC onlyoffice app, add /api/v1/office session+callback endpoints
    status: completed
  - id: phase1-scaffold
    content: Scaffold drive-suite Next.js app (auth, API client, basePath /drive, app shell, URL routing)
    status: completed
  - id: phase2-mvp
    content: Build Google Drive file browser MVP wired to real /api/v1/drive (upload, browse, share, trash, recent, starred)
    status: completed
  - id: phase3-editor
    content: Integrate OnlyOffice editor embed for docx/xlsx/pptx via /drive/edit/{id}
    status: completed
  - id: phase4-chrome
    content: "Google-like editor chrome: OnlyOffice theme first, then custom wrapper using drive-dump specs if needed"
    status: completed
  - id: phase5-suite
    content: "Wire suite integration: nginx /drive route, launcher hrefs, mail attachment picker, Authentik callbacks"
    status: completed
isProject: false
---

# UltiDrive (Drive Suite) — Build Plan

## Repository roles

| Repo | Responsibility |
|------|----------------|
| **[`ulti-backend`](file:///Users/red/workdev/ulti-backend)** | **All backend infrastructure and server-side code:** custom Go API (`ultid`), Docker Compose, edge nginx, Authentik, Postgres/KeyDB/RustFS, headless Nextcloud, mail stack (IMAP/SMTP/sync), OnlyOffice Document Server, Jitsi, observability, deploy scripts, env templates, Authentik blueprints. This is the single ops + API hub for the suite. |
| **[`gmail-interface-clone`](file:///Users/red/workdev/gmail-interface-clone)** | **Frontend only — Ultimail.** Next.js UI for mail (list, compose, settings, contacts). Talks to ulti-backend via `/api/v1` proxy; no server infrastructure lives here. |
| **[`drive-suite`](file:///Users/red/workdev/drive-suite)** | **Frontend only — UltiDrive.** Next.js UI for the Google Drive–like file browser and editor shell. Same pattern as mail: API calls go to ulti-backend; deploy wiring (nginx route, compose service) is defined in ulti-backend. |

Suite frontends are thin clients; every persistent service and integration runs from **ulti-backend**.

---

## Current state

| Layer | Status |
|-------|--------|
| [`drive-suite/`](file:///Users/red/workdev/drive-suite) | Empty placeholder — greenfield |
| [`ulti-backend`](file:///Users/red/workdev/ulti-backend) | Headless NC 30 deployed; `/api/v1/drive` proxy (list, upload, download, move, copy, rename, trash, recent, starred, shares) |
| [`gmail-interface-clone`](file:///Users/red/workdev/gmail-interface-clone) | Auth stack, shadcn UI, app shell patterns; UltiDrive tile exists but has no `href` |
| [`ultimail/apps/web/src/components/drive/`](file:///Users/red/workdev/ultimail/apps/web/src/components/drive) | Mock UI components (file-browser, share-dialog, upload-zone) — reference only |
| [`drive-dump/`](file:///Users/red/workdev/drive-dump) | Google Docs chrome research (titlebar, toolbar CSS specs) — useful for editor skinning, not for file browser |
| OnlyOffice | Not deployed; mentioned only in [`project-plan/ultidrive.md`](file:///Users/red/workdev/ulti-backend/project-plan/ultidrive.md) |

**Critical backend bug to fix first:** drive handlers pass `claims.Sub` as NC user ID, but NC OIDC maps `preferred_username` (email). Contacts already use `EnsurePrincipal(email, sub, name)` — drive must match or provisioning fails.

---

## Target architecture

```mermaid
flowchart TB
  subgraph frontends [Frontend repos]
    MailFE["gmail-interface-clone"]
    DriveFE["drive-suite"]
  end

  subgraph ultiBackend [ulti-backend deploy and API]
    Nginx["edge nginx"]
    Ultid["ultid Go API"]
    Auth["Authentik"]
    NC["Nextcloud headless"]
    OO["OnlyOffice"]
    MailSvc["mail IMAP SMTP sync"]
    S3["RustFS S3"]
  end

  MailFE -->|"/mail"| Nginx
  DriveFE -->|"/drive"| Nginx
  Nginx --> Ultid
  Nginx --> Auth
  Ultid --> DriveAPI["/api/v1/drive"]
  Ultid --> OfficeAPI["/api/v1/office"]
  Ultid --> MailAPI["/api/v1/mail"]
  Ultid --> WS["/ws"]
  DriveAPI --> NC
  OfficeAPI --> NC
  OfficeAPI --> OO
  NC --> OO
  NC --> S3
  MailAPI --> MailSvc
```

**Principles:**
- Browser never talks to Nextcloud or OnlyOffice directly — all via **ultid** in ulti-backend (auth, RBAC, stable API).
- Nextcloud UI stays hidden (`/cloud/` admin-only); users only see UltiDrive.
- Same Authentik OIDC session; production path `/drive` on the suite domain (shared cookies with `/mail`).

---

## Phase 0 — Backend hardening ([`ulti-backend`](file:///Users/red/workdev/ulti-backend) only)

All work in this phase stays in ulti-backend (Go API + `deploy/`). No infrastructure or API logic belongs in drive-suite or gmail-interface-clone.

Fix and extend the existing drive layer before building UI.

### 0.1 User provisioning fix
In [`internal/api/drive/handlers.go`](file:///Users/red/workdev/ulti-backend/internal/api/drive/handlers.go) and `service.go`, replace raw `claims.Sub` with:

```go
userID, err := nc.EnsurePrincipal(ctx, claims.Email, claims.Sub, claims.Name)
```

Mirror the contacts pattern in [`internal/nextcloud/users.go`](file:///Users/red/workdev/ulti-backend/internal/nextcloud/users.go).

### 0.2 API gaps (priority order)

| Endpoint | Purpose |
|----------|---------|
| `GET /drive/quota` | Storage bar in UI |
| `GET /drive/shares?path=` | List existing shares |
| `PUT /drive/shares/{id}` | Update role / expiry / password |
| `DELETE /drive/shares/{id}` | Revoke share |
| `POST /drive/trash/restore` | Restore from trash |
| `POST /drive/favorite` | Star/unstar |
| `POST /drive/files/new` | Create empty doc/sheet/slide (NC template or OO blank) |
| `GET /drive/search` | Full-text (v1: name filter; v2: Meilisearch) |

Extend [`internal/nextcloud/drive.go`](file:///Users/red/workdev/ulti-backend/internal/nextcloud/drive.go) with OCS share list/update/delete and favorite WebDAV properties.

### 0.3 WebSocket events
Add `drive.file_changed`, `drive.share_updated` to ultid WS hub so drive UI can invalidate TanStack Query cache (same pattern as mail events in [`gmail-interface-clone/lib/api/ws.ts`](file:///Users/red/workdev/gmail-interface-clone/lib/api/ws.ts)).

### 0.4 OnlyOffice + Nextcloud deployment

New files under [`deploy/onlyoffice/`](file:///Users/red/workdev/ulti-backend/deploy):

- `docker-compose.onlyoffice.yml` — Document Server container
- Update [`deploy/nextcloud/init.sh`](file:///Users/red/workdev/ulti-backend/deploy/nextcloud/init.sh) — `$OCC app:enable onlyoffice`
- Update [`deploy/compose-up.sh`](file:///Users/red/workdev/ulti-backend/deploy/compose-up.sh) — conditional include when `ONLYOFFICE_ENABLED=true`
- Update [`deploy/nginx/default.conf.template`](file:///Users/red/workdev/ulti-backend/deploy/nginx/default.conf.template):
  - `/drive/` → drive-suite container
  - `/office/` → OnlyOffice (internal, JWT-protected; not exposed to users directly)

Env vars to add to `.env.example`:

```
ONLYOFFICE_ENABLED=true
ONLYOFFICE_URL=http://onlyoffice:80
ONLYOFFICE_JWT_SECRET=...
ONLYOFFICE_PUBLIC_URL=https://{DOMAIN}/office
```

### 0.5 Office API (new package `internal/api/office/`)

| Endpoint | Role |
|----------|------|
| `POST /office/session` | Given `{ path, mode: view\|edit }`, return OnlyOffice editor config + signed JWT |
| `GET /office/callback` | OnlyOffice save callback → NC WebDAV PUT |
| `POST /office/create` | Create blank docx/xlsx/pptx in user folder |

Flow: drive UI opens `/drive/edit/{fileId}` → backend builds OnlyOffice config pointing at NC file via internal URL → OnlyOffice loads document → saves via callback.

---

## Phase 1 — Scaffold [`drive-suite`](file:///Users/red/workdev/drive-suite) (Google Drive shell)

Frontend-only repo. Copy **UI/auth/client patterns** from [`gmail-interface-clone`](file:///Users/red/workdev/gmail-interface-clone) (the mail frontend), not from ultimail (different router). Match stack: **Next.js 16, React 19, Tailwind 4, shadcn new-york, Zustand 5, TanStack Query 5, pnpm**.

### 1.1 Repo bootstrap
```
drive-suite/
├── app/
│   ├── layout.tsx              # QueryProvider + AuthProvider
│   ├── page.tsx                # redirect → /drive
│   ├── api/auth/               # copy OIDC routes from mail
│   ├── auth/complete/
│   ├── login/
│   └── drive/
│       ├── layout.tsx          # DriveAppShell
│       ├── [[...segments]]/page.tsx
│       ├── edit/[fileId]/page.tsx   # OnlyOffice embed (Phase 3)
│       └── settings/[[...section]]/
├── components/drive/           # file browser, sidebar, header, share, upload
├── lib/
│   ├── auth/                   # session, pkce, jwt-claims
│   ├── api/client.ts           # Bearer + 401 refresh
│   ├── api/hooks/use-drive-queries.ts
│   ├── drive-url.ts            # URL = source of truth
│   └── stores/                 # drive-ui-store, drive-settings-store
├── next.config.mjs             # basePath: '/drive' in prod; /api/v1 rewrite
└── Dockerfile                  # same multi-stage pattern as mail
```

**`basePath: '/drive'`** in production so assets and routes work behind nginx path routing.

### 1.2 Auth
Copy auth stack from mail; change:
- Default `returnTo` → `/drive`
- Persist key → `ultidrive-auth`
- Keep `ulti_*` cookie names (same origin)

### 1.3 App shell (Google Drive UX)

Three-zone layout mirroring [`mail-app-shell.tsx`](file:///Users/red/workdev/gmail-interface-clone/app/mail/mail-app-shell.tsx):

```
┌──────────────────────────────────────────────────────────┐
│ Header: search, view toggle, account, suite launcher    │
├──────────┬───────────────────────────────────────────────┤
│ Sidebar  │ Main: breadcrumb + file grid/list             │
│ My Drive │                                               │
│ Recent   │ (optional detail panel for file preview)      │
│ Starred  │                                               │
│ Trash    │                                               │
│ Storage  │                                               │
└──────────┴───────────────────────────────────────────────┘
```

Port/adapt from ultimail mock components:
- [`file-browser.tsx`](file:///Users/red/workdev/ultimail/apps/web/src/components/drive/file-browser.tsx) — grid/list, sort, multi-select
- [`breadcrumb-nav.tsx`](file:///Users/red/workdev/ultimail/apps/web/src/components/drive/breadcrumb-nav.tsx)
- [`upload-zone.tsx`](file:///Users/red/workdev/ultimail/apps/web/src/components/drive/upload-zone.tsx) — drag-drop + chunked upload via `X-Upload-*` headers
- [`share-dialog.tsx`](file:///Users/red/workdev/ultimail/apps/web/src/components/drive/share-dialog.tsx)
- [`quota-bar.tsx`](file:///Users/red/workdev/ultimail/apps/web/src/components/drive/quota-bar.tsx)

Add `--drive-*` CSS tokens in `globals.css` parallel to mail's `--mail-*`.

### 1.4 URL routing

[`lib/drive-url.ts`](file:///Users/red/workdev/drive-suite/lib/drive-url.ts) + `use-drive-route.ts`:

| URL segment | View |
|-------------|------|
| `/drive` | My Drive root |
| `/drive/folders/documents/page/2` | Folder + pagination |
| `/drive/recent` | Recent files |
| `/drive/starred` | Favorites |
| `/drive/trash` | Trash |
| `/drive/search?q=report` | Search results |
| `/drive/file/{id}` | Preview panel |
| `/drive/edit/{id}` | Editor (Phase 3) |

Wire real API from day one — no mock data.

---

## Phase 2 — Core Drive features (MVP)

Ship a usable Google Drive clone before editors.

| Feature | Backend | Frontend |
|---------|---------|----------|
| Browse folders | `GET /drive/files/*` | Grid/list + breadcrumb |
| Upload (simple + chunked) | `POST /drive/files/*` | Progress, retry |
| Create folder | `POST /drive/folders/*` | New folder dialog |
| Rename / move / copy | existing endpoints | Context menu + drag-drop move |
| Delete / trash / restore | DELETE + trash endpoints | Trash view |
| Starred / recent | existing endpoints | Sidebar views |
| Share (link + users) | shares CRUD | Share dialog (owner/editor/viewer) |
| Download | `GET /drive/download/*` | Context menu |
| Preview | download + mime routing | Images, PDF inline; others download |
| Storage quota | `GET /drive/quota` | Sidebar bar |
| Multi-select bulk ops | batch endpoints or sequential | Toolbar actions |

**New file affordance (Google-style):**
- Dropdown: Blank document / spreadsheet / presentation / folder / upload
- Creates file via `POST /office/create` or NC template, then opens editor

---

## Phase 3 — OnlyOffice editor integration

### 3.1 Embed pattern
[`app/drive/edit/[fileId]/page.tsx`](file:///Users/red/workdev/drive-suite/app/drive/edit/[fileId]/page.tsx):

1. Fetch editor config from `POST /api/v1/office/session`
2. Load `@onlyoffice/document-editor-react` (or iframe with DocsAPI)
3. Full-viewport editor; minimal chrome initially

### 3.2 File type routing

| MIME / extension | Editor |
|------------------|--------|
| `.docx`, `.odt` | Document |
| `.xlsx`, `.ods` | Spreadsheet |
| `.pptx`, `.odp` | Presentation |
| Other | Preview or download only |

### 3.3 Save / co-editing
OnlyOffice handles real-time co-editing natively when connected to NC via the OnlyOffice app. Backend callback ensures saves land in NC WebDAV. No custom CRDT needed for v1.

---

## Phase 4 — Google-like editor chrome (theme → wrapper → fork)

Decision gate after Phase 3 ships with stock OnlyOffice UI.

### Step A — OnlyOffice theme (try first, ~1–2 weeks)
- Custom JSON skin: toolbar colors, fonts (Roboto/Google Sans), icon set
- CSS overrides via OnlyOffice `customization` config
- Hide OnlyOffice branding; match light/dark to suite theme

### Step B — Custom chrome wrapper (if theme insufficient)
Leverage [`drive-dump`](file:///Users/red/workdev/drive-dump) research:
- [`docs/CHROME-SPEC.md`](file:///Users/red/workdev/drive-dump/docs/CHROME-SPEC.md) — titlebar, menubar, toolbar DOM/CSS specs
- [`web/components/chrome/`](file:///Users/red/workdev/drive-dump/web/components/chrome) — React chrome components
- Build suite-owned titlebar (doc title, share, star, account) **above** OnlyOffice iframe
- Pass `customization.layout` to hide OnlyOffice's own header/toolbar
- Separate chrome per app type: Docs, Sheets, Slides (toolbar sets differ)

### Step C — Fork (last resort)
Only if theme + wrapper cannot reach Google parity on toolbars/menus. Fork OnlyOffice Document Server frontend (AGPL) — high maintenance cost. **Defer until A+B evaluated.**

Per [`drive-dump/docs/EDITOR-CORE-DECISION.md`](file:///Users/red/workdev/drive-dump/docs/EDITOR-CORE-DECISION.md): do **not** rebuild the editing engine — OnlyOffice remains the core; only chrome is customized.

---

## Phase 5 — Suite integration

### 5.1 Cross-app navigation
- [`header-account-actions.tsx`](file:///Users/red/workdev/gmail-interface-clone/components/gmail/header-account-actions.tsx): add `href: "/drive"` to UltiDrive tile
- Reciprocal launcher in drive-suite header back to `/mail`, Agenda, etc.
- Shared suite favicon/branding via `pnpm run brand:authentik` pattern

### 5.2 Mail ↔ Drive
- **Mail UI** (gmail-interface-clone): compose attachment picker "Insert from UltiDrive"; "Save attachment to Drive" on received mail
- **API** (ulti-backend): mail attachment upload → `POST /drive/files/...` in ultid

### 5.3 Deploy wiring (ulti-backend)

All routing and compose changes live in **ulti-backend** [`deploy/`](file:///Users/red/workdev/ulti-backend/deploy). The drive-suite repo supplies the Docker image / build context only.

Update [`deploy/nginx/default.conf.template`](file:///Users/red/workdev/ulti-backend/deploy/nginx/default.conf.template):

```nginx
location /drive/ {
    proxy_pass http://drive-suite:3000/;
    # WebSocket + standard headers
}
```

Add a `drive-suite` service to ulti-backend compose (port 3000 internal; dev: build from `../drive-suite` or host :3001). Mail frontend is deployed the same way today — gmail-interface-clone is not part of ulti-backend source tree, but its container is orchestrated from ulti-backend deploy.

Update Authentik blueprints / OIDC redirect URIs in ulti-backend for `/drive/api/auth/callback`.

---

## Phase 6 — Advanced (post-MVP)

| Feature | Approach |
|---------|----------|
| Full-text search | Meilisearch indexer on NC files + `/drive/search` |
| Version history | NC versions API wrapper |
| Shared drives / team folders | NC `groupfolders` app + API |
| Public link sharing page | `/drive/shared/{token}` read-only view |
| Desktop mount | rclone sidecar in Tauri (per ultidrive.md) |
| Offline uploads | IDB queue pattern from mail offline-queue |
| Comments on files | OnlyOffice comments (built-in) or NC comments API |

---

## Suggested delivery order

```mermaid
gantt
    title UltiDrive delivery phases
    dateFormat YYYY-MM-DD
    section ulti backend
    API and provisioning     :a1, 2026-06-02, 14d
    OnlyOffice deploy        :a2, after a1, 7d
    section drive suite
    Scaffold                 :a3, 2026-06-02, 7d
    Drive MVP                :a4, after a3, 21d
    Editor embed             :a5, after a2, 14d
    Google chrome            :a6, after a5, 21d
    section integration
    Suite wiring             :a7, after a4, 7d
```

Parallel tracks: **a1→a2** (ulti-backend) runs alongside **a3→a4** (drive-suite); **a5** starts after OnlyOffice (**a2**); **a7** (suite wiring) after Drive MVP (**a4**).

**First shippable milestone (4–5 weeks):** Phase 0 + 1 + 2 + 5 nginx/auth — full Drive file browser on real backend.

**Second milestone (+3 weeks):** Phase 3 — open and edit docs/sheets/slides in OnlyOffice.

**Third milestone (+3 weeks):** Phase 4 — Google-like editor chrome.

---

## Key files to create/modify

| Repo | Role | Files |
|------|------|-------|
| **ulti-backend** | Backend + all infra | `internal/api/drive/*`, `internal/api/office/*`, `deploy/onlyoffice/*`, `deploy/nginx/*`, compose, Authentik blueprints |
| **drive-suite** | Drive frontend only | Entire Next.js app; copy auth + API client patterns from gmail-interface-clone |
| **gmail-interface-clone** | Mail frontend only | `header-account-actions.tsx`, compose drive picker (UI links to `/drive`) |
| **Reference only** | — | `ultimail/.../drive/*`, `drive-dump/docs/CHROME-SPEC.md` |

---

## Risks and mitigations

| Risk | Mitigation |
|------|------------|
| NC user ID mismatch | Fix EnsurePrincipal in Phase 0 before any UI work |
| OnlyOffice JWT / NC connector misconfig | Test with single doc open before building chrome |
| Chunk upload edge cases | Reuse existing `X-Upload-*` protocol; add integration tests |
| Google UI parity scope creep | Theme first; wrapper second; fork only as last resort |
| Path-based routing (`/drive`) asset breaks | `basePath` in next.config from day one |
