Skip to content

Attachment storage

Attachment storage is optional. When you configure it, the backend publishes base_url, max_upload_bytes, and retention_seconds to clients. The backend signs one PUT request for each upload. Clients send the ciphertext to the storage target and fetch it from base_url. The backend does not store the ciphertext.

Applications use the SDK’s built-in attachment service to create a pending attachment, send its remote record, and upload the staged ciphertext. The service requests a signed PUT from the backend and sends the file directly to the target. The recipient explicitly requests a download; message receipt does not fetch the file. See Native attachment uploads and downloads for Kotlin and Swift examples, retry behavior, and local file management.

The published upload limit bounds the encrypted envelope, including its MIME type, filename, framing, and authentication tag. A source file exactly at the limit can exceed it after encryption. Target retention controls how long a remote file remains available. It does not remove the plaintext copies that clients already downloaded.

[attachments]
base_url = "https://files.example.com/attachments"
max_upload_bytes = 10485760
retention_seconds = 2592000
[attachments.target.S3]
endpoint = "https://s3.example.com"
region = "us-east-1"
bucket = "attachments"
key_prefix = ""
presign_ttl_seconds = 900
[attachments.target.S3.credentials]
kind = "environment"

The public base_url must be an absolute HTTPS URL in production. It must have no user info, query, fragment, trailing slash, or dot path segment. HTTP is accepted only for a loopback host, such as 127.0.0.1, [::1], or localhost. A private LAN address still needs HTTPS. The target must serve objects at {base_url}/{hex SHA-256 digest}. This backend signs path-style S3 requests, so use an endpoint that supports them. Keep the endpoint, bucket, and credential values private. The backend validates the URL, upload limit, and retention limit at startup.

The target must enforce If-None-Match: * on PUT, return 412 when an object exists, check x-amz-checksum-sha256 against the request body, and reject a request whose signed content length differs from the body length. A successful GET from the public URL must return the exact bytes. Check these operations before you use a new target in production.

Set CORS on the bucket when browsers upload attachments. Permit PUT and GET from the application origins. Permit the content-length, host, if-none-match, and x-amz-checksum-sha256 request headers. Expose response headers that the application needs. Use a specific origin list for a production application.

The backend publishes the configured retention_seconds, or 0 when none is configured. A published 0 is the no-expiry value in the offer; it does not control the target. For positive retention, set the target’s expiry rule to the same period after upload. When the target keeps objects without expiry, set retention_seconds = 0 or omit it. Check the target’s lifecycle timing and clock behavior. The target applies its expiry rule; the backend does not delete objects.

max_upload_bytes defaults to 104857600 (100 MiB) and must be from 1 through 4294967295. presign_ttl_seconds defaults to 900 and must be from 300 through 3600. Temporary credentials can shorten the signed request’s lifetime. Credentials that leave less than 300 seconds cannot sign an upload.

The credentials.kind value selects how the backend gets signing credentials:

KindRequired keysUse
staticaccess_key_id, secret_access_key; optional session_tokenLocal tests or an external secret injector.
default_chainNoneAWS default provider chain.
environmentNoneAWS credential environment variables.
profilenameNamed AWS profile.
ssoaccount_id, region, role_name, start_url; optional session_nameAWS IAM Identity Center.
processcommandExternal credential process.
web_identityNoneWeb identity token.
containerNoneContainer credential endpoint.
instanceNoneInstance metadata.
assume_rolerole_arn; optional external_id, session_nameAWS role assumption.

Use env:NAME for secret settings in TOML. Limit the credential to PUT on the attachment bucket. Give the public GET path a separate read policy. If the backend cannot obtain credentials, or they expire too soon, CreateUpload returns UNAVAILABLE. If the credential source reports an invalid configuration, it returns FAILED_PRECONDITION. If the backend cannot build or sign the request, it returns INTERNAL. The backend refuses to start if credentials.kind is unknown or a required field is missing. GetConfigurationResponse contains no storage credential, bucket name, or storage endpoint URL other than base_url.