Production-readiness roadmap
Kutup is pre-production: there is no public release yet (until the first
v* / desktop-v* git tag). This document is the canonical list of everything
between today and “ready to tag v1”.
It is the bridge between docs/ (current state, authoritative) and
docs/research/ (forward-looking design notes that may never ship). The blocker
and follow-up sections are remaining work; the Chat phase table is retained as a
gate record and explicitly labels completed slices.
When a feature lands, move it out of this file and update the appropriate docs/*.md. The roadmap should always describe the gap to v1, not the past.
What “production-ready” means for kutup
Section titled “What “production-ready” means for kutup”The bar for the first v* tag:
- No silent stubs in admin-facing UI. Every clickable action that exists in the UI must work end-to-end. No “wire-up pending” toasts in shipped builds.
- Deletion is recoverable. ✅ Shipped: owner-scoped trash with restore + permanent delete, and an hourly retention sweeper (
TRASH_RETENTION_DAYS, default 30). Seedocs/api.md→ Trash. - Self-hosters can recover broken users without SSH. ✅ Shipped: force-disable 2FA (lost authenticator), re-enable account (accidental disable), rotate temp password (first-login accounts), and the destructive wipe for users who lost both password and recovery phrase — all from the responsive web admin UI.
docs/research/10-admin-password-reset.mdrecords why “reset password” is two actions under E2EE. - Builds are signed. Unsigned binaries trigger macOS Gatekeeper and Windows SmartScreen warnings that look like malware to non-technical users.
- Admin actions leave an audit trail. ✅ Shipped: every mutating admin endpoint writes an
admin_audit_logrow;GET /admin/activityserves the feed and the Recent-activity cards render it in both responsive web layouts. Seedocs/api.md→ Admin. - Basic abuse protection. ✅ Shipped: per-IP limits on login/preflight/register/recovery/federation/admin (env-overridable
RATE_LIMIT_*), per-account login lockout (LOGIN_LOCKOUT_*), per-token TOTP blocking, and proxy-aware client-IP resolution (X-Real-IP). Seedocs/self-hosting.md. - Documentation tracks reality. ✅ Shipped: full docs sweep against the shipped code (stale Go-stack references scrubbed, env vars + endpoints verified), and every HTTP operation is annotated with
#[utoipa::path]soGET /api-docs/openapi.jsonlists the complete API (a coverage test inopenapi.rskeeps it honest). Interactive Swagger UI remains deferred (see below).
Items below are organized by whether they block v1 vs. whether they can ship in a subsequent release.
Blockers for v1 (must-have)
Section titled “Blockers for v1 (must-have)”V1 cryptographic and identity cutover
Section titled “V1 cryptographic and identity cutover”Kutup is still preproduction, so the first stable tag must freeze the clean
format rather than preserve development-only ciphertexts or trust machinery.
This destructive-change permission expires at the first stable v* tag;
afterward docs/crypto-agility.md requires versioned readers, authenticated
migrations, peer capability windows and no silent downgrade.
The normative checklist is
docs/draft/v1/security-review-follow-ups.md.
The following are release blockers:
- one parameterized Argon2id root with HKDF-separated KEK/login keys, a derived recovery-auth proof that never exposes recovery entropy, and suite-bearing account envelopes;
- canonical Rust
kutup-cryptoused by the browser through WASM, subject only to the documented per-operation 10× primitive-adapter exception; - one account-signed
AccountManifestV1, complete history, durable TOFU/QR pins, explicit account-incarnation reset, and removal of the global transparency log/checkpoint/proof/monitor stack; - typed context-bound XChaCha Drive/profile/collaboration envelopes, authenticated X25519-HPKE named shares and owner-authenticated collection epochs;
- an administrator-controlled 1–10 active-device limit (default and hard cap 10), enforced identically by every Chat and identity path;
- Signal-class web-device continuity: active-installation review, editable account-private installation labels, safe revocation, and always-on account-local E2EE Chat backup are implemented. Numeric device IDs remain immutable protocol-routing metadata. Every durable display mutation enters a crash-safe encrypted outbox; the homeserver stores an opaque signed base-plus-tail restore point and separately encrypted media under an administrator-controlled Chat quota. A recovered browser uses the existing Drive recovery setup and restores from the server; device-to-device history transfer is not supported. Missing or invalid backup state produces an explicit history-loss warning rather than a silent empty inbox;
- MLS suite
0x0003and real 256-account/2,560-leaf group gates; and - confidential broadcast for 1,000,000 accounts and up to 10,000,000 device grants using the separate LKH plus small-MLS-control-group design.
The format inventory and threat models are
docs/v1-format-inventory.md,
docs/drive-security-threat-model.md, and
docs/broadcast-security-threat-model.md.
The exact third-party ownership boundary is
docs/cryptographic-dependencies.md.
Signed builds
Section titled “Signed builds”Desktop release builds are currently unsigned. macOS Gatekeeper and Windows
SmartScreen treat unsigned .dmg / .msi as untrusted; non-technical users
see scary warnings.
| What’s needed | Where |
|---|---|
| Apple Developer ID for macOS signing + notarization | external — requires Apple Developer Program ($99/yr) |
| Microsoft Authenticode certificate for Windows | external — DigiCert / Sectigo (~$300/yr) |
.github/workflows/release-desktop.yml — accept signing secrets, run codesign (mac) + signtool (win) |
repo |
| Native iOS signing, TestFlight/App Store Connect, entitlements, and opaque production icons | external + sibling kutup-ios repository |
| Native Android upload key, Play App Signing/Console, and release metadata | external + sibling kutup-android repository |
Documentation: docs/release-signing.md covering how to rotate keys |
new doc |
Important (should-have, can ship after v1)
Section titled “Important (should-have, can ship after v1)”These aren’t blockers — kutup can release without them — but they’re real production gaps and should land in v1.1 or shortly after.
SMTP integration
Section titled “SMTP integration”Without SMTP, kutup can’t:
- Send welcome emails (we cut the “Send welcome email” toggle from the admin create-user dialog because there’s no flow)
- Send password-reset links (admins currently share temp passwords out-of-band)
- Send share notifications (“Alice shared a folder with you”)
| What’s needed | Where |
|---|---|
Backend: SMTP client + env-var config (SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS, SMTP_FROM) |
crates/kutup-server/src/email.rs (new) |
| Backend: template system for welcome / reset / share emails (HTML + plaintext) | crates/kutup-server/templates/email/ |
Frontend: re-enable the “Send welcome email” toggle in AdminCreateUserDialog (currently dropped) |
frontend/src/components/admin/AdminCreateUserDialog.tsx |
Documentation: docs/email.md setup guide |
new |
Admin · System status endpoint
Section titled “Admin · System status endpoint”The desktop Admin Overview’s System card is hidden today because the backend doesn’t expose uptime, TLS expiry, or the public URL. Useful for self-hosters at a glance.
| What’s needed | Where |
|---|---|
Backend: GET /admin/system returning { uptime, tlsExpiry, publicURL, version } |
new handler |
| Backend: track process start time, parse cert expiry from TLS config | service-level |
Frontend: unhide the System card on AdminOverviewTab |
frontend/src/components/admin/AdminOverviewTab.tsx |
Admin · Required-2FA + Drive-default-quota + trash-retention settings
Section titled “Admin · Required-2FA + Drive-default-quota + trash-retention settings”/admin/settings now exposes registration, the dedicated default Chat quota,
and mailbox/temporary-media retention in desktop-width and mobile-width web
layouts. Server-driven
required 2FA, a default Drive/general quota, and runtime trash retention remain
unimplemented; their cards stay hidden.
| What’s needed | Where |
|---|---|
Backend: extend admin_settings JSON with require_2fa_users, require_2fa_admins, default_storage_quota_bytes, and trash_retention_days |
crates/kutup-server/src/handlers/admin.rs |
Backend: enforce require_2fa_users on next sign-in (force-set TOTP within N days or block) |
crates/kutup-server/src/handlers/auth.rs |
Backend: apply default_storage_quota_bytes when creating new users |
crates/kutup-server/src/handlers/admin.rs |
| Frontend: add the remaining controls without disturbing the shipped Chat-storage controls | frontend/src/components/admin/AdminSettingsTab.tsx, mobile equivalent |
Admin · Danger-zone actions
Section titled “Admin · Danger-zone actions”The design has “Re-index search” and “Purge soft-deleted files now” in a Settings → Danger zone card. Both hidden today.
| What’s needed | Where |
|---|---|
Backend: POST /admin/actions/reindex-search (kicks off the encrypted-search reindex) |
new |
Backend: POST /admin/actions/purge-trash (forces the trash retention sweeper — jobs::trash_sweep_once — to run now) |
new |
| Frontend: unhide the danger zone card | both admin Settings tabs |
Native iOS and Android apps
Section titled “Native iOS and Android apps”The dedicated kutup-ios and kutup-android apps are active work in sibling
repositories and are not release-ready. The implemented kutup-client-ffi
boundary is only one dependency of those products; native UI integration,
platform lifecycle, secure storage, complete Direct/MLS/media/backup parity,
packaging, signing, store metadata, and real-device acceptance remain gated in
their own plans. See mobile-build.md and
chat-native-bindings.md.
The Tauri shell’s retained mobile targets are experimental. In that path iOS Keychain works, while Android still lacks a keyring backend and re-authenticates after restart. Work on that wrapper must not be reported as completion of the dedicated native apps.
Responsive web · mobile selection mode
Section titled “Responsive web · mobile selection mode”Per the design + user direction: long-press / “Select” button on mobile turns the page into Google-Drive-style full-screen takeover with checkboxes, top “Cancel · N selected · Select all” bar, bottom action bar (Share / Move / Delete / More).
Desktop selection is explicitly carved out — kutup keeps its existing no-layout-shift selection pattern there.
| What’s needed | Where |
|---|---|
| Frontend: selection state in MobileShell or a context | frontend/src/components/mobile/ |
| Frontend: row checkboxes on FolderTile + FileListRow when selection mode is on | mobile components |
| Frontend: replace MobileBottomNav with a selection action bar while active | shell |
| Frontend: replace MobilePageHeader with the selection top bar while active | shell |
Responsive web · optional file filters, view modes, and gestures
Section titled “Responsive web · optional file filters, view modes, and gestures”The current Files page intentionally removed the old non-functional category chips and list/grid toggle. Add them only with complete filtering, persistence, keyboard, and touch behavior.
| What’s needed | Where |
|---|---|
| Frontend: wire chip filters to filter the rendered items | frontend/src/pages/mobile/MobileFilesPage.tsx |
| Frontend: List vs Grid toggle with localStorage persistence | same |
| Frontend: iOS-style swipe-to-share / swipe-to-delete on file rows | new |
Responsive web · page transitions
Section titled “Responsive web · page transitions”Sub-pages currently appear / disappear instantly. iOS users notice the missing slide-in / slide-out.
| What’s needed | Where |
|---|---|
Frontend: a thin <RouteTransition /> wrapper that animates push (left → right) and pop (right → left) |
frontend/src/components/mobile/ |
Responsive web · recently shared by me
Section titled “Responsive web · recently shared by me”The Shared tab has an empty hero for this section. Data is derivable from the shares table; just needs wiring.
Responsive web · viewer touch tweaks
Section titled “Responsive web · viewer touch tweaks”Excalidraw / photo / PDF viewers work on mobile but some tap targets are desktop-sized + the top status bar overlaps content in places. Carve-out: the viewers themselves are NOT redesigned (user said they’re “clean and useful”). Just touch + safe-area tweaks.
Native mobile · push notifications
Section titled “Native mobile · push notifications”Native iOS/Android notifications for shared-file and Chat events. Not v1.
| What’s needed | Where |
|---|---|
| Apple Push Notification Service setup | external |
| Backend: APNS sender + per-user device-token registry | new |
| Native clients: APNS/FCM registration, permission, and presentation | sibling iOS/Android repositories |
Responsive/native mobile · recovery-phrase management
Section titled “Responsive/native mobile · recovery-phrase management”The mobile Encryption Keys page explains the recovery boundary and links to account recovery; it does not claim that an authenticated session can display or rotate the stored phrase. A future phrase-rotation product needs a separate security design, backend contract, and word-by-word confirmation flow.
Backup / restore CLI
Section titled “Backup / restore CLI”Self-hosters need an easy way to back up + restore the full encrypted dataset (DB + S3 blobs). The Rust CLI exists (crates/kutup-cli); adding kutup backup / kutup restore subcommands is mostly tooling around pg_dump + mc mirror.
V1 major track · Federated E2EE chat (“ileti”)
Section titled “V1 major track · Federated E2EE chat (“ileti”)”The shipped web track provides libsignal Direct Chat/Note to Self and OpenMLS private groups with encrypted media over the same application edge and authenticated federation stack as Drive. Chat objects use the same SeaweedFS infrastructure but separate formats, references, keys, and a dedicated quota; they are not Drive files. The current UI is part of the main responsive web app, not a required second domain. Voice/video calls remain a later phase.
The full architecture is captured in docs/research/11-federated-chat.md (libsignal v0.97.2 study, Matrix take-vs-leave, single-443 topology, risks), the wire-contract fixes in docs/research/12-chat-improvements-for-clients.md, and — decisively — the adversarially-verified comparative study docs/research/13-chat-architecture-comparative-research.md (Signal/Matrix/XMPP + local libsignal/Prosody/ejabberd/Monal code). Direction is committed and validated. Locked decisions: libsignal-protocol as a pinned wrapped dependency (AGPL-compatible, never reimplement the ratchet); transport-only federation (signed s2s over 443 + .well-known, no Matrix-style replicated room state — the DAG is CVE-confirmed as the mistake); PQ (PQXDH + SPQR) always-on with a versioned suite registry, algorithm agility as a protocol mechanism not a user downgrade toggle.
The normative wire contract the three clients freeze against is docs/chat-protocol.md (v1) — it consolidates the wire-affecting decisions from 11-/12-/13- into one spec, tagging every field [IMPL] (phase-2 server, frozen), [ADD] (additive, phase-2b), or [RSV] (reserved now, implemented later so it’s not a breaking migration). Implement against that.
Current group decision: the earlier GV2/sender-key proposal is superseded by
the RFC 9420 MLS architecture in chat-mls.md. Direct Chat and
Note to Self remain on pinned libsignal; private groups and the small broadcast
administrator control group use OpenMLS. Groups use owner-approved,
dynamically replaceable multi-server ordering authorities rather than a
permanent homeserver. Account/device authenticity is a V1 requirement.
Sealed sender remains all-or-nothing with no identified fallback, and all
federation continues to use the common authenticated transport rather than a
room DAG.
Phases (each lands as its own PR-series; the table order is the current delivery priority, while the stable phase labels keep existing specifications and test names unambiguous):
| # | Slice | Gate |
|---|---|---|
| 1 | Spike: libsignal-protocol + spqr on wasm32 |
✅ GO (2026-07-12, spikes/libsignal-wasm/) — compiles for the browser target on stable, full PQXDH+Triple-Ratchet round-trip executes in wasm; web client shares kutup-chat-core |
| 2 | Server slice: kutup-chat-proto + prekey directory, per-device mailboxes, WSS drain |
✅ landed — crates/kutup-chat-proto, migration 021, handlers/chat.rs, chat_hub.rs, nginx /api/chat/ws; full REST + WS contract smoke-verified against the live stack (incl. one-time-prekey consumption, last-resort fallback, the 409 missing/stale/extra device contract, live envelope push). Playwright chat spec lands with phase 2b |
| 2b | Shared core + minimal 1:1 reference web UI | Implemented and live-stack verified. Includes durable typed inbound journal/quarantine, SQLCipher/IndexedDB stores, crash-safe registration/prekeys, signed manifests, WASM transport, Web Locks, REST+WS reconciliation, history, Note to Self, and ordinary linked-device sent transcripts. Web remains the product client until the messaging milestone is complete; native packaging/integration is not a gate. |
| 3 | Web federation foundation | Implemented and two-server live verified: canonical username@server, typed conversations, one persistent v2 server identity, signed .well-known endpoint/capability discovery, immutable identity history and authenticated rotation, strict RFC 9421/9530 request/response authentication, replay reservation, DNS-rebinding/SSRF-safe resolution, durable per-destination in-order Chat delivery, device-mismatch recovery, terminal rejection, and sequence-gap replay. Drive now uses that same stack for signed account lookup, domain-bound fragment capabilities, invite acceptance, file lists, idempotent upload/delete, persisted ciphertext digests, and verify-before-release streaming downloads. The isolated harness proves the Drive round trip, that Chat reuses a Drive-established pin, and that Chat retry survives an origin restart while the destination is offline. The generic responsive admin control plane provides a global stop, feature-scoped disabled/allowlist/blocklist/open admission and trust floors, directional domain rules, shared Chat/Drive diagnostics, peer search/trust filters, retry-one/retry-visible workflows, TOFU verification, exact immutable quarantine/history evidence, break-glass re-pin, and filtered audit presentation/CSV export. A disabled feature is omitted from discovery while the other remains available. Both old feature-specific federation stacks and raw remote URL routes were removed; there is no v1 downgrade. No alias namespace. See docs/federation-protocol.md. |
| 4 | Web contact privacy and trust | Implemented and two-server verified. Account-signed complete manifest history, durable gray-TOFU/green-QR/red-quarantine state, explicit account-incarnation replacement, message requests/blocking, contacts-only libsignal sealed sender, offline-root/online-certificate policy, database-backed abuse limits, anonymous local/federated delivery, capability rotation on block, no identified fallback, and the shared typed XChaCha profile envelope are restart- and two-server-browser verified. |
| 5 | MLS private groups | V1 binding implemented, activated and two-server verified. Durable OpenMLS state, manifest-bound devices, private role/control state, quorum-certified multi-authority ordering, owner-approved governance/recovery, destination-private delivery, invitation consent, linked-device sync, restart reconciliation and exact group security inspection use RFC 9420 suite 0x0003 with X25519/ChaCha/Ed25519. The native scale gate operates an actual 256-account × 10-device OpenMLS tree (2,560 independent leaves); the same Rust core builds for WASM, while browser lifecycle, adversarial replay/enumeration, restart/federation and destination-metadata gates pass. Direct and Note to Self remain libsignal. See docs/chat-mls.md. |
| 6 | Web messaging and media | The currently scoped Phase 6 messaging/media slices are implemented: Chat-media is advertised and clean two-server verified; encrypted replies, reactions, author-only edits/deletions, delivery/read receipts, ephemeral typing indicators, disappearing messages, private local search, platform photo/video capture and voice-note recording are present. Direct, Note-to-Self and MLS attachments use one immutable Rust/WASM-secretstream object, resumable tus upload, sender-free durable federation copies, a dedicated administrator-controlled Chat quota, manual streaming download, clearable encrypted per-conversation accounting and continuous E2EE history/media backup. Browser camera capture uses the browser/OS permission UI and feeds the resulting file into that same encrypted path without a second format, endpoint or plaintext fallback. Voice recording is browser-permission-owned, bounded to 10 minutes and 64 MiB (or the lower server limit), stops every microphone track on all terminal paths, authenticates its duration in the E2EE descriptor and uses the same immutable media path. Replies bind a canonical logical message UUID only inside the shared E2EE content and survive server-hosted backup restore. Reactions are bounded encrypted set/remove operations reduced as one deterministic latest reaction per account without server-visible metadata. Edits use deterministic author-authenticated replacement operations; irreversible deletion tombstones prevent stale-device resurrection. Batched receipt targets and state remain E2EE; read receipts are browser-local opt-in and MLS views aggregate accounts. Typing controls are E2EE, accepted only for established conversations, excluded from product history/transcripts, locally expiring and burst-throttled to limit MLS one-time KeyPackage pressure. Disappearing timers are hidden E2EE controls; every affected text/attachment authenticates its own 30-second-to-30-day duration, senders count from durable creation, and recipients count from first actual view with the earliest absolute start synchronized privately across their linked devices. Durable plaintext and derived controls are atomically purged, unsaved media references are released, and backup restore or browser replacement cannot restart expiry. Local search scans only the decrypted visible browser history, applies edits/deletes/expiry before matching, and never emits a query or plaintext index to a server. The Chat quota defaults to 2 GiB per account and can be increased by administrators. See docs/chat-media.md and docs/chat-protocol.md. |
| 5b | Confidential broadcast | V1 blocker scheduled after Phase 6, not an oversized MLS group. A small MLS owner/admin control group authorizes publishers and the replaceable ordering authorities. A fixed-depth account-leaf LKH serves up to 1,000,000 subscribed accounts; each account access secret is independently wrapped to up to ten manifest devices (10,000,000 grants). Posts are encrypted once and pulled/cached by subscriber homeservers. Removal rekeys before the next post, owner removal performs a restart-safe full rebuild, and history policy is 0..=365 days (default 30) over daily one-way content epochs. See docs/broadcast-security-threat-model.md. |
| 7 | Web PWA completion | Generic content-free Web Push, offline/restart recovery, responsive/accessibility/browser matrix, security/load tests, and protocol freeze. |
| 8 | Calls | 1:1 WebRTC → SFU group calls; TURN + SNI demux on 443. Separate from the messaging-complete web milestone. |
| 9 | Native clients | Work in progress; not release-ready. Stabilize the UniFFI APIs, package XCFramework/AAR artifacts, complete Keychain/Keystore and lifecycle integration, reach Direct/MLS/media/backup parity, and pass real-device release gates in the sibling iOS/Android repositories. |
Device-list authenticity (the signed per-account device manifest) is not in phase 7 — it is a phase-2b/2 wire-contract requirement per the comparative study.
Device continuity is also a V1 blocker, not generic PWA polish. Browser installations are volatile: site-data eviction, private windows, profile replacement and manual storage clearing all create new cryptographic devices. V1 exposes active Chat installations and last activity, supports immediate revocation, and continuously protects display history in an account-local E2EE server backup. After unified recovery, a replacement browser restores the verified encrypted base and event tail, then establishes fresh Direct/MLS protocol state for new messages. The UI reports missing, invalid, offline, or quota-blocked backup state explicitly. Automated manual-fixture setup must not publish disposable headless devices unless it retains or revokes them.
Phase 6 attachment storage and download decision
Section titled “Phase 6 attachment storage and download decision”The normative protocol and threat model are
chat-media.md and
chat-media-security-threat-model.md.
Chat attachments reuse Kutup’s encrypted Drive/TUS and federation machinery; they do not introduce a second object-storage stack. The sender uploads an immutable encrypted attachment and retains a retry/outbox copy until every destination homeserver has durably accepted it. Each destination homeserver stores one opaque ciphertext copy, reference-counted for all of its local group members and devices, so a received attachment remains available if the sender or sender’s server later goes offline. Its logical bytes are reported as Chat media and count against the recipient’s dedicated Chat quota.
V1 uses an administrator-configured dedicated Chat quota, currently 2 GiB by
default, separate from the Drive/general quota. The Chat storage view splits
message history, ordinary delivery media, and protected history media; a future
Photos product receives its own namespace rather than classifying encrypted
MIME data. The same view also shows Chat-media usage per conversation (for
example, Family group — 842 MiB) and let the user review or clear that
conversation’s stored media. Clearing a recipient’s copy releases its quota
and affects that account’s linked devices, but never deletes another
participant’s copy. The Chat quota is administrator-controlled; there is no
user-adjustable media sub-budget in V1.
Per-conversation accounting must not weaken sealed-sender metadata privacy. The homeserver stores only the recipient, opaque attachment/reference IDs, storage namespace and bounded ciphertext byte counts; it does not persist a sender identity or sender-recipient/chat correlation. The client joins those opaque IDs to its E2EE message index and computes the named per-chat totals locally.
The attachment-reference index follows the client-derived encrypted-entity
pattern used by Ente for private user entities and derived file data. Kutup
defines a typed ChatAttachmentLedgerV1 payload and purpose-specific key, while
reusing the common Rust/WASM envelope, canonical encoding and parser machinery.
The homeserver persists only a suite-bearing encrypted entity, random opaque
entity ID, exact ciphertext size, monotonic account cursor/revision and
tombstone state. It cannot read the conversation ID, message ID, media kind or
display name. Clients fetch bounded cursor pages, decrypt and validate them,
then maintain a disposable local IndexedDB/SQLCipher projection for instant
per-chat totals. A recovered or newly linked device can rebuild that projection
from the encrypted remote ledger without trusting another device to remain
online.
Unlike an unauthenticated last-write-wins blob, ledger updates require exact previous revision/digest continuity and idempotent operation IDs so concurrent linked devices cannot silently overwrite one another. The server’s separate opaque byte-accounting rows remain authoritative for quota enforcement; the encrypted client index is authoritative only for private presentation and cleanup selection. This shares machinery with other future encrypted account state without sharing its key or typed payload.
V1 device download is always manual: receiving a message may persist its encrypted attachment at the homeserver, but no photo, audio, video or file is downloaded to a recipient device until the user taps Download. A message request carries only the encrypted attachment descriptor; its destination homeserver must not fetch or allocate storage for the blob before the recipient accepts the request. This prevents an unauthenticated sender from consuming the recipient’s storage quota.
The V1 protocol ceiling is a 2 GiB plaintext-class attachment plus the exact bounded overhead of its typed ciphertext framing. Every server defaults to that ceiling and may advertise a lower local limit, but cannot advertise a larger V1 limit. Admission reserves the exact ciphertext bytes before transfer and rejects an oversized object before object-storage mutation. The server applies one media-object limit because MIME type remains encrypted; client recording and preview code may use smaller type-specific limits.
Save to Drive decrypts and re-encrypts the attachment into a visible, recipient-owned Drive collection charged to Drive/general storage. Clearing the separate Chat copy releases its Chat charge. Names, captions, thumbnails, MIME details, capabilities and keys remain inside the E2EE message; servers handle only bounded opaque ciphertext and public size/accounting fields.
Post-V1 clients may add WhatsApp-style per-user automatic-download policy, separately for mobile data, Wi-Fi and roaming and separately for photos, audio, videos and files, with optional size limits. The settings synchronize between the user’s devices through encrypted Note-to-Self state. They control only device downloads and never weaken the homeserver’s durable-storage rule after an accepted delivery.
Platform · Advanced traffic-inspection protection [TODO, post-MLS v1]
Section titled “Platform · Advanced traffic-inspection protection [TODO, post-MLS v1]”Add an optional traffic-obfuscation layer for every connection to a Kutup server and every Kutup server-to-server connection. A shared protected transport should multiplex Chat, Drive, collaboration, authentication, policy, identity-manifest, and federation streams instead of creating feature-specific cover channels. Chat cells carry opaque MLS/anonymous-delivery envelopes; Drive and collaboration cells carry their existing encrypted requests, responses, and blob chunks.
This is distinct from sender-metadata minimization and zero-knowledge storage: fixed-size traffic alone does not hide operation counts, timing, origin domains, the local authenticated user from their own server, or the recipient known to its own server. Large Drive transfers also reveal approximate total size and duration unless bucket padding or correspondingly expensive cover traffic is enabled.
- Define authenticated, versioned traffic profiles. A required profile never silently falls back: unavailable protection queues the message and warns the user or pauses the protected operation; weaker transport requires an explicit user action.
- Keep Save mobile data enabled by default. In this mode use size-bucket padding, immediate delivery, opportunistic batching, normal foreground connections, and no dummy or constant-rate traffic.
- With Save mobile data disabled, use 1,024-byte application cells, encrypted bounded fragmentation/reassembly, persistent connections where the platform permits, multiplexed batches, dummy cells, and padded acknowledgements, errors, authentication exchanges, API responses, KeyPackages, MLS control traffic, Drive operations, collaboration updates, receipts, and typing events.
- Keep Hide message timing disabled by default. When enabled, replace scheduled dummy cells with real cells, use a controlled-rate scheduler and bounded delay presets, and batch, delay, or disable typing indicators and receipts. Urgent security control such as block/device removal may bypass artificial delay but must remain padded.
- Allow separate user choices for mobile data and Wi-Fi/Ethernet, synchronize them through the MLS Note-to-Self group, and show estimated data use.
- A user may unilaterally send with a stronger profile. Requiring a minimum profile needs both users for a direct chat and owner approval plus the group ordering quorum for a group.
- Servers advertise supported profiles. Administrators configure global and per-feature cell rates, cover-traffic bandwidth budgets, maximum artificial delay, padding buckets, large-transfer padding limits, and whether protection is offered before login. Server-to-server cover traffic must share one authenticated peer channel across features so its cost is amortized. Browser/OS background throttling must be surfaced as protection unavailable, never treated as permission to downgrade.
- Add strict fragment count, total-size, timeout, replay, deduplication, and reassembly-memory bounds. Dummy and real cells must be indistinguishable to the passive observer covered by the selected profile.
- Verify packet-size distributions, timing leakage, downgrade behavior, data budgets, login/bootstrap behavior, small and large Drive transfers, reconnects, background throttling, cross-feature federation batching, and adversarial fragment streams before advertising any profile.
Polish / smaller items (future)
Section titled “Polish / smaller items (future)”Files workspace follow-up
Section titled “Files workspace follow-up”The Polar Workspace redesign now provides the responsive Files header, folder-scoped search, creation menus, folder colors, sorting, selection, upload progress, drag/drop, contextual empty states, and right-side details inspector. Future work here is performance measurement for very large folders and optional filtering/view modes backed by real behavior.
Federation polish
Section titled “Federation polish”Cross-server presence indicators in collab, outgoing Drive-share revocation, and federation discovery UX. The common Chat/Drive trust and transport stack is implemented; these are product-lifecycle improvements above it.
Test coverage gaps
Section titled “Test coverage gaps”- Tauri session-persistence — no E2E test today
- Browser-level Drive federation UI coverage (the isolated two-server server harness already covers the complete Drive and Chat transport lifecycle)
- Responsive web has an automated phone/desktop axe and state-transition gate; physical-device browser coverage and dedicated native-app flows remain open
Performance baselines
Section titled “Performance baselines”docs/research/perf-baseline-2026-05-06.md is a single point. Continuous benchmarking (or even a manual quarterly pass) would catch regressions.
Tauri shell · real OnlyOffice / Office docs
Section titled “Tauri shell · real OnlyOffice / Office docs”Desktop OnlyOffice was stripped from the Tauri build to avoid the OOM on
tauri::generate_context!() (the ~2.6GB SDK gets embedded as a static byte
array). The same applies to mobile. Loading the SDK from
${serverUrl}/onlyoffice/… so the shell streams it from the user’s server
remains separate Tauri work.
Responsive web · federation share-with from sheet
Section titled “Responsive web · federation share-with from sheet”The mobile share sheet doesn’t yet expose federated share flows (cross-server) — only public link sharing.
CLI follow-ups (from the CLI improvements batch)
Section titled “CLI follow-ups (from the CLI improvements batch)”The .excalidraw whiteboard asset extraction/hydration deferral is done
(crates/kutup-cli/src/whiteboard.rs — upload extracts + re-snapshots,
download re-inlines; Go-CLI parity reached). What remains around the CLI:
- Share lifecycle management (needs server slices first). There is no
endpoint to list a collection’s outgoing user shares, revoke one, or
list/delete public links (the web UI can’t either — only recipient-side
DELETE /api/drive/federation/shares/:shareIdexists). Server work:GET /api/collections/:id/shares,DELETE /api/collections/:id/share/:userId,GET/DELETE /api/user/shares(public links, owner-scoped viapublic_shares.created_by); thenkutup share ls / revoke / unlinkand matching web UI. Until then the CLI ships no affordance (no stubs). - Server improvements that unlock better CLI behavior (noted per the
“do when we touch the server” decision):
latestVersionIdon theGET /collections/:id/filesrows (oneLEFT JOIN LATERAL) — kills the sync engine’s per-filelist_versionspolling (its remote-change signal;files.updated_atis never bumped).trashRetentionDaysinGET /api/auth/settings— letskutup trash lsshow an accurate EXPIRES column on any server config (currently omitted rather than hardcoding 30).
- Sync engine: whiteboard assets.
kutup syncpushes/pulls.excalidrawfiles as opaque bytes; the extract/hydrate steps only run inupload/download. Wirecrate::whiteboardinto the engine’s push/pull paths. - Streaming multipart uploads for remote
share upload— still buffers the whole encrypted file in memory (Part::bytes); switch toPart::readerwith an encrypting reader for large-file parity with tus. kutup versions restorevs collab snapshots. CLI restore re-encrypts with the file key in secretstream framing, while web collab snapshots are AEAD envelopes under a derived content key — CLI restore round-trips CLI/sync-created files, not live-collab documents. Needs the collab content-key path if full parity is wanted.kutup admincommand group. The full/api/admin/*surface (users, quotas, stats, activity, settings, 2FA reset, wipe) has no CLI coverage; useful for self-hosters. Deliberately deferred from the improvements batch.
Go→Rust server rewrite · interactive Swagger UI
Section titled “Go→Rust server rewrite · interactive Swagger UI”The Rust kutup-server (crates/kutup-server)
generates its OpenAPI spec with utoipa and serves the machine-readable document at
GET /api-docs/openapi.json. The Go server served an interactive Swagger UI at
/swagger/* (swaggo/fiber-swagger). That route is not yet restored in Rust: the
utoipa-swagger-ui crate downloads the Swagger UI bundle from GitHub in its build
script, which breaks offline/sandboxed builds (and the rule that the server compiles
offline). Restore it by vendoring the UI bundle (SWAGGER_UI_OVERWRITE_FOLDER or a
file:// SWAGGER_UI_DOWNLOAD_URL) so the build stays network-free, then mount it at
/swagger. The OpenAPI JSON is unaffected.
Research / open questions
Section titled “Research / open questions”These live in docs/research/ because the design hasn’t been chosen yet:
- WebDAV mount —
docs/research/06-webdav-support.md. Client-side proxy is the only viable path because server-side WebDAV breaks E2EE. Long-term work. - WebAuthn / passkey support — not yet captured in
docs/research/. Would supplement TOTP for second-factor. Useful research before adding. - Chat open questions — the post-RFC post-quantum MLS suite transition and advanced traffic-obfuscation profiles. Direct/MLS retention, continuous recovery, and the confidential-broadcast design are already specified or implemented; follow the current Chat documents rather than the July research baselines.
Collaborative text/Office/whiteboard editing and Drive-style version history
are shipped. Research notes 01–08 are retained as the design and debugging
record; they are not open implementation tracks.
Working with this file
Section titled “Working with this file”- When something lands, delete its entry and update the appropriate
docs/*.mdto describe the now-shipped behavior. - When you discover a new gap, add it here. Be specific: file paths, what endpoint, what the user-visible change is.
- Don’t add items that are pure ideas. Those belong in
docs/research/as exploratory notes. This file is for committed, scoped work. - The
Blockers for v1section is the gate to the firstv*tag. If you’re tempted to ship before everything there is done, push back — the user has explicitly asked for production-grade, not fast.