Design: Files Transfer Patterns
Prerequisites
- trellis-patterns.md - Trellis architecture and communication model
- store-resource-patterns.md - service-owned blob-store resources
- ../contracts/trellis-contracts-catalog.md - contract ownership and permission rules
Context
Services often need to expose file-like behavior to apps and peer services without exposing raw store bindings.
Examples:
- transfer an attachment into service-owned storage
- receive a generated export from a service-owned transfer endpoint
- inspect file metadata before deciding whether to fetch bytes
- delete a stored object through the owning service’s business rules
resources.store solves service-owned blob persistence. It does not by itself
define a public API for callers.
Files is the public pattern that sits on top of service-owned store resources.
Scope
This document defines the public Trellis files pattern:
- which actions stay ordinary contract RPCs
- how byte transfer is modeled as one runtime concept with caller-facing directions
- how services back the public files surface with service-owned
store - how callers and providers receive per-chunk transfer progress
It does not define a global admin UI, cross-service shared raw store access, or
ordinary language-library walkthroughs. TypeScript and Rust usage examples
belong in /guides/libraries/typescript, /guides/libraries/rust, and the
generated API references linked from /api.
Design
Ownership Model
Rules:
- the owning service keeps direct access to
service.store.<alias> - clients and peer services do not resolve raw store bindings
- public file behavior is exposed through the owning service’s contract surface
- if another service needs file access, it uses the owning service’s
Files.*API rather than the raw store binding Filesis the public interface tostorein the same way that contract-owned operations are the public async workflow interface for service-private execution machinery
Public Files API Split
There are three common categories of file behavior.
Metadata and control RPCs
File metadata and small control actions remain ordinary contract-owned JSON RPCs.
Examples:
Documents.Files.ListDocuments.Files.HeadDocuments.Files.Delete
Rules:
- these methods use normal Trellis RPC auth and capability checks
- they return JSON payloads and
Result-modeled failures listis prefix plus standard page request oriented in v1 rather than an arbitrary metadata query language: callers send{ offset?: number; limit: number }plus file-domain filters such asprefix, and services return{ entries, count, offset, limit, nextOffset? }- file listing is live offset pagination, not snapshot or cursor pagination; concurrent file writes or deletes can change what appears at later offsets
Send transfer operations
When the caller sends bytes to the service, file bytes use an operation-native model:
- a contract-owned operation accepts JSON input and declares
direction: "send"transfer support - the caller configures the operation input and sends bytes through the generated transfer-capable operation helper for that language
- callers do not start the same send-transfer operation first and attach bytes later
- the provider awaits the runtime’s durable transfer completion signal and continues with service-owned processing
Example:
Documents.Files.Upload
Rules:
- send transfer is modeled as a capability of an operation; upload/file-ingest remains operation-native
- the operation contract declares the backing store alias and the input pointers
used to derive transfer metadata such as
keyandcontentType - the actual byte movement still uses raw NATS chunk traffic rather than JSON/base64 RPC payloads
- the transfer protocol is Trellis-owned runtime machinery, not a service-specific public protocol surface
- callers observe transport progress through operation watch transfer events, language-library transfer callbacks where available, and durable snapshot state
- providers observe the same transport progress through provider-side transfer update streams
Receive transfer RPCs
When the caller receives bytes from the service, metadata and control remain a contract-owned RPC and the RPC returns a receive transfer grant.
Example:
Documents.Files.Download
Rules:
- the RPC declares
transfer: { direction: "receive" } - the RPC response contains a Trellis transfer grant, not raw store binding details
- callers consume the grant through the language runtime’s receive-transfer helper for large streams or buffered reads
- service code decides whether and how the requested object maps to a service-owned store entry
- product-facing docs may still use words such as upload and download, but the platform API should prefer transfer, send, and receive language where possible
Operation Transfer Declaration
Transfer-capable operations declare transfer support in the operation descriptor.
Example:
operations: {
"Documents.Files.Upload": {
version: "v1",
input: ref.schema("FilesUploadRequest"),
progress: ref.schema("FilesUploadProgress"),
output: ref.schema("FilesUploadResult"),
transfer: {
direction: "send",
store: "uploads",
key: "/key",
contentType: "/contentType",
expiresInMs: 60_000,
},
capabilities: {
call: ["uploader"],
observe: ["uploader"],
},
},
} Rules:
transfer.storenames the owning service store resource alias used for stagingtransfer.directionis explicit; operation-native file ingest uses"send"transfer.keypoints into the validated operation input and resolves to the staged store key- optional pointers such as
contentTypeandmetadataresolve from the same validated input payload - contract validation should fail if the configured store alias does not exist or a configured input pointer does not exist in the input schema
RPCs that issue receive grants declare the receive direction explicitly:
rpc: {
"Documents.Files.Download": {
version: "v1",
input: ref.schema("FilesDownloadRequest"),
output: ref.schema("FilesDownloadResponse"),
transfer: {
direction: "receive",
},
capabilities: {
call: ["reader"],
},
},
} The response schema carries file metadata and the transfer grant. The service still owns the raw store binding and any lookup, authorization, retention, or audit policy behind the RPC.
Runtime Helper Boundaries
Rules:
- language runtimes expose send transfer through generated transfer-capable operation helpers, not through a follow-up method on an already-started operation reference
- service-issued receive grants are consumed through root transfer helpers that expose streaming and buffered reads where the language supports them
- supported body forms, exact method names, callback shapes, and error wrapper
types are language-library API details documented in
/guides/libraries/typescript,/guides/libraries/rust, and/api - metadata actions such as list, head, and delete remain ordinary typed RPC calls on the contract client
- all languages preserve the same chunk-progress semantics and Result-style expected failure model
- providers that expose send-transfer operations MUST await the provider-side durable transfer completion primitive before treating bytes as durably available; completion resolves only after the transfer endpoint has accepted EOF and written the object to the configured service-owned store, or fails with a transfer error if durable storage was not reached
Wire Behavior
Rules:
- byte transfer uses raw NATS messages, not JSON/base64 wrappers
- request signing still uses session-bound proof headers
- send transfer sends ordered chunk requests on a runtime-owned transfer subject and receives per-chunk acknowledgements
- receive transfer streams ordered chunks from a service-owned transfer endpoint to the caller; callers use language-runtime transfer helpers with the returned receive-transfer grant rather than resolving raw store bindings
- the runtime emits one transfer update per acknowledged chunk on both caller and provider sides
- chunk sequence and end-of-stream markers are runtime protocol details owned by Trellis
This mirrors the general style used by NATS object store: raw chunk payloads plus separate metadata/control frames.
Store Backing
Rules:
- canonical v1 file persistence lands in the owning service’s
resources.store - services may use one or more store aliases as transfer staging backends
- services may later mirror or copy files to external systems, but
Filesdoes not depend on those backends Filesdoes not imply shared raw store access across services- receive grants expose bytes from a service-owned endpoint; they do not expose or delegate raw store bindings
- once transfer completes, service code works with the staged object through the
owning service’s normal store API; exact helper names belong in
/api - Trellis should make staged transferred objects easy for the owning service to access, but it does not impose post-transfer processing policy on the service author
Transfer Plus Operations
For caller-visible file-processing workflows, the recommended pattern is:
- a contract-owned operation declares transfer support
- the caller starts the operation and begins watching it, either directly or through language-runtime transfer helper callbacks where available
- the caller sends bytes through the generated transfer-capable operation helper
- the provider awaits the runtime’s durable transfer completion signal and updates business progress or enqueues follow-up work
- the operation completes when the service-owned workflow completes
Rules:
- transfer success means
bytes stored, notworkflow finished - a durable transfer completion signal means all chunks were accepted, EOF was received, and the configured service-owned store write completed; service code can then read the object through normal store APIs
- use runtime-owned transfer events or language-runtime transfer callbacks for progress bars and service-authored progress calls for domain milestones
- use operations for caller-visible progress and final results
- use jobs for service-private execution, retries, and background processing after the bytes are stored
Events
If the owning service exposes file lifecycle events, they should be
contract-owned Files.* events rather than raw store events.
Rules:
- public file events represent the service’s contract view of file changes
- direct store writes performed by the service may still be normalized into
public
Files.*events - the public abstraction stays
Files, not backend-native store notifications
Non-Goals
This document does not define:
- direct client resolution of
resources.store - HTTP send/receive transfer endpoints
- arbitrary query/filter semantics for file listing
- shared writable store bindings across services
- a global cross-service files admin query surface in v1