Skip to main content
All insights
Security7 min readAugust 30, 2026

Private Evidence Files and Short-Lived Links

The short answer

Keep the bytes in private object storage and only the metadata and object key in the database. Serve each file through a signed link that expires in minutes, issued per request after a permission check, and record every issue and every fetch. An unguessable URL is not access control, because a URL is a thing people forward.

By Timothy Indarsingh, Founder & CEO, Firelinkx

The examples below are deliberately generalized and may use simplified or hypothetical scenarios. They are not descriptions of any identifiable organisation's current systems, architecture or control state.

A photograph taken on a site visit and a photograph on a marketing page are the same file format and nothing else alike. One is decoration. The other may be the reason a contractor is paid, and eighteen months later an auditor may ask who took it, when, against which milestone, and who has looked at it since.

Systems built for the second case and not the first tend to go wrong in the same few ways: files uploaded into the application database, served from a public path with a long random filename, with no record of access. Each of those is convenient and each becomes expensive at the moment somebody disputes a payment.

Separate the bytes from the record

Store the file in object storage that has no public access at all, and keep in the database only what the application needs to reason about it: the object key, the size, the declared content type, a checksum, who uploaded it, when, and what it is evidence of. The database row is the record. The object is the payload.

Putting the bytes in the database itself is the version of this that looks simplest on day one. It survives until the backup takes four hours, a restore drill is quietly abandoned, and a query that once returned a page of rows starts moving gigabytes across the network. Object storage exists for this and costs very little at the volumes a typical operational system produces.

An unguessable URL is not a permission

The common shortcut is a public bucket with long random filenames, on the reasoning that nobody can guess the address. That is true and beside the point. A URL is designed to be copied. It goes into a WhatsApp group so a colleague can look at it, into an email to a contractor, into a support ticket, and each copy is permanent and unrevocable. There is also no record anywhere of who opened it.

Serve every file through a signed link generated at request time. The application checks that this user may see this file for this record, then asks storage for a link valid for a short window, and redirects. The link is a temporary capability issued to a person who was entitled at that moment, and it stops working shortly afterwards whether or not anyone remembers to revoke anything.

  • Check permission on the record that owns the file, since entitlement comes from the work the evidence belongs to.
  • Keep the expiry short, in minutes, because the link only has to survive from click to download.
  • Issue a fresh link per request and never store one, so a link cannot outlive the permission that produced it.
  • Bind the link to the exact object, and never accept a path or key supplied by the browser.
  • Set the response disposition and content type from the stored record, so an uploaded file cannot decide how it is rendered.

Short expiry breaks some habits, and that is the trade

A link that expires in five minutes cannot be pasted into an email for somebody to open tomorrow. This is genuinely inconvenient, and it is worth being honest with users about it rather than discovering it as a complaint.

The answer is to share the record itself. A link to the work item, which requires signing in and then issues its own fresh file links, does everything the pasted link did and leaves an access trail. Where somebody outside the organisation genuinely needs a file, issue a deliberate external share with its own expiry, its own recipient and its own audit entry, so the exception is visible as an exception.

Upload without proxying the bytes

Uploads from a phone on a construction site are slow and get interrupted. Routing them through the application server ties up a request for the duration and makes the application the bottleneck for every large file.

Issue a signed upload target instead. The application decides that this user may attach evidence to this record, constrains the size and the permitted types, and returns a short-lived destination the device uploads to directly. The database row is created only once the object has landed, so a failed upload leaves no record claiming evidence exists.

Verify what actually arrived

A declared content type is a claim made by the uploading client. Read the file's leading bytes after it lands and confirm they match the declared type, record the checksum, and reject or quarantine anything inconsistent. Never serve a stored file with a content type the uploader chose.

Record access, because the question will be asked

The audit question about evidence is rarely whether a file exists. It is who has seen it, when, and under what authority. Answering that requires recording the moment a link is issued: the user, the file, the record it belongs to, the reason the permission check passed, and the time.

Log the issuing of the link as well as the fetch. A fetch may be repeated by a caching layer, retried by a flaky mobile connection, or never made at all if the user changes their mind. The issue event is the one that maps cleanly to a decision your system made, and it is the one an auditor can reason about.

Deletion, retention and the copies you do not control

Evidence supporting a payment usually has a retention period set by policy or law, and it is frequently longer than the life of the project it belongs to. Decide the period per category of evidence, store it on the record, and let deletion be a scheduled consequence of that rule.

Deleting the object is not the whole job. Purge derived copies such as thumbnails and previews, remove or tombstone the database row according to whether the record itself must be retained, and accept that backups will hold the object until the backup rotation catches up. Say so in the retention policy. A policy that claims immediate erasure while nightly snapshots hold the file for another month is a policy nobody can honour.

What to test

  • A user without permission on the record requesting a file, asserting no link is issued and the attempt is logged.
  • An expired link, asserting storage refuses it and the application never gets the chance to permit it.
  • A link issued to a user whose access is removed before they use it, asserting the short window is the actual bound.
  • A file key supplied by the client, asserting it is ignored and the key comes from the record.
  • An upload whose leading bytes contradict its declared type, asserting it is rejected and no evidence row is created.
  • An interrupted upload, asserting no orphan record claims the file exists.
  • A file at the size limit and one above it, asserting the limit is enforced at the storage target and not only in the browser.
  • A deletion under retention, asserting derived copies are purged and the audit trail of past access survives.

Why this shape holds up

Every element here answers a question somebody eventually asks. Where is the file. Who could see it. Who did see it. Is it the file that was uploaded. How long will it be kept. A system that stores evidence in a public folder can answer the first question and none of the others.

The work is not large. It is a bucket with public access disabled, a route that checks a permission and issues a signed link, a table that records the issue, and a retention field. Doing it at the start costs a day. Reconstructing who saw a photograph, after the fact, from a public bucket with no logs, is not a job that can be done at all.

Holding files that a payment or an audit depends on?

Firelinkx can model evidence storage, permissioned access and retention as part of a separately scoped project.

WhatsApp Us