The Write That Cannot Refuse
Object storage has no concept of refusing a write. Put an object at a key that is occupied and the old one is simply gone — which means the safety that stops a document being replaced has to live somewhere else, and it is worth knowing exactly where.
Three of the four storage providers this system archives to can tell you a file already exists and decline to replace it. The fourth cannot, because it does not have the concept. Object storage takes a key and some bytes and puts the bytes at the key, and whatever used to be there stops existing. There is no error, no warning, and no version of the operation that behaves differently.
Where the check lives
So the protection has to be somewhere, and on this connector it sits one layer above the write.
The job that archives a document works out what the object would be called, asks the bucket whether anything is at that key, and returns without doing anything if there is. Only when the key is clear does it render the document and hand the bytes to the write.
The outcome on the archive path is the same as on the other three: run it twice, nothing is duplicated, nothing is replaced. The structural difference is worth naming precisely, because it tells you what to be careful about.
| Provider | Where the "already there" check sits | What the raw write would do |
|---|---|---|
| Google Drive | Inside the archive call. | Create a second file with the same name. |
| Dropbox | Inside the archive call. | Refuse to overwrite; add a numbered copy. |
| OneDrive | Inside the archive call. | Replace the file at that path. |
| Object storage | In the job that calls it. | Replace the object at that key. |
A guarantee held by the caller is a real guarantee. It is just a guarantee about one caller, and it is worth knowing that is what you have.
The archiving job is the only thing in the system that writes documents to your bucket — the sole other write is the connection test, which puts a marker object under your prefix on purpose. That is a small enough surface to have checked rather than assumed, and it is the reason the arrangement is sound rather than merely fortunate.
Why not push the check into the write?
Because it would be a check that could not keep its promise, and a promise that cannot be kept is worse than an honest absence.
Object storage offers no atomic write-if-absent in the way a filesystem does. Any check-then-write on a bucket has a gap between the two halves, and two workers can both pass the check. Building that gap into the write would make the write look safe — a method named for refusing collisions, which occasionally does not — and the failure would be invisible in exactly the conditions where it matters.
Left in the caller it is honest about what it is: an optimisation that avoids re-rendering and re-uploading a document already filed, not a lock. The actual protection against losing a document to a race is somewhere else entirely, and it is stronger.
The protection is that the two copies are the same document
This is the point that makes the whole design work, and it is easy to walk past.
The key is derived from the document number. Invoice INV-2026-0413 lands at the key ending in that number, always, because the number is where the name comes from. So if two jobs do race, the second write replaces the first with a re-render of the same invoice.
A collision on a key that identifies a document is not a conflict. It is the same document arriving twice. The only way to lose something real would be for two different documents to produce one key, and that would require two records to share a document number — which the numbering will not do.
That is why deterministic naming is worth more than it looks. It is not tidiness. It is what converts an unsafe operation into a safe one, and it does so on every provider at once rather than needing four different mechanisms.
Where history belongs
There is a real question underneath all this. If a document is corrected and re-archived, should the archive hold both?
It is not a question a connector should answer, and this one does not try. Object storage already has versioning — turn it on for the bucket and every replacement keeps the previous object, retrievable, with a retention rule you set. It is enforced by the store rather than by a feature, applies to everything in the bucket rather than only what we wrote, and survives us entirely.
A vendor reimplementing that on top would be building a weaker copy of something your provider does properly, and putting the retention policy for your financial records in our settings screen rather than in your cloud account. The position is that history is your bucket's job, and that the honest thing is to say so rather than to ship a version list that looks reassuring.
If you want a permanent, immutable archive, the tools are all on your side of the line: versioning, lifecycle rules, and an object lock if your obligations call for one. The connector writes; what the bucket does with a write is yours to decide.
When something goes wrong, it says so
One more deliberate choice, in the opposite direction to everything above.
The storage connection is built to raise on failure rather than to return a quiet false. Object storage libraries often default to the quiet version, which means a bad credential, an unwritable prefix or a bucket in the wrong region produces a call that appears to succeed and an archive that is empty.
Raising means the reason arrives as a reason. It is caught, logged with the provider's own message, and the run continues with the next document. The alternative — silence that reads as success — is the single most expensive shape a storage bug can take, because nobody goes looking until somebody needs a document.
The existence check is deliberately the other way round: a lookup that fails for an unrelated reason is read as not there, which risks one extra write and never a missing document.
What is in place, layer by layer
Write semantics on your own bucket
Keys derived from document numbers
The object key comes from the invoice, order or sale number, so the same record always lands at the same key and a collision is the same document rather than a different one.
An existence check before rendering
The job asks whether the key is occupied before producing a single page, so a repeat run costs one small request instead of a render and a transfer.
A single write path
The archiving job is the only thing that writes documents to your bucket. The connection test writes one marker object under your prefix, deliberately.
Failures that raise
The storage connection is configured to throw rather than to return false, so a credential, region or permission problem surfaces as a logged reason instead of an empty folder.
Unknown reads as absent
An existence lookup that cannot complete is treated as "nothing there", which risks one extra object and never a document that was silently skipped.
Private visibility, explicitly
Every object is written private rather than inheriting the bucket default, so an archived invoice cannot become publicly readable by configuration.
Content type carried
The mime type goes on the object, so a PDF opens as a PDF and a spreadsheet export as a spreadsheet when somebody fetches it from the console.
Three attempts per document
A transient failure is retried a small number of times and then left alone, so a permanently broken document cannot occupy the queue indefinitely.
Versioning left to the bucket
Object history, retention and immutability are your provider's features, configured in your account, applying to everything in the bucket rather than only to what we wrote.
One difference from the other three destinations, stated because it will be visible: the folder names on object storage are set explicitly rather than derived, so the vendor document folder reads in full here and is abbreviated on the others.
Three positions held on purpose
- A safety check is placed where it can be honest about its strength. A collision guard inside the write would read as a lock and behave as an optimisation, and code that reads stronger than it is will be trusted for something it cannot carry.
- Document history belongs to your storage provider. Versioning and retention are enforced by the store, cover everything in the bucket, and outlive any vendor — reimplementing them in a settings screen would be a weaker copy in a worse place.
- Storage failures are loud. A write library that returns quietly on error produces an archive that looks healthy and is empty, which is the one failure mode nobody discovers until they need the document.
Five questions about writes to an archive you own
What stops the same document being written twice?
A good answer sounds like
A named mechanism, in a named place.
What ours actually is
An existence check in the archiving job, before rendering. It is an optimisation, and it is described as one.
What if two runs overlap?
A good answer sounds like
Nothing is lost.
What ours actually is
Both would write the same document to the same key, because the key is the document number. A collision is a repeat, not a conflict.
How many things can write to my bucket?
A good answer sounds like
A small, countable number.
What ours actually is
The archiving job, and the connection test. That is the whole surface.
How do I keep old versions?
A good answer sounds like
Use the store's own feature.
What ours actually is
Bucket versioning and lifecycle rules, configured in your account. The connector does not attempt its own.
What happens on a bad credential?
A good answer sounds like
It fails loudly.
What ours actually is
The write raises, the provider's message is logged, and the run moves on rather than reporting success.
Our take
The instinct when a storage API will not refuse a write is to build the refusal yourself, and it is worth resisting for about ten minutes to see whether you need it. Here the answer was that the naming already did the work — a key derived from a document number turns every possible collision into the same document arriving twice, which is not a problem that needs solving. What was left was an optimisation, and calling it an optimisation rather than a lock is the difference between code somebody can reason about and code that will be trusted for something it never promised. Most integration safety is like that: less machinery than you expected, in a place you have to be able to point at.
Put the retention policy where it belongs
Versioning, lifecycle and object lock are your bucket's features, in your account, under your rules. The connector's job is to write into them.
Talk through archive retentionFrequently asked questions
If I correct an invoice and re-issue it, does the archive update?
Not by itself. The key comes from the invoice number, so the corrected version resolves to the same key, the existence check finds the original, and nothing is written. Delete the object and re-run the sync to file the revision — or turn on bucket versioning first, so both are kept.
Should I turn on versioning?
If the archive is there for a compliance or audit reason, yes, and pair it with a lifecycle rule so old versions expire on a schedule you chose. If it is a convenience copy of documents that also live in your workspace, it is an expense without much return.
Can the connector delete objects from my bucket?
No. There is no delete path in it, which is why a key policy granting only the ability to put objects works perfectly well. Clearing out an archive is something you do in your own account.
Why is the folder name for vendor documents different here from the Drive version?
Because the names on object storage are set explicitly and the other three derive theirs from the internal type key. The result is one folder that reads in full here and abbreviated elsewhere. It is cosmetic, and it is mentioned so nobody comparing two destinations wonders whether documents went missing.
What happens if the bucket fills up or the key loses permission mid-run?
The write raises, the provider's message is logged for that document, and the run continues with the next one. Because everything is keyed on document numbers, fixing the cause and re-running picks up precisely what did not land.