Privacy and Security Architecture
Table of Contents
1. Introduction: From Promise to Proof
A privacy promise is cheap. Any website can claim to be "100% GDPR-compliant", that it "stores no data", or that "all processing takes place within the EU". Public security tests — A+ ratings, 100% badge scores — measure the transport layer (TLS configuration, HTTP headers, certificates), but say nothing about what happens under the bonnet: how the application actually handles user data during processing.
This document is designed to bridge that gap. It is not a marketing piece — it is architectural evidence. For a knowledgeable reader — an auditor, security professional, or developer — it makes the claims the platform presents in its user interface verifiable at the code level. Where possible, sanitised code excerpts demonstrate how each mechanism works. The excerpts focus on the key decision points; they do not expose the full source code, but they are sufficient to show exactly where each privacy decision is made.
How to read this document: This document is deliberately nuanced. It presents not only strengths, but also consciously accepted trade-offs. Real engineering systems always involve trade-offs; naming them openly — rather than concealing them — is precisely what distinguishes an auditable system from a marketing claim.
The platform comprises three independent applications, each facing a different privacy challenge, and each applying a different architectural strategy suited to that challenge:
- Interview & Speech Transcriber — processing large audio files. Challenge: file size and long processing time. Answer: EU data-handling guarantees and multi-layer deletion assurance.
- Bank Statement Converter — processing financial documents. Challenge: extremely sensitive personal data. Answer: data minimisation and user-verifiable, irreversible redaction.
- Secure Academic Proofreader — analysing academic texts. Challenge: the full document content as sensitive data. Answer: session-level privacy and client-side encryption.
2. Shared Architectural Foundations
Before examining the individual applications, it is worth reviewing the security and privacy mechanisms that run throughout the entire platform. These are implemented once at the backend (server.js) level, and every application inherits them.
2.1. The GDPR Compliance Guard — the server cannot start with an invalid configuration
In most systems, compliance is a matter of a configuration setting that can be misconfigured. Here, the backend imposes a start-up precondition: if the Google Cloud credential is missing or invalid, the process halts immediately. The application is physically incapable of starting in a broken state.
// GDPR COMPLIANCE GUARD — mandatory check at start-up
// If this block throws, the server CANNOT start.
if (!process.env.GOOGLE_CREDENTIALS_JSON) {
console.error('FATAL: GOOGLE_CREDENTIALS_JSON is not set. ' +
'Server cannot start due to EU GDPR compliance requirements.');
process.exit(1);
}
try {
JSON.parse(process.env.GOOGLE_CREDENTIALS_JSON);
} catch (e) {
console.error('FATAL: GOOGLE_CREDENTIALS_JSON is not valid JSON.');
process.exit(1);
}
Excerpt from the backend start-up sequence. The process.exit(1) guarantees there is no intermediate "started but misconfigured" state.
This is complemented by explicitly pinning the Vertex AI to the EU region. The AI SDK is initialised with the location: 'eu' parameter, which constrains all data processing to EU multi-region infrastructure:
// Strict EU-only Vertex AI routing (100% EU Jurisdiction)
ai = new GoogleGenAI({
vertexai: true,
project: process.env.GOOGLE_CLOUD_PROJECT,
location: 'eu'
});
All AI calls remain within EU jurisdiction. From an auditor's perspective, this claim is directly verifiable from the code.
Auditor's note: "EU-only processing" here is not a statement of intent — it is an invariant enforced by the code. Compliance is not a runtime toggle: if the EU credentials are missing, there is no running server to audit.
2.2. Defence Middleware: Helmet, CSP and Attack-Pattern Filtering
The backend uses the helmet package to enforce a strict Content Security Policy (CSP). The core principle is defaultSrc: 'none' — everything is blocked by default, and only explicitly whitelisted sources may run. Scripts and styles may only be loaded from the application's own origin ('self'), which drastically reduces the XSS attack surface.
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'none'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'"],
imgSrc: ["'self'", "data:", "blob:"],
connectSrc: ["'self'", "blob:", "https://storage.googleapis.com"],
objectSrc: ["'none'"],
frameAncestors: ["'none'"],
upgradeInsecureRequests: [],
}
},
hsts: { maxAge: 31536000, includeSubDomains: true, preload: true },
referrerPolicy: { policy: 'no-referrer' },
frameguard: { action: 'deny' }
}));
The CSP is whitelist-based: connectSrc allows only the application's own server and Google Cloud Storage — precisely what the Signed URL architecture requires, and nothing more.
The platform serves all JavaScript libraries and fonts locally (noted in the CSS as Zero Tracking). There are no external CDN calls and no third-party scripts loaded — a benefit that is simultaneously a privacy advantage (no external tracking) and a security advantage (no supply-chain risk through an external CDN).
The backend also runs a lightweight URL-level attack-pattern filter (against path traversal, SQL injection, and XSS patterns), and blocks by user-agent those AI scrapers that do not respect the robots.txt file.
2.3. Rate Limiting — Tuned per Endpoint
Every sensitive endpoint sits behind its own rate limiter, calibrated to the risk profile of that endpoint. Token verification and uploads are subject to stricter limits than general API calls; feedback submission is the most restrictive of all (five requests per hour) to prevent spam.
const tokenLimiter = rateLimit({ windowMs: 15*60*1000, max: 30 });
const feedbackLimiter = rateLimit({ windowMs: 60*60*1000, max: 5 });
const transcribeLimiter = rateLimit({ windowMs: 15*60*1000, max: 30 });
const statusLimiter = rateLimit({ windowMs: 60*1000, max: 120 });
The limiters are differentiated: status polling is permissive (frequent, low risk); feedback is extremely strict (abuse-prone).
2.4. Account-Free, Anonymous Access: the Credit Pack Model
The platform deliberately avoids user accounts. There is no registration, no email address, no password. Access is provided by a Credit Pack Code (format: SAU-XXXX-XXXX-XXXX), which is tied to a simple credit balance in the server-side database. This is a data-minimisation decision: the system simply does not hold personal identifiers, because it does not collect them.
The free trial allocation uses IP-address-based abuse prevention, but the raw IP address is never stored. Instead, a daily-salted SHA-256 hash is computed:
const today = new Date().toISOString().split('T')[0];
const salt = process.env.FREE_TOKEN_SALT;
const ipHash = crypto.createHash('sha256')
.update(clientIp + today + salt)
.digest('hex');
// The raw IP is never stored — only the hash.
The IP address is immediately converted into a one-way hash. Because the salt rotates daily, the hash changes every day, making it unsuitable for long-term tracking.
Auditor's note: The anonymous Credit Pack model surpasses traditional account-based systems from a privacy standpoint: there is no password database to breach, no email list that can leak, and payment (Creem) is decoupled from usage. At the moment of use, the system does not know who the user is.
3. Interview & Speech Transcriber
The challenge: processing large audio files (interviews potentially hours in length), which may contain conversations, personal opinions, and third-party data. Processing takes a long time, and file sizes preclude simple synchronous handling.
The architectural answer: the audio file never touches the application's own backend server, it remains in the EU, and it is deleted through multiple independent mechanisms once processing is complete.
3.1. The Signed URL Architecture — the backend never sees the audio file
This is the most important decision in the design. The typical transcription service flow: the user uploads the file to the provider's backend, which stores it and then forwards it to the AI. Here, by contrast, the file goes directly to Google Cloud Storage (GCS) via a time-limited, signed upload URL (Signed URL). The backend only generates the URL — it never sees, buffers, or stores the file's contents.
Step 1 — the backend generates a signed URL (without receiving the file):
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 GCS 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 });
});
The backend returns only a time-limited, write-scoped URL. The audio file's bytes never pass through it.
Step 2 — the browser uploads directly to GCS (from the client-side code):
// 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: browser → GCS. The application's own backend is bypassed entirely.
Auditor's note: This decision is architecturally robust: even a fully compromised application backend would have no access to the raw audio files, because they never pass through it. The backend delegates access — it does not relay data.
3.2. The Multi-Layer Deletion Guarantee
Most services fulfil the "immediate deletion" promise with a single, potentially unreliable API call. Here, three independent layers ensure the audio file does not remain in cloud storage.
3.2.1. Layer: the processing job's finally block
The background processing function deletes the audio file in a finally block — meaning the deletion runs whether the AI processing succeeds or fails. There is no error path that leaves the file in cloud storage.
} finally {
let gcsDeleted = false;
if (gcsStorage) {
try {
await gcsStorage.bucket(GCS_BUCKET).file(objectName).delete();
gcsDeleted = true;
console.log('Cleanup: Audio permanently deleted from GCS.');
} catch (e) {
if (e.code === 404) gcsDeleted = true; // already gone
else console.error('GCS Cleanup failed for object.');
}
}
if (gcsDeleted) {
await db.run('DELETE FROM gcs_lifecycle_tracking ' +
'WHERE object_name = ?', [objectName]);
}
}
The finally block is guaranteed to execute in JavaScript's error-handling model. The 404 case (file already deleted) is treated as a successful deletion — idempotent behaviour.
3.2.2. Layer: user-initiated On-Demand Erasure
Once the transcript is ready, the client sends an explicit deletion request for the result object. The endpoint verifies that the request comes from the legitimate owner (by requiring both job_id and token to match), and only then proceeds with deletion.
app.post('/api/transcribe/delete-result', transcribeLimiter, async (req, res) => {
const { jobId, token } = req.body;
// Only the legitimate owner may delete (job_id AND token must match)
const job = await db.get('SELECT * FROM transcription_jobs ' +
'WHERE job_id = ? AND token = ?', [jobId, token]);
if (!job) return res.status(404).json({ error: 'Job not found' });
const resultObjectName = `results/${jobId}.json`;
await gcsStorage.bucket(GCS_BUCKET).file(resultObjectName).delete();
await db.run('DELETE FROM gcs_lifecycle_tracking ' +
'WHERE object_name = ?', [resultObjectName]);
return res.status(200).json({ success: true, deleted: true });
});
The ownership check prevents anyone else from deleting (or probing the existence of) another user's job through the deletion endpoint.
3.2.3. Layer: the Orphan Sweeper (hourly cleanup job)
If, for any reason (network error, browser crash), a file remains in cloud storage, a background process running every hour detects and deletes objects older than 3 hours.
async function orphanSweeper() {
const threshold = Date.now() - (3 * 60 * 60 * 1000); // 3 hours
const orphans = await db.all(
'SELECT object_name FROM gcs_lifecycle_tracking WHERE created_at < ?',
[threshold]);
for (const orphan of orphans) {
await gcsStorage.bucket(GCS_BUCKET).file(orphan.object_name).delete();
await db.run('DELETE FROM gcs_lifecycle_tracking ' +
'WHERE object_name = ?', [orphan.object_name]);
}
}
setInterval(orphanSweeper, 60 * 60 * 1000); // every hour
The lifecycle-tracking table (gcs_lifecycle_tracking) ensures no object can be "lost" without a record.
GDPR connection: Together, the three layers constitute an auditable, documentable implementation of Article 5(1)(e) of the GDPR — the "storage limitation" principle. Storage is restricted to the period strictly necessary for processing, and deletion is guaranteed through multiple independent mechanisms.
3.3. Client-Side Architecture: Parsing and Export
Hybrid duration parser. Before anything is sent to the server, the browser reads the audio file's duration at the binary level (OGG granule position, MP4 mvhd atom, MP3 Xing/Info frame). Length and size validation therefore happens client-side, before upload — a file that is too long or invalid never starts its journey to the cloud.
Client-side export. Exporting the completed transcript to TXT, DOCX, and PDF takes place entirely in the browser (using locally loaded docx.js and pdf-lib libraries). The transcript content itself arrives in the browser directly from GCS via a signed URL, and the exported document never returns to the backend server.
3.4. Accepted Trade-Off
Nuance — what an auditor would raise: The Gemini model accesses the audio file in GCS via a
gs://URI, meaning Google (as a data processor) has access to the content during processing. This is covered by Vertex AI Enterprise contractual guarantees: the API payload is not used for model training, and the data remains in the EU. This is not a hidden risk — it is an openly managed, documented data-processing relationship governed by Google Cloud's Data Processing Addendum (DPA). The platform's role is to minimise the window and scope of exposure to this relationship.
4. Bank Statement Converter
The challenge: processing bank statements — some of the most sensitive financial and personal data in existence: names, addresses, account numbers, transaction history.
The architectural answer: sensitive data is technically incapable of leaving the user's device before redaction — and this is verifiable by the user.
4.1. Client-Side Pixel Destruction — Redaction as an Irreversible Operation
PDF processing begins entirely in the browser. pdf.js (a locally loaded library) renders the PDF on a Canvas element on the client's machine. The user draws black boxes over sensitive fields, and the code burns those boxes directly into the Canvas pixels via ctx.fillRect(). This is not a CSS overlay and not a visual layer: the redacted pixels are physically destroyed.
// Burning the masks into the canvas pixels
const masksToDraw = [];
globalRedactions.forEach(m => masksToDraw.push(m));
if (pageRedactions[p]) pageRedactions[p].forEach(m => masksToDraw.push(m));
ctx.fillStyle = "black";
masksToDraw.forEach(mask => {
ctx.fillRect(
(mask.pctX / 100) * canvas.width, (mask.pctY / 100) * canvas.height,
(mask.pctW / 100) * canvas.width, (mask.pctH / 100) * canvas.height
);
});
// Flattening the redacted canvas into an image (the original PDF layer is discarded)
allImages.push(canvas.toDataURL('image/webp', 0.80));
The output is a flat WebP image. The original PDF's text layer, metadata, and redacted pixels are gone — they cannot be recovered.
The key distinction from market competitors: typical online PDF redaction tools upload the PDF to their server, where redaction takes place. At that point, the sensitive data — names, account numbers, addresses — has already left the user's device before redaction occurs. Here the opposite is true: the raw PDF never leaves the browser.
Auditor's note: This is one of the rare cases where a privacy claim is not a matter of trust, but a directly verifiable fact from the client-side code. By monitoring the browser's network traffic (DevTools → Network), one can confirm that the raw PDF never leaves the client in any form — only the already-redacted, flat image is transmitted.
4.2. The Audit Payload ZIP — Verifiable Transparency
Before extraction begins, the user can download a ZIP package containing exactly the images that the AI will receive — the already-redacted WebP files. The user can therefore verify with their own eyes that the sensitive data has been removed before sending anything.
// Building the "Audit Payload": the same redacted images the AI will receive
for (let p = 1; p <= pdfDoc.numPages; p++) {
await page.render({ canvasContext: ctx, viewport: viewport }).promise;
ctx.fillStyle = "black";
masksToDraw.forEach(mask => { ctx.fillRect(/* ...redaction... */); });
const base64Data = canvas.toDataURL('image/webp', 0.80).split(',')[1];
zip.file(`redacted_page_${p}.webp`, base64Data, { base64: true });
}
const content = await zip.generateAsync({ type: "blob" });
// Downloaded to the user's device for inspection
The user can download and examine precisely what the AI will see. There is no discrepancy between what is previewed and what is submitted.
GDPR connection: This is an unusually strong technical implementation of Article 5(1)(a) of the GDPR — the "transparency" principle. Most systems fulfil transparency through a written statement; here the user can verify byte-for-byte what is submitted for processing.
4.3. Stateless Processing — the Principle of Proportionality
Unlike the Transcriber, the Converter does not use GCS, a lifecycle tracker, or an orphan sweeper — and this is a deliberate, correct decision. The redacted images travel to the backend as base64-encoded JSON in a single synchronous request; the backend immediately forwards them to the Gemini Vision model and returns the response. The data has no "residence" on the server: no database writes, no disk writes.
If the server were to crash mid-processing, the redacted images would simply be lost from memory — which in this case is precisely the desired behaviour. The architecture involves exactly as much infrastructure as is strictly necessary, and no more.
Credit integrity and automatic refund. The backend deducts credits using an atomic SQL operation:
// 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 eliminates race conditions: a negative balance cannot occur, even with concurrent requests.
Memory cleanup at the code level. After the API call payload is assembled, the code explicitly empties the array holding the images (allImages.length = 0), so that the base64 data does not remain unnecessarily in browser memory. This is not a required step — its presence indicates careful, data-aware development.
4.4. Accepted Trade-Off
Nuance — what an auditor would raise: Unlike the Transcriber, the redacted images here pass through the application's own backend server (as base64 JSON) before reaching Gemini — there is no Signed URL. This is a consciously accepted simplification. The key point, however, is that what the backend sees is already the redacted image: the sensitive pixels were destroyed on the user's device before any network transmission. The image the backend sees is identical to the image the AI sees — and the same image the user was able to inspect in advance via the Audit ZIP.
5. Secure Academic Proofreader
The challenge: analysing academic papers, research materials, and manuscripts. Here, the processed content itself is the sensitive data — intellectual property, unpublished research, and potentially personally identifiable text.
The architectural answer: session-level privacy and genuine client-side encryption, enabling the secure local storage of work in progress.
5.1. AES-GCM Encrypted .SAU Session File
This is the point at which the Proofreader goes beyond the privacy approach of the other two applications. The complete session state — document content, identified errors, accepted and rejected corrections — can be saved to a .sau file, which may optionally be protected with AES-256-GCM encryption, using the browser's native Web Crypto API. The key is derived from a user-supplied password via PBKDF2 (100,000 iterations, SHA-256) with a random salt.
async function deriveKey(password, salt) {
const keyMaterial = await window.crypto.subtle.importKey(
"raw", new TextEncoder().encode(password),
{ name: "PBKDF2" }, false, ["deriveKey"]);
return window.crypto.subtle.deriveKey(
{ name: "PBKDF2", salt: salt, iterations: 100000, hash: "SHA-256" },
keyMaterial, { name: "AES-GCM", length: 256 }, false,
["encrypt", "decrypt"]);
}
// Encryption: random salt (16 bytes) + IV (12 bytes) + AES-256-GCM
const salt = window.crypto.getRandomValues(new Uint8Array(16));
const iv = window.crypto.getRandomValues(new Uint8Array(12));
const key = await deriveKey(pwd, salt);
const ciphertext = await window.crypto.subtle.encrypt(
{ name: "AES-GCM", iv: iv }, key, enc.encode(payload));
Standard, correctly parameterised cryptography: unique salt and IV for every save, authenticated encryption (GCM mode). The password and the derived key never leave the browser.
Auditor's note: This is the only one of the three applications capable of maintaining an encrypted, persistent state on the user's own machine — entirely bypassing the server. Encryption happens on the client; the key is never transmitted. Competing academic tools typically store session state on their own servers, with no client-side encryption option.
5.2. HTML Tokenisation — Separating Structure from Content
The DOCX is converted to HTML by mammoth.js in the browser. Before the content reaches the AI, the code replaces HTML tags with tokens ([__T_0__], [__T_1__], etc.). This serves two purposes simultaneously: it improves AI accuracy (the model does not conflate content with structure), and — from a privacy perspective — ensures that only the tokenised text is uploaded to GCS, without the DOCX's internal metadata (author, tracked-changes author names).
function tokenizeHtml(htmlStr) {
return htmlStr.replace(/<[^>]+>/g, (match) => {
// Emphasis tags are preserved (em, i, b, strong)
if (/^<\/?(em|i|b|strong)\b[^>]*>$/i.test(match)) return match;
// All other tags become tokens; the original is stored in a map
let token = `[__T_${globalTokenCounter}__]`;
tagMap[token] = match;
globalTokenCounter++;
return token;
});
}
The tag map (tagMap) remains in the browser. Detokenisation (reassembling the HTML) also happens client-side during export.
This is complemented by the Mammoth Artifact Cleaner, which strips internal footnote back-reference links generated during conversion, before the text enters processing — so that superfluous internal document identifiers do not reach the cloud.
5.3. Signed URL and the Content's Journey
As with the Transcriber, document content is uploaded directly to GCS via a signed URL — the backend never sees the raw text. The /api/proofread/start call passes only metadata (character count, selected modules, language code) through the backend; the content itself does not.
Integrity protection (fraud detection). Before processing begins, the backend fetches the actual size of the uploaded file from GCS and compares it with the character count declared by the client. A significant discrepancy causes the job to fail with a fraud_detected error. This simultaneously protects the credit system's integrity and prevents a payload substantially larger than declared from being processed.
Client-side export. Generating the corrected document and the errata report takes place entirely in the browser. The SmartReplace engine applies corrections to the tokenised content, detokenisation is client-side, and the finished HTML file never returns to the backend.
5.4. Accepted Trade-Off
Nuance — what an auditor would raise: The
.saufile encryption is optional: if the user does not provide a password when saving, the file contains unencrypted JSON. This is a deliberate, user-controlled decision (convenience vs. protection), but it should be explicitly documented in an audit. During processing, the full document text resides in a GCS object — this is covered by precisely the lifecycle tracking and deletion guarantees described in the Transcriber section.
6. Comparative Summary
The three applications do not copy the same template — each applies a proportionate privacy strategy suited to its own use case. This in itself is an auditor's virtue: applying the right level of protection — no more and no less — signals that genuine engineering judgement underlies the architecture.
| Transcriber | Converter | Proofreader | |
|---|---|---|---|
| Data processed | Audio file (large, long processing) | Financial PDF (maximum sensitivity) | Academic text (intellectual property) |
| Data path | Browser → GCS (backend bypassed) | The raw PDF never leaves the browser | Browser → GCS (backend bypassed) |
| Key mechanism | Signed URL + three-layer deletion | Client-side pixel destruction + Audit ZIP | AES-256-GCM .SAU + tokenisation |
| Strength | EU guarantees and deletion assurance | Data minimisation and verifiable redaction | Session privacy and client-side encryption |
In one sentence: the Transcriber excels at EU data-handling guarantees and deletion assurance; the Converter at data minimisation and verifiable redaction; the Proofreader at session privacy and client-side encryption.
6.1. Closing Auditor's Assessment
The platform's privacy and security architecture is not "checkbox compliance". Privacy considerations are built into the structure of the code — not added as an afterthought in a policy document. The mechanisms presented here — the Signed URL architecture, the three-layer deletion guarantee, client-side pixel destruction, the Audit ZIP, the AES-GCM session file, the GDPR Compliance Guard — are all decisions directly verifiable from the code.
That is precisely the purpose of this document: it does not claim that the applications are secure — it shows how they are secure, with enough detail to allow an auditor or developer to conduct an independent review. The code excerpts are sanitised (focused on the key mechanisms, not exposing the full implementation), but they are sufficient for the critical privacy decision points to be validated.
In summary: A promise is cheap; proof is expensive. This platform chose the expensive option: its privacy claims are implemented not in text, but in architecture and code — where a professional can verify them.
Secure Academic Studio — Architectural Documentation
Code excerpts in this document illustrate the key mechanisms and do not constitute the complete source code.