- Direct file upload solves an important scaling problem.
- Instead of sending a large catalogue, inventory file or shopping list through the commerce API, the browser receives a short-lived upload instruction and sends the bytes to the file provider.
- But a presigned URL or upload endpoint solves only transport.
- For a multi-sided marketplace built with Medusa, we made the upload session the security and workflow boundary.
The client problem
Direct file upload solves an important scaling problem.
Instead of sending a large catalogue, inventory file or shopping list through the commerce API, the browser receives a short-lived upload instruction and sends the bytes to the file provider. The backend can process the result asynchronously without holding a large request open.
But a presigned URL or upload endpoint solves only transport. It does not answer the business questions that determine whether the file may be trusted:
- Who requested this upload?
- Which seller or organisation owns it?
- Was it intended for inventory, a product catalogue, a price list or a buyer shopping list?
- Has the file actually arrived?
- Can the same reference be reused after ingestion begins?
- May another authenticated actor discover its status or download it?
For a multi-sided marketplace built with Medusa, we made the upload session the security and workflow boundary. The platform records the actor, organisation, provider key, intended purpose and lifecycle state before accepting bytes. Every downstream ingest path rechecks those facts.
The upload is not a generic file handle. It is the first step of one authorised business operation.
A secret URL is not an ownership model
Presigned uploads are often treated as safe because the URL is difficult to guess and expires.
That protects the storage write to a degree, but it does not prove that a later API caller owns the resulting file. If ingestion accepts any valid storage key, one user can submit another user's key. If the same uploaded CSV can be passed to any importer, a shopping list may accidentally enter an inventory workflow. If status endpoints expose sessions by identifier alone, an authenticated actor can enumerate someone else's processing metadata.
We separated transport authority from business authority.
The provider grants a temporary way to transfer bytes. The database session records who may use those bytes and why. The ingest route requires both contracts to remain true.
This gives the backend a durable decision point after the upload URL has expired and before the file changes commerce data.
Create the operation before accepting the file
The presign request creates an upload-session record first.
That record carries a unique session ID, a whitelisted purpose, provider identity and key, original filename, content type, optional size, uploader identities, organisation context, status and metadata. It begins in a pending state.
Only then does the provider produce the upload instruction. In the provider contract, that instruction includes the method, destination and an expiry timestamp. The local development implementation uses an authenticated backend upload route; a cloud implementation can use direct object-storage instructions. The ownership model does not depend on pretending both transports are cryptographically identical.
This sequence is important. The file never appears as an orphaned key that the application must later guess how to classify. Its intended operation already exists when transport begins.
The session also gives the browser one stable ID for upload, ingestion and progress polling without exposing raw storage structure as the public API.
Whitelist purpose at the first boundary
A client should not invent arbitrary workflow names.
The Store and Vendor presign routes accept a closed set of purposes. Examples include shopping-list CSV, organisation onboarding data, product import, catalogue update, inventory import and price-list import. Unsupported values are rejected before a session is created.
Purpose is not decorative metadata. The downstream route expects a specific value and rejects a session created for another feature.
Consider a buyer shopping-list importer. It requires shopping_list_csv. An organisation import derives the expected purpose from the requested entity type. Vendor import routes admit their own catalogue, inventory or price-list purposes. A file uploaded for one journey cannot be redirected merely by submitting its session ID to another endpoint.
This prevents confused-deputy behaviour: a privileged importer cannot be tricked into acting on a file whose original authority was granted for a different, less powerful operation.
Bind the session to the authenticated actor
Every presign route derives ownership from authentication context.
For a buyer-side upload, the session records the customer and, where applicable, the organisation. For supplier work, it records the seller identity used by the vendor surface. The browser does not receive permission to nominate an unrestricted owner in the request body.
When bytes arrive through the local upload route, the authenticated customer must match the session's uploader. When an ingest begins, the route retrieves the session and repeats the owner check. Organisation imports additionally compare the recorded organisation with the caller's current organisation context.
Foreign or missing sessions use a not-found response pattern. The API does not confirm that a guessed session exists for somebody else.
The same rule applies to status and source-file access. Possession of the session ID is not sufficient. The current actor must still own the tracked operation.
Keep seller identity consistent across marketplace surfaces
Vendor portals add a subtle identity problem.
The authenticated principal may be a vendor user, while the commerce resources belong to a seller entity. Upload ownership needs to follow the seller context that downstream catalogue and inventory workflows enforce.
Supplier presign and import routes therefore carry the seller-scoped identity into the session. The importer compares its authenticated seller with the recorded uploader before reading the file. Price-list routes also verify that the target price list belongs to the same seller context.
This prevents a valid supplier account from using an upload session to mutate another supplier's catalogue or commercial list.
The design follows a repeatable marketplace rule: derive seller identity once from the authenticated request, store it with the operation and verify it again at every resource boundary that consumes the operation.
The file provider does not need to understand marketplace tenancy. The application session supplies that missing business context.
Use state to constrain what happens next
An upload has a lifecycle, not a Boolean “exists” flag.
The session moves through states such as pending, uploaded, ingesting, ingested, failed or cancelled. Each API action requires the state it knows how to handle.
The local upload endpoint accepts bytes only while the session is pending. An ingest route expects uploaded. Starting background processing moves the session to ingesting and can initialise progress metadata. The workflow eventually marks completion or failure. Status polling returns the current state and bounded metadata to the owner.
These transitions prevent accidental reuse. A client cannot keep overwriting a session once the file has advanced. An importer does not start from a pending session whose bytes may still be incomplete. Two workflows cannot safely claim the same session merely because the provider key exists.
State also gives operations a useful answer when processing fails. They can distinguish transport failure, validation failure and an ingest already in progress.
Read through the provider abstraction, not a public URL
Downstream workers need a controlled way to retrieve bytes.
The provider key stored on the session is used by the backend file service to obtain a download stream. A retained file URL may help trace legacy or provider context, but it is not the ownership check and should not become the primary trust primitive.
This keeps storage details behind the application boundary. A workflow receives a validated session and a provider-backed stream rather than trusting an arbitrary URL supplied at ingestion time.
It also lets local and remote storage use the same business flow. Development can upload through an authenticated backend route. A production object-store provider can issue an expiring direct instruction. Both produce the same tracked session that downstream features understand.
The public claim is therefore precise: upload instructions expire and the application session is owner- and purpose-bound. The architecture does not need to call every local development URL a cryptographic cloud signature.
Validate the file before mutating commerce
Ownership says who may submit a file. Purpose says which workflow may read it. Neither says its contents are valid.
Each importer still applies feature-specific validation. A shopping-list upload checks the requested SKU column and rejects empty data. Organisation onboarding distinguishes entity types and records the resulting import job. Product, inventory and price-list flows parse their own schemas, create durable import records and expose progress.
For price lists, a preview path can inspect the uploaded session without changing its state or mutating prices. The actual import remains a separate decision. This preserves a useful boundary between reading proposed changes and applying them.
The upload session therefore composes with domain validation rather than replacing it. Security answers “may this actor use this file for this operation?” The importer answers “does this file describe valid changes for that operation?”
Both questions must pass before commerce data moves.
Make progress polling respect the same perimeter
Asynchronous work improves reliability only if users can understand what is happening.
The session provides a status URL and metadata for progress, failures and output references. But those endpoints repeat the ownership check before returning anything. A buyer sees their own shopping-list import. A seller sees the progress of their own catalogue or inventory work.
This matters because metadata can be sensitive even without file contents. Original filenames, row counts, errors, provider references and business-purpose names reveal operational information.
The API returns a safe status shape rather than the whole database model. Internal provider keys and unrelated actor identities do not need to become browser data.
The same operation ID can safely connect a long-running workflow, its progress screen and support investigation because access remains contextual at every read.
Expiry limits transport, while state limits business reuse
URL expiry and session state solve different problems.
An expiring instruction narrows the time in which bytes may be sent through that transport. Once the provider accepts the file, the application still needs to decide how long the uploaded evidence remains available and whether it can enter a workflow.
That is why the session's status and ownership checks remain necessary after expiry. A leaked old upload URL may no longer write, but a leaked session ID must also fail to authorise ingestion or status access by another actor.
Operational cleanup can cancel or remove stale sessions according to retention policy. The core security property does not depend on secrecy alone: downstream routes require current state, correct owner and exact purpose.
Layering these controls produces a clearer system. Transport credentials are temporary. Business authority is explicit and durable enough to govern the asynchronous job.
Why Medusa was the right foundation
Medusa provided authenticated Store, Vendor and Admin surfaces, file-service integration and extensible workflows. The client needed a shared upload primitive that could support several marketplace capabilities without giving every importer a generic storage back door.
We added a dedicated module for upload sessions and connected it to the authentication context of each surface. Product catalogue, inventory, price-list, organisation and shopping-list flows can reuse the transport while retaining their own purpose and validation rules.
This is the architectural benefit of extending a commerce engine around business operations. The file does not live in a parallel utility disconnected from sellers, organisations and import records. It enters through the same identity and workflow boundaries that govern the data it may eventually change.
One reusable primitive reduces duplicated upload plumbing. Explicit ownership and purpose prevent that reuse from flattening every permission model into one.
A practical secure-upload checklist
Before accepting direct uploads, define:
- Is the operation record created before bytes are accepted?
- Which authenticated identity owns it?
- Is seller or organisation context stored separately where needed?
- Which purpose values are allowlisted?
- Does every ingest route require its exact purpose?
- Does a foreign session return the same shape as a missing one?
- Which status permits the initial upload?
- Which status permits ingestion?
- Can a session be claimed by two workflows?
- Does the provider instruction expire?
- Is expiry distinct from application-session authority?
- Do workers read through a trusted provider key or arbitrary URL?
- Does content validation happen before commerce mutation?
- Can a preview remain non-mutating?
- Are progress and download endpoints owner-scoped?
- Which metadata is safe to expose in the browser?
- How are failed, cancelled and stale sessions handled?
These contracts turn a convenient upload into a controlled business input.
The broader lesson
The dangerous part of direct upload is not moving bytes around the API server.
It is losing the relationship between those bytes and the business authority that requested them.
For this marketplace, every upload begins as a tracked session. Authentication supplies the customer or seller. Organisation context is retained where relevant. A whitelist supplies the one allowed purpose. Provider instructions expire. State records whether the file is pending, uploaded or already being processed. Ingestion, polling and download routes verify the same ownership again.
The file can travel efficiently without becoming an anonymous capability token for the rest of the platform.
That pattern scales beyond CSV. Any asynchronous document, image or batch import becomes safer when transport is temporary and the application can still answer: who owns this input, what may it do and where is it in its lifecycle?
