Uploading, downloading and describing files with Atomic Data
The Atomic Data model (Atomic Schema) is great for describing structured data, but for many types of existing data, we already have a different way to represent them: files. In Atomic Data, files have two URLs. One describes the file and its metadata, and the other is a URL that downloads the file. This allows us to present a better view when a user wants to take a look at some file, and learn about its context before downloading it.
The File class
url: https://atomicdata.dev/classes/File
Files always have a downloadURL.
They often also have a filename, a filesize, a checksum, a mimetype, and an internal ID (more on that later).
They also often have a parent, which can be used to set permissions / rights.
If the file is an image they will also get an imageWidth and imageHeight property.
Uploading a file
In atomic-server, a /upload endpoint exists for uploading a file.
- Decide where you want to add the file in the hierarchy of your server. You can add a file to any resource - your file will refer to this resource as its
parent. Make sure you havewriterights on this parent. - Use that parent to add a query parameter to the server’s
/uploadendpoint, e.g./upload?parent=https%3A%2F%2Fatomicdata.dev%2Ffiles. - Send an HTTP
POSTrequest to the server’s/uploadendpoint containingmulti-part-form-data. You can upload multiple files in one request. Add authentication headers, and sign the HTTP request with the - The server will check your authentication headers, your permissions, and will persist your uploaded file(s). It will now create File resources.
- The server will reply with an array of created Atomic Data Files
Downloading a file
Simply send an HTTP GET request to the File’s download-url (make sure to authenticate this request).
Image compression
AtomicServer can automatically generate compressed versions of images in modern image formats (WebP, AVIF). To do this add one or more of the following query parameters to the download URL:
| Query parameter | Description |
|---|---|
| f | The format of the image. Can be webp or avif. |
| q | The quality used to encode the image. Can be a number between 0 and 100. (Only works when f is set to webp or avif). Default is 75 |
| w | The width of the image. Height will be scaled based on the width to keep the right aspect-ratio |
Example: https://atomicdata.dev/download/files/af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262?f=avif&q=60&w=500 (the path segment is the file’s BLAKE3 hash).
Storage model: content-addressed blobs
Files are stored using a content-addressed model. Every file’s bytes are hashed with BLAKE3, and the bytes are stored under that hash in a key-value blob store. The hash is the blob’s identity — its canonical form is a DID:
did:ad:blob:{blake3}
where {blake3} is the 32-byte BLAKE3 hash, hex-encoded. See Blob identifiers for the full identifier definition.
This separates the file’s metadata (the File resource — filename, mimetype, parent, ACL) from its data (the bytes), and lets the same blob be referenced by any number of File resources without duplication. The File resource points at its blob via a blob property whose value is a did:ad:blob: reference. Bytes flow over the peer-to-peer sync protocol independently of the resource graph: a peer that receives a File resource looks up the blob locally, and if it doesn’t have the bytes, asks any connected peer for them.
The HTTP form <origin>/download/files/{blake3} is a deployment-specific alias for the underlying DID and remains the URL clients use over plain HTTP.
Authorization model: hashes are bearer capabilities
The blob store has no permission system of its own. Knowing a did:ad:blob: identifier is the capability to retrieve the bytes — there is no second authorization check inside the blob store, and there does not need to be. The reasoning:
- A 256-bit BLAKE3 hash is unforgeable. You cannot guess one.
- The only ways to obtain a blob DID are: you already had the bytes (and computed the hash yourself), or you read the File resource that referenced it.
- Reading a File resource passes through the normal resource-level hierarchy authorization. That check is where access control happens — the bytes simply follow.
In other words, the auth boundary is the File resource, not the blob. Once a client legitimately holds a blob DID, they can fetch the corresponding bytes from any peer that happens to have them. This is the same model used by Git objects, IPFS CIDs, S3 presigned URLs, and Iroh tickets.
Three properties follow from this and should not be tangled up later:
- Blobs are facts, not resources. They have no subject metadata, no parent, no ACL, no class. They are addressed only by content hash.
- The File resource is where read permission is enforced. Any client that can read the File resource can read its bytes.
- Blob DIDs are bearer tokens. Treat a leaked
did:ad:blob:the same as a leaked file — equivalent to leaking an S3 presigned URL.
A note on existence side-channels
A consequence of content-addressed storage is that an attacker who already knows the BLAKE3 hash of some specific byte-string (for example, by hashing a publicly-leaked document) can ask a server “do you have this blob?” and learn the answer from the response. This is intrinsic to any CAS system and is generally accepted; mitigations like rate-limiting unauthenticated blob fetches are orthogonal to the capability model and can be applied at the deployment layer if needed.