Media uploader — drop & attach any file
The editor handles every file type, not just images. The same paths exist on desktop, mobile, and the web build target — they only differ in where the asset is stored at the end.
Entry points
There are four ways to land a file in the workspace and get a Markdown reference inserted at the cursor:
Drag & drop — drop one or more files onto the editor pane. Images become
. Anything else becomes[<filename>](../assets/<file>). Plain-text files (.txt,.md) keep their existing behavior of opening as a new note instead.Attach File toolbar button — the paper-clip icon in the editor toolbar opens the OS native file picker. Multi-select is supported and the chosen files are inserted in selection order.
Clipboard paste — copy a file from the OS file manager (Finder / Explorer / Files) and paste into the editor. See the engine caveat below.
Keyboard shortcut —
Mod+Shift+U(Cmd+Shift+Uon macOS,Ctrl+Shift+Uelsewhere). The shortcut also has a command-palette entry under "Attach file…".
Supported file types and size cap
Every file type is allowed. The 25 MiB cap (MAX_ASSET_SIZE) applies uniformly to every entry point. Files over the cap are rejected with a toast that mentions both the filename and the limit.
There is no magic-byte MIME validation — the type field is set from the extension. Same-name uploads in the same workspace get a numeric suffix (screenshot.png → screenshot-1.png) so existing files are never silently overwritten.
Where assets live
| Build target | Storage |
|---|---|
| Desktop (Tauri) | Encrypted cloud asset surface once the workspace is armed; native import first reads the selected local source file |
| Web (cloud) | Encrypted R2 object at assets-e2ee/<workspaceId>/<random-object-id> plus an opaque D1 name token |
The asset path written into your Markdown is always . — the same shape regardless of build target. The bridge layer (@takazudo/backend-bridge) hides where the bytes actually go.
How non-image assets render
The Markdown preview pane renders <a> tags whose href starts with . as a small file chip: a document icon, the filename, and a download cursor. Clicking the chip downloads the file via assets.readFile → blob URL → programmatic <a download> click → blob URL revoke. If the read fails the chip falls back to a plain styled link.
The Assets manager dialog (open from the toolbar) shows images with thumbnails and non-images with a document icon in the same square frame. Click an image to enlarge in the gallery overlay; click a non-image to download it. Non-image entries are never read at list time — the icon renders from kind alone — so opening the dialog in cloud mode does not download every R2 object.
Cloud-mode encryption contract
The bridge.assets.* API deliberately remains a plaintext application contract: callers pass normal filenames and base64 file data and receive normal filenames, metadata, and base64 file data. In a cloud workspace, the shared backend bridge encrypts both pieces before the request. The filename uses deterministic AES-256-GCM so lookup and collision handling can use one stable opaque token; the bytes use randomized AES-256-GCM plus HMAC authentication. The server stores only the token, encrypted envelope size, upload time, and a random per-upload R2 key. It neither receives nor derives the MIME type; the client derives kind and MIME from the decrypted extension.
This is zero-knowledge for contents and names, but it is not traffic-analysis resistant. Within a workspace, deterministic name tokens expose filename equality and their encoded length tracks the plaintext filename's UTF-8 byte length. The server also sees encrypted object size (and therefore plaintext size plus fixed envelope overhead), upload timing, and list/read/delete access patterns. The deterministic 96-bit IV inherited from encrypted path handling has a theoretical nonce-collision risk at extreme per-workspace namespace sizes; the bounded filename namespace makes that risk negligible, not impossible.
Every request also requires authentication, workspace ownership/binding, and the method's asset scope when a PAT is used. Asset mutations do not require an active subscription; creation remains subject to storage quota. Base64 remains the transport encoding for compatibility with the unchanged bridge API; replacing its roughly 33% wire expansion with a streaming/binary transport is a follow-up, not part of the encryption contract.
Clipboard paste — engine caveat
Pasting a non-image file from the OS file manager works on macOS / WebKit (both the real Tauri runtime and Safari). Coverage is patchier on Linux Chrome and varies by source application. The handler is defensive: if the clipboard item reports kind === "file" but getAsFile() returns null, the item is skipped silently. Image clipboard paste is unaffected and works everywhere the platform delivers an image item.
Mobile (iOS)
The mobile write toolbar exposes two picker buttons by default: the photo / camera entry (accept="image/*", unchanged from the prior release) and a generic file picker (accept="*/*") for documents and any other type. Both routes round-trip through bridge.assets.saveFile — the previous mobile behavior of inlining the picked file as a giant data: URL in the Markdown body is gone. A future on-device verification can collapse the two buttons into one if accept="*/*" is confirmed to preserve the photo / camera UX in the iOS Safari WebView; the safer two-button layout ships today.