Stateless vs. Signed-URL Architecture: A Technical Comparison
Table of Contents
- 1. Two Architectures, One Question
- 2. What “Stateless” Means at the Code Level
- 3. What “Signed-URL” Processing Means at the Code Level
- 4. Side by Side: What Each Pattern Optimises For
- 5. The Price of Persistence: Why Signed URLs Need a Deletion Story
- 6. Choosing Between Them: Engineering Rules of Thumb
- 7. Why Secure Academic Studio Uses Both
1. Two Architectures, One Question
Every system that lets a browser send something "away" to be processed has to answer a deceptively simple question: where does that data actually live while the work is happening, and for how long? Two architectural answers dominate modern web engineering, and both show up, side by side, across Secure Academic Studio's own tools.
One answer keeps the payload small, anonymous, and disposable: it exists only for the lifetime of a single request, inside a server process's memory, and is gone the instant the response is sent. The other accepts that some data has to persist somewhere — because the file is large, the processing takes minutes rather than milliseconds, or the workflow has to survive a dropped connection — and builds a deliberate infrastructure of time-limited access and provable deletion around that persistence.
This article is not a ranking. Neither pattern is "more secure" in the abstract; each is the correct answer to a different set of constraints, and choosing the wrong one for a given job creates real problems — either an architecture that silently struggles with large files, or one that carries deletion obligations it never needed to take on. What follows is a code-level look at how each pattern actually moves bytes from a browser to an AI model and back, what each one costs in engineering complexity, and, in the closing section, which of our own applications uses which pattern and why.
2. What “Stateless” Means at the Code Level
"Stateless" is often used loosely to mean "the HTTP request is self-contained," which is trivially true of almost every API call. The narrower, more useful meaning used here is stricter: the payload itself — the actual file content being processed — is never written to a database, a disk, or an object store at any point in its journey. It exists as a variable in the browser, then as an in-memory buffer inside a server process, and then as nothing.
2.1. Client Side: Building and Sending the Payload
In Secure Academic Studio's document converters, a PDF page is rendered onto an HTML canvas element entirely inside the browser, flattened into a compact image, and base64-encoded. The "upload" is not a separate step at all — it is folded into the very same request that asks for the data to be extracted:
const response = await fetch('/api/extract-statement-batch', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: bodyData, // { images: [...], token }
signal: controller.signal
});
The entire "upload" is a single POST request. There is no distinct storage step to sequence before it — the images travel inside the very request that asks for them to be processed.
2.2. Server Side: Process, Respond, Forget
On the backend, the only thing written to a database is a credit-ledger entry — a number and a token, never a byte of the submitted image:
// Atomic deduction: only succeeds if credits >= totalCost
const deduct = await db.run(
'UPDATE wallets SET credits = credits - ? ' +
'WHERE token = ? AND credits >= ?',
[totalCost, token, totalCost]);
if (!deduct || deduct.changes === 0) {
return res.status(402).json({ error: "CREDIT_ERROR: Insufficient credits." });
}
// ...AI call... on failure: rollbackCredits() returns the deducted credit
The atomic UPDATE is the only durable side effect of the whole request. The image data itself is passed straight into the AI SDK call as an in-memory value and, once the response has been serialised and sent, nothing in the process still holds a reference to it — it becomes eligible for garbage collection like any other short-lived object.
Note: this is why a stateless endpoint has no matching
delete-resultroute. A deletion endpoint presupposes that something was stored in the first place. Here, nothing was — which makes "nothing to delete" not a policy promise but a direct, verifiable consequence of the code path: there is no line anywhere that writes the payload to disk or to a database.
3. What “Signed-URL” Processing Means at the Code Level
A signed URL is a time-limited, scope-limited credential issued by a cloud storage provider. It authorises exactly one operation — for example, "write to this one object path, for the next hour" — without ever routing the underlying bytes through the application's own backend. This is a deliberate departure from the stateless model: it introduces a persistence layer on purpose, because the task at hand genuinely needs one. An audio file can run for hours; a lengthy document can take real wall-clock time to review. Neither fits comfortably inside a single synchronous HTTP request.
3.1. Server Side: Issuing a Time-Limited Upload Ticket
app.post('/api/transcribe/get-upload-url', transcribeLimiter, async (req, res) => {
const { token, fileName, contentType } = req.body;
// Token check (does the wallet exist?)
const wallet = await db.get('SELECT credits FROM wallets WHERE token = ?', [token]);
if (!wallet) return res.status(404).json({ error: 'CREDIT_ERROR' });
const objectName = `pending/${Date.now()}_${rand}.${ext}`;
// v4 signed URL, 'write' permission, 1-hour expiry
const options = { version: 'v4', action: 'write',
expires: Date.now() + 60*60*1000, contentType };
const [uploadUrl] = await gcsStorage.bucket(GCS_BUCKET)
.file(objectName)
.getSignedUrl(options);
// Register the object in the lifecycle tracker
await db.run('INSERT INTO gcs_lifecycle_tracking ' +
'(object_name, created_at, status) VALUES (?, ?, ?)',
[objectName, Date.now(), 'uploading']);
return res.status(200).json({ uploadUrl, objectName });
});
Two things happen here that have no equivalent in the stateless path. First, the backend deliberately creates a persistent record — a lifecycle-tracking row — before a single byte of the file exists. Second, it hands the client a credential rather than a data channel: the backend never asks to see the file, only to be told where it will land.
3.2. Client Side: Uploading Directly to Storage
// The frontend PUTs to the signed URL — directly to cloud storage
const xhr = new XMLHttpRequest();
xhr.open('PUT', uploadUrl, true);
xhr.setRequestHeader('Content-Type', mimeType);
xhr.upload.onprogress = (e) => { /* update progress bar */ };
xhr.send(audioFile);
The data path is browser → cloud storage, bypassing the application's own backend entirely. This is a structural guarantee, not merely a promised one: even a fully compromised backend has no code path through which the raw file could pass, because it was never routed there to begin with.
Because the file now genuinely persists somewhere, the architecture takes on an obligation the stateless path never had: it must be able to prove, not merely promise, that the object is deleted once it is no longer needed. Our own Privacy and Security Architecture document describes this in full: five independent layers, from a finally-block deletion immediately after processing to an hourly orphan-sweeper and a cloud-provider lifecycle rule as the ultimate backstop.
4. Side by Side: What Each Pattern Optimises For
Laid out next to each other, the two patterns pull in almost opposite directions on every axis except the one that matters most — both, correctly implemented, keep the raw content out of reach of anyone who should not have it.
| Stateless (in-memory, synchronous) | Signed URL (object storage, asynchronous) | |
|---|---|---|
| Data path | Browser → backend → AI → backend → browser, one request | Browser → cloud storage (backend bypassed), processed later |
| Where the payload lives | RAM only, for the duration of one HTTP request | On a storage bucket, until explicitly deleted |
| Practical ceiling | Small payloads; processing that fits inside one request/timeout window | Large files and/or processing that can run for minutes or hours |
| Dropped connection | Client simply retries the whole request; nothing to clean up server-side | Upload can resume, but an orphaned object may now exist and needs cleanup |
| Deletion logic required | None — nothing was ever stored | Yes — explicit delete calls, lifecycle rules, and a scheduled sweep |
| Backend complexity | Low: one endpoint, one code path | Higher: URL issuance, status tracking, a processing trigger, deletion, and a sweeper job |
| Auditability | "We never had it" is directly verifiable from the absence of storage calls in the code | Requires demonstrating the deletion guarantees themselves, layer by layer |
5. The Price of Persistence: Why Signed URLs Need a Deletion Story
The moment an architecture accepts that a file will persist somewhere, even briefly, it takes on a responsibility the stateless model never had to consider: proving deletion rather than merely no longer needing it. This is why a signed-URL-based tool like our Interview & Speech Transcriber needs five independent deletion layers rather than one delete call — two enforced at the storage-infrastructure level (soft-delete and object versioning disabled on the bucket) and three enforced in application code (a finally-block deletion after processing, a client-initiated confirmation flow with automatic retries, and an hourly orphan-sweeper), with a native cloud-provider lifecycle rule as the statistical last resort.
None of this is over-engineering for its own sake — it is the direct cost of the persistence the workload required in the first place. A three-hour audio recording or a long academic manuscript simply cannot be pushed through a single synchronous request the way a single-page bank statement can; once you accept that the file has to live somewhere the backend can reach it later, "somewhere" needs its own, independently verifiable exit plan.
The stateless converters need none of this — not because they are less careful, but because the architecture removes the entire category of risk before it can arise. As we put it while auditing our own Bank Statement and Invoice Converters: there is no file, on either side of the request, for a deletion mechanism to act on. The guarantee is not fast deletion; it is the absence of a store to delete from.
6. Choosing Between Them: Engineering Rules of Thumb
None of the following rules are specific to any one vendor's SDK; they apply to any system deciding between an in-memory, synchronous design and a persistent, signed-URL one.
- Payload fits comfortably in a normal request (a few megabytes) and processing finishes in seconds — favour stateless. There is little to gain, and a real privacy cost to pay, by making that data durable even briefly.
- The file is large (tens or hundreds of megabytes) or processing routinely runs longer than a browser tab can be expected to wait synchronously — favour signed URLs. Synchronous HTTP requests have practical timeout ceilings that large media and long AI passes will eventually hit.
- You need resumable uploads, or the workflow must survive a dropped connection mid-transfer — favour signed URLs, since object storage clients are generally built to resume from partial uploads.
- You want the strongest possible provable-non-retention claim for an audit — favour stateless wherever the workload allows it. "We never stored it" is a stronger and simpler claim than "we stored it and can prove we deleted it."
- You need a durable job record you can poll, retry, or notify about later (background processing, webhooks, long-running queues) — favour signed URLs, because that kind of workflow needs somewhere for state to live between requests.
- Every piece of persistence infrastructure you add is also something you can get wrong. Bucket configuration, lifecycle rules, and a sweeper cron job are all additional surface area to configure correctly and keep correctly configured; the stateless path simply has fewer moving parts to misconfigure.
7. Why Secure Academic Studio Uses Both
Both patterns are in production, in the same codebase, applied to the workload each one actually fits.
The Bank Statement Converter (single-file and batch), and the Invoice Converter (single-file and batch), use the stateless path. Their payload is one or a handful of flattened page images, comfortably small, and Gemini Vision returns a result in seconds. There is nothing to gain by ever making that data durable — and, from a privacy standpoint, everything to gain by structurally guaranteeing that it never becomes so.
The Interview & Speech Transcriber and the Academic Proofreader use the signed-URL, GCS-backed path. Audio recordings can run up to three hours and reach into the hundreds of megabytes; academic documents can be long, and a thorough AI review pass takes real wall-clock time. Both workflows need to survive a user closing their laptop mid-upload, and both need a job the backend can check on later — which means the file has to live somewhere the backend, not just the browser tab, can still reach it. That requirement is exactly what justifies taking on the five-layer deletion architecture described in our Privacy and Security Architecture document.
Neither choice was made by default. Each is the deliberate, narrowest architecture that the workload in question actually requires — which, for an auditor or a fellow engineer, is a more convincing signal than either pattern used everywhere out of habit.
In summary: stateless processing is the stronger claim whenever the workload allows it — not "we deleted your data quickly," but "your data was never in a place that needed deleting." Signed-URL, object-storage-based processing is the correct and necessary alternative whenever the workload does not allow it — and in that case, the deletion guarantees have to be built, documented, and layered, precisely because the convenience of persistence was not free.
Secure Academic Studio — Engineering Notes
Code excerpts in this article illustrate the key mechanisms discussed and do not constitute the complete source code.