Documents, audio and everything else
Not everything you upload is a photo. Four paths, decided by MIME type, and the last one is the one people assume wrongly.
| MIME | path | what you get |
|---|---|---|
image/* |
ladder | WebP variants + palette + dimensions |
video/* |
background job | poster, MP4, HLS, optional AI proxy |
audio/* |
inline transcode | an MP3 variant plus the untouched original |
| anything else | passthrough | the bytes, stored and served as-is |
Voice notes, clips, tracks. Two things are written:
- The original, untouched, under the
originalvariant. - An
mp3variant — mono, ~96 kbps, tuned for speech.
The transcode exists because of a real interoperability problem: MediaRecorder
emits audio/webm;opus on Chrome/Android and audio/mp4/AAC on iOS, and each
one chokes somewhere the other works. MP3 is the one container and codec every
target browser decodes.
// Give raw bytes an explicit audio MIME, or they classify as `other`// and you get no mp3 variant.await nt.upload(bytes, { contentType: "audio/mpeg", fileName: "voice-note.mp3",});Legacy .m4a/.wav with no matching pipeline still fall back to original, so
the bytes are safe either way.
PDFs, spreadsheets, zips — the passthrough path
Section titled “PDFs, spreadsheets, zips — the passthrough path”A PDF, an .xlsx, a .csv, a .zip, a font, a .glb — anything that is not
image, video or audio takes the same branch:
- stored raw, byte-for-byte, under the
originalvariant key - served from the CDN with its real content type
- the row is
readyimmediately — there is no job to wait for - classified as
kind: "document"(PDF) orkind: "other"(everything else)
const res = await nt.upload(pdfFile, { fileName: "contract.pdf" });// → { ok: true, passthrough: true, assetId, url }What you do NOT get, and should not wait for
Section titled “What you do NOT get, and should not wait for”This is the part worth being blunt about:
- No thumbnail. No first-page render of a PDF, no spreadsheet preview.
- No text extraction. No search, no OCR, no page count.
- No transforms. A
/t/URL on a document is meaningless. - No virus scanning. The bytes are stored as received.
If you need any of that, you need a document pipeline, and this is not one. What this is good at is being the single addressed, deduplicated, CDN-backed place your files live — so a contract and the photos attached to it are the same kind of thing to your code.
Getting the extension right matters more here
Section titled “Getting the extension right matters more here”For images the stored extension rarely bites. For documents it always does,
because original is the only variant there is, and its extension comes from
the uploaded filename, not the MIME:
// Right: the URL will end in .pdfawait nt.upload(bytes, { fileName: "contract.pdf", contentType: "application/pdf" });
// Wrong: the URL ends in .bin and the browser will not know what to do with itawait nt.upload(bytes);Archiving a video as bytes
Section titled “Archiving a video as bytes”A useful trick, since video presets silently drop original: upload it under a
non-video filename and it takes the passthrough branch instead.
// Stored verbatim, no transcode, no HLS — a real backup of the source file.await nt.upload(clipBytes, { fileName: "master.mp4.bin" });You lose poster, playback variant and streaming — which is the point. Do this in addition to a normal video upload, not instead of it.
Deduplication applies to all of it
Section titled “Deduplication applies to all of it”Every path is content-addressed. Uploading the same PDF twice returns the same asset, and the second upload transfers nothing. That is also true across different filenames — the SHA is of the bytes, not the name.