- A supplier uploads a large catalogue.
- Twenty minutes later, nobody knows whether the platform is parsing rows, creating products or silently stuck.
- That was the client problem in a B2B marketplace built on Medusa.
- We built the import around durable units of work.
The client problem
A supplier uploads a large catalogue. The browser confirms that the file was received, then displays a spinner.
Twenty minutes later, nobody knows whether the platform is parsing rows, creating products or silently stuck. Closing the tab feels dangerous. Starting again could duplicate work. Cancelling might mean losing everything.
That was the client problem in a B2B marketplace built on Medusa. Catalogue imports were too important to behave like one long web request, and operators needed more than a cosmetic percentage.
We built the import around durable units of work. Rows are staged before product creation, each row carries a business status, progress snapshots identify the current phase, and seller-scoped routes expose the real row counts. If a worker stops, processing can resume from rows that remain pending. If an operator cancels an eligible import, the platform stops further progression without pretending that already committed batches vanished.
The result is an import journey that can answer three practical questions: what is happening now, what has already happened and what can the operator safely do next?
The client problem: “processing” is not an operational answer
A single status field is useful at the start and end of a job. It says pending, processing, completed or failed.
It says very little during the hours or minutes that matter.
An import can spend time receiving a file, staging rows, validating data, creating products, waiting for decisions about updates or finalising search data. If all those phases are labelled “processing”, the operator cannot tell healthy work from a stalled worker.
A percentage alone is not enough either. Ten per cent of file staging and ten per cent of product creation describe different work. A counter based on browser time is worse: it looks smooth while saying nothing about the backend.
We made progress a persisted description of completed work. The interface reads the same state the workflow writes rather than animating an estimate independently.
Separate the upload from the import
The browser's first responsibility is to hand off the file reliably.
An upload session records the purpose, storage provider, file identity, uploader and lifecycle. The file can move from pending to uploaded, then into ingestion, completion, failure or cancellation.
This hand-off lets the import continue after the browser request returns. The user's connection is no longer the lifetime of the catalogue job.
The ownership fields are important. A supplier can poll or control its own import without receiving another supplier's file state merely by changing an identifier.
The upload session also provides a stable place for progress metadata. The workflow can update the phase, processed count, total, percentage and timestamp while preserving other result fields already attached to the session.
For the operator, the first reassuring signal is therefore truthful: the platform owns the file, and the background journey has an identity that can be revisited.
Report phases because the total is not always known yet
During streamed staging, the platform has not necessarily reached the end of the file. It knows how many rows it has accepted so far, but the final total may still be unknown.
The progress writer supports that reality. A snapshot contains:
- the current phase;
- the number processed;
- the known total, when available;
- a derived percentage;
- the time of the latest update.
While staging, the total can remain zero and the interface can show a growing row count instead of a false percentage. Once the stream ends, the import record stores the final row total. Product processing can then report meaningful completion against that denominator.
This makes the display more pedagogical. “Staging — 25,000 rows received” is useful even before the final count is known. “Creating products — 12,500 of 40,000” becomes useful when the platform has a bounded population to process.
The phase name explains what the number measures.
Make every row part of the progress model
The durable staging table is not only a performance technique. It is the ledger that makes progress explainable.
Each row belongs to an import and keeps its original position, raw values, product identifiers and current status. The workflow can distinguish work that is:
- still pending;
- successfully created or updated;
- skipped under a known policy;
- waiting for an update or restore decision;
- parked for later review;
- failed with a row-level reason.
The supplier-facing status route calculates these counts directly from the staged rows. It does not infer success from how many batches a worker claims to have run.
Skipped rows are also categorised. A duplicate inside the file is different from a zero-price row rejected by import policy, a product belonging to another seller or a re-upload that contains no changes.
That distinction turns “37 rows skipped” from a mystery into an actionable summary.
Process in bounded batches and commit visible progress
After staging, the workflow selects pending rows in their original order and processes a bounded group at a time.
Each group creates the connected commerce records required by Medusa: products, variants, prices, inventory and marketplace relationships. The database transaction covers that batch rather than the entire file.
When the transaction succeeds, the corresponding staged rows move to a completed status. The next progress snapshot can therefore be grounded in durable database state.
This provides failure containment. If a later batch encounters a problem, earlier committed batches remain identifiable as completed and the unresolved work remains visible.
It also gives the operator a more useful mental model. The import is not one opaque operation that either exists or disappears. It is a sequence of controlled work units whose outcomes can be counted.
Let the interface poll facts, not worker memory
The vendor status endpoint returns the import identity, high-level state, totals, error information and detailed row-status counts.
Because those counts come from PostgreSQL, a new browser session can recover the same view. The operator does not need to keep the original tab alive, and the worker does not need to retain an in-memory progress object for the interface.
The updated_at value helps distinguish slow work from stale work. A phase with a recent timestamp may simply be processing a large unit. A phase that has not changed can trigger investigation or expose a resume action.
The interface can then present useful language:
- receiving and staging supplier rows;
- creating catalogue products;
- waiting for update review;
- completed with created, skipped and failed counts;
- interrupted with pending work available to resume.
This is operational progress: a view of the workflow's business state, not a theatrical loading bar.
Resume from durable pending work
A worker can stop after deployment, infrastructure interruption or an unexpected error.
The seller-scoped resume route first verifies that the import belongs to the authenticated supplier. It marks the record as processing again, reconnects the existing upload session when available and re-enqueues only the staged-row processing workflow.
The processing query selects rows whose status is still pending. Rows already marked created are not selected again by that query.
This gives the system a practical recovery point. The supplier does not need to upload the whole file again simply because the worker stopped after several batches.
The upload session returns to an active phase so the existing polling interface can follow the resumed work under the same identity.
The public promise remains precise: resume continues from durable pending rows. The platform does not need to describe every possible side effect as universally exactly-once in order to provide meaningful recovery.
Make cancellation mean “stop further processing”
Operators also need a way to stop an import that should not continue.
The seller cancellation route checks ownership, import type and current status. Only eligible batch-create imports in a pending or processing state can be cancelled through that path.
Cancellation changes the import's state so further work can stop. It is not presented as a rollback of every product already committed by earlier batches.
That wording matters. If 5,000 products were created before cancellation, showing “nothing happened” would be misleading and operationally dangerous. The row ledger can still explain what completed and what did not.
A separate upload-session cancellation path can mark the session and associated work appropriately. The interface should therefore show cancellation as a terminal decision with outcome counts, not as a rewind animation.
This makes the control trustworthy: the operator knows what the button promises.
Preserve row-level errors without flooding the interface
A large import can contain many malformed rows. Returning one enormous error response would overwhelm both the workflow and the operator.
The platform persists an error against the affected staged row and keeps aggregate counts on the import. A bounded set of processing errors can be surfaced in the main result, while detailed status categories explain the broader population.
This supports two levels of use:
- the supplier sees whether the import is broadly healthy and which policy categories need attention;
- an operator can inspect specific failed rows and their original position when remediation is required.
Progress and diagnostics remain connected. A job that reached 100 per cent with 200 failed rows is not labelled simply “successful”; its result describes the mix of created, skipped and failed work.
Reuse the operating model across import types
The same operational problem appears beyond product creation.
Inventory updates and price-list imports also benefit from durable records, status routes, progress snapshots, cancellation and explicit result counts. The business fields differ, but the control plane is similar.
That reuse is valuable for merchants. Operators should not learn a completely different language for every bulk operation. “Uploaded, staging, processing, waiting for review, completed, failed or cancelled” can become a coherent platform vocabulary.
The underlying row states can remain specific to the job. A price update may wait for approval while a catalogue row may wait for categorisation. The shared progress contract tells the interface how to explain the journey without flattening those differences.
Why Medusa was the right foundation
Medusa supplied the catalogue, pricing, inventory and workflow primitives. The client needed an operational layer that could turn supplier files into those connected commerce records safely.
We added upload sessions, durable staging, bounded processing, progress snapshots, seller-scoped status and recovery controls around the engine.
This is where extensibility matters. A rigid platform often exposes a generic importer whose progress and failure semantics cannot be adapted to the supplier journey. Here, the import could reflect the actual business states while still producing native Medusa products and inventory.
The architecture choice was therefore practical: use Medusa for commerce truth and build the missing operating system for long-running supplier work.
A practical progress-and-recovery checklist
Before putting a long import in front of suppliers, define:
- Does the browser hand the file off before processing begins?
- Is the upload session owned by the authenticated supplier?
- Which phases can the operator see?
- What does “processed” mean in each phase?
- Can the total remain unknown without inventing a percentage?
- Is every row represented by durable state?
- Are skip reasons grouped into business-readable categories?
- Does each committed batch update the row ledger?
- Can a new browser session recover the same progress view?
- Does the interface show when progress was last updated?
- Can interrupted work resume from pending rows?
- Is resume restricted to the supplier that owns the import?
- Does cancellation stop further work without claiming a global rollback?
- Can operators distinguish completion with failures from full success?
These questions turn background processing into an operable merchant capability.
The broader lesson
The client did not need a prettier spinner.
They needed confidence that a supplier catalogue could leave the browser, continue as durable work and remain understandable throughout its lifecycle.
We gave each upload an identity, each phase a persisted snapshot and each row a durable outcome. The interface polls database facts. Interrupted work can continue from pending rows. Cancellation stops future processing while preserving the truth about completed batches.
For suppliers, long imports become less intimidating. For operators, failures become diagnosable. For the platform, progress becomes part of the workflow contract instead of an animation layered on top of it.
