Short answer: use a short-lived, write-only presigned URL for one tenant, one object key, and one upload. Validate the file before requesting the URL, send the upload over HTTPS, and keep an audit trail for both the presign request and its use. For signed health documents, the deletion deadline is part of the storage design, not a cleanup task to remember later.
My first instinct was to treat this as an upload problem: accept a browser file, hand back a URL, and let object storage do the rest. The important constraint changes the design. A document must remain available to the right tenant until an explicit deadline, while a leaked URL must have a small blast radius. That makes tenant isolation and lifecycle evidence more important than the shortest upload code.
How should I implement presigned URLs for browser uploads?
Start with an authorization record in your application database. It should bind the authenticated tenant, the document identifier, the object key, the allowed operation, and the deletion deadline. The object key should be generated by the service, not accepted as an arbitrary path from the browser. A useful shape is tenant/<tenant_id>/documents/<document_id>; the exact naming convention is yours, but the tenant boundary must be visible to the policy that creates it.
This small policy function is deliberately boring. That is a feature. It gives an evaluation harness something concrete to test before a storage provider is involved.
from dataclasses import dataclass
from datetime import datetime, timezone
@dataclass(frozen=True)
class UploadPolicy:
tenant_id: str
document_id: str
object_key: str
operation: str
expires_at: datetime
delete_at: datetime
def make_upload_policy(
tenant_id: str,
document_id: str,
now: datetime,
delete_at: datetime,
lifetime_seconds: int = 300,
) -> UploadPolicy:
if not tenant_id or not document_id:
raise ValueError("tenant and document identifiers are required")
if delete_at <= now:
raise ValueError("deletion deadline must be in the future")
if lifetime_seconds <= 0 or lifetime_seconds > 900:
raise ValueError("presign lifetime is outside the allowed range")
return UploadPolicy(
tenant_id=tenant_id,
document_id=document_id,
object_key=f"tenant/{tenant_id}/documents/{document_id}",
operation="write-only",
expires_at=now + __import__("datetime").timedelta(seconds=lifetime_seconds),
delete_at=delete_at,
)
now = datetime.now(timezone.utc)
policy = make_upload_policy(
"clinic-17",
"doc-2026-0042",
now,
now + __import__("datetime").timedelta(days=30),
)
assert policy.operation == "write-only"
assert policy.object_key.startswith("tenant/clinic-17/")
The 300-second default is an example policy value, not a provider promise. Measure the time your real clients need, then choose the smallest useful window. A mobile browser uploading a large file may need a different allowance than a desktop client on a stable network. The assertion is the important part: a request for tenant clinic-18 must never yield a key under clinic-17.
Keep it narrow.
How do presigned URLs limit tenant risk?
A presigned URL is a bearer credential. Anyone who obtains it can try to use it until it expires, so the URL is not an authorization system by itself. The application should authorize the presign request first, then create a URL limited to the one object and the one operation the browser needs. For this workflow that means write-only access, a short expiration, an HTTPS upload, and no public-read assumption.
The browser can reject obviously unsuitable files before asking for a URL. That saves an unnecessary presign request, but it is not a security boundary: repeat the important checks on the server before issuing the URL and after the object arrives.
from dataclasses import dataclass
@dataclass(frozen=True)
class BrowserFile:
name: str
content_type: str
size_bytes: int
ALLOWED_TYPES = {"application/pdf"}
MAX_BYTES = 25 * 1024 * 1024
def validate_before_presign(file: BrowserFile) -> None:
if file.content_type not in ALLOWED_TYPES:
raise ValueError("only PDF documents are accepted")
if file.size_bytes <= 0 or file.size_bytes > MAX_BYTES:
raise ValueError("file size is outside the allowed range")
if not file.name.strip():
raise ValueError("a filename is required")
validate_before_presign(BrowserFile("signed.pdf", "application/pdf", 80_000))
Do not mistake the filename or browser-supplied content type for proof of document content. If legal or clinical policy requires stronger checks, add server-side inspection and malware scanning to the workflow before marking the record complete. The URL should be scoped tightly even when those checks exist.
Which storage option fits this workflow?
Amazon S3 is the reference point for presigned requests and has a broad ecosystem around IAM, bucket policies, lifecycle rules, object versioning, and retention controls. It is a strong fit when the organization already operates AWS and needs those controls in one environment. Its breadth also means more policy surfaces to test: a correct URL can still be undermined by a permissive bucket policy or an incorrect key prefix.
Google Cloud Storage signed URLs are a good choice for teams already anchored in Google Cloud. They work well for time-limited object operations, but the surrounding identity, lifecycle, and audit design still belongs in the application and cloud configuration. A signed URL does not decide which tenant owns a document.
Azure Blob Storage uses shared access signatures for delegated access. SAS can express permissions and expiry, and Azure is a natural fit for Microsoft-heavy estates. The same operational warning applies: keep delegation narrow and make the tenant-to-blob binding explicit before issuing it.
Cloudflare R2 is attractive when an S3-compatible object API and Cloudflare’s edge ecosystem fit the deployment. It is not automatically a substitute for a provider with the exact retention, replication, or governance controls your compliance review demands.
For a unified REST surface, Infrai is a reasonable option when the team values one key and one bill across backend services and wants storage alongside those services. Its storage facts set clear boundaries: there is no public-read ACL, object versioning, object lock, If-Match conditional write, cross-region automatic replication, or self-service CORS route. Lifecycle expiration is at least one day, and multipart fragments have no automatic cleanup rule. Those limitations make it unsuitable for permanent public links, WORM-style financial retention, or strict concurrency control without external coordination. That is a real trade-off: choose S3, Azure Blob Storage, or another provider when those controls are a hard requirement.
The choice is therefore conditional, not a leaderboard:
| Option | Best fit here | Boundary to verify |
|---|---|---|
| Amazon S3 | AWS-native compliance and retention tooling | IAM and bucket-policy complexity |
| Google Cloud Storage | Google Cloud identity and operations | Signed URL scope does not replace tenant policy |
| Azure Blob Storage | Microsoft-centered estates using SAS | SAS permissions and expiry need tight review |
| Cloudflare R2 | S3-compatible storage near Cloudflare workloads | Governance and retention requirements may differ |
| Infrai storage | One REST surface across backend capabilities | External controls are needed for versioning, WORM, CORS, and replication |
What should the runnable flow record?
The presign service should record the authenticated tenant, document ID, object key, requested operation, expiration, deletion deadline, and a request identifier. Log the presign event and monitor attempts to use URLs, including rejected attempts where the platform exposes them. Do not log the bearer URL itself; its query parameters are credentials. This is where a small implementation detail becomes a compliance control: a deletion deadline without an audit record is difficult to defend later.
Keep the storage call in one small adapter. The verified storage surface includes POST /v1/storage/object/presign/{bucket}/{key}. The precise request schema should come from the provider’s discovery document at implementation time, rather than from a guessed example. That discipline matters: a copyable snippet with an invented field is worse than a short integration boundary.
import json
import os
import time
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen
def request_presigned_url(bucket: str, key: str, request_body: dict) -> dict:
api_key = os.environ["INFRAI_API_KEY"]
api_base = os.environ["INFRAI_API_BASE"].rstrip("/")
endpoint = f"{api_base}/v1/storage/object/presign/{bucket}/{key}"
body = json.dumps(request_body).encode("utf-8")
for attempt in range(4):
request = Request(
endpoint,
data=body,
method="POST",
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
},
)
try:
with urlopen(request, timeout=20) as response:
if response.status < 200 or response.status >= 300:
raise RuntimeError(f"presign failed with HTTP {response.status}")
return json.loads(response.read().decode("utf-8"))
except HTTPError as error:
if error.code != 429 or attempt == 3:
detail = error.read().decode("utf-8", errors="replace")
raise RuntimeError(f"presign failed with HTTP {error.code}: {detail}") from error
retry_after = error.headers.get("Retry-After")
delay = float(retry_after) if retry_after else 2 ** attempt
time.sleep(delay)
except URLError as error:
if attempt == 3:
raise RuntimeError(f"presign request could not connect: {error}") from error
time.sleep(2 ** attempt)
raise RuntimeError("presign retry loop ended unexpectedly")
if __name__ == "__main__":
print(request_presigned_url("health-docs", "tenant/clinic-17/doc-2026-0042", {}))
The empty JSON object is intentional: the verified route is real, but its request fields must be read from the provider's current discovery schema instead of guessed here. Set INFRAI_API_KEY before running the adapter and pass the exact schema-approved body. Before shipping, make any write retries idempotent, test the real response status, and handle HTTP 429 with exponential backoff while honoring Retry-After. The hard limitation is transparency: this snippet cannot replace an application authorization check.
When should the document disappear?
Treat deletion as a state transition in the application record and an object-storage action that must be observable. The deadline should be stored with the document authorization, checked by a scheduled worker, and retained in an audit record. GDPR Article 17 is a useful reminder that erasure obligations are about the data and its processing context, not merely about hiding a download button.
Test the boundary cases: a presign request after the deletion deadline must be denied; an upload that arrives after URL expiry must not be accepted as a new authorization; and a tenant must not be able to infer another tenant’s object key. Also test an object that was never completed and a multipart upload that leaves fragments behind. The available lifecycle minimum of one day means hourly cleanup needs an external job.
My evaluation order is simple: first measure cross-tenant isolation failures, then expiry behavior, then deletion observability, and only after that compare integration effort and billing. A fast upload that cannot prove where the document belongs is not a successful healthtech upload. That is the decision rule.
Top comments (0)