Back to Home

Stateless vs. Signed-URL Architecture: A Technical Comparison

A Technical Comparison for Engineers and the Technically Curious

Related reading: Privacy and Security Architecture — the full, code-level audit this article draws its examples from.
A general architecture discussion, illustrated throughout with sanitised excerpts from Secure Academic Studio's own tools

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-result route. 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.

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.