Security and Privacy
For technical reviewers: hospital IT departments, information security teams and privacy offices evaluating BAPa for institutional use.
Every claim in this document is drawn from the application's source code, and can be checked against the running application without our assistance. See Verifying these claims independently at the end.
Summary
BAPa has two distinct parts, and they have different privacy properties. Conflating them is the single most common misreading, so this document separates them throughout.
The analysis tool (/tool) | The research chat (/research) | |
|---|---|---|
| Who may use it | Approved accounts only | Anyone, no sign-in |
| What it handles | Your laboratory data file | Typed questions about published research |
| Where processing happens | Entirely inside your browser | On our server and third-party AI services |
| Does data leave the device | No | Yes. Questions, and dictated audio if used |
Patient data is only ever loaded into the analysis tool, and the analysis tool does not transmit
it. The claim in this document is scoped deliberately: it is a statement about /tool, not about
every page on the site.
1. Where uploaded data is processed
In the browser, on the reviewer's own machine. The file is never uploaded.
When a user selects a spreadsheet, the browser hands it to a Web Worker, a background thread inside the same browser tab. That worker runs a complete Python environment compiled to WebAssembly (Pyodide), including pandas, NumPy, SciPy and matplotlib. The Sequential Difference Deviation analysis is performed there, and the resulting graph is drawn there.
At no point is the file, any part of its contents, or any statistic derived from it sent to a server. There is no server-side analysis component. If the machine were disconnected from the network after the page finished loading, the analysis would still run to completion.
This is verifiable from outside the application: open the browser's developer tools, switch to the Network tab, and load a file. No request carrying the file appears, because none is made.
2. Whether patient data are transmitted over the internet
No patient data is transmitted from the analysis tool.
The application's source contains no network call of any kind within the analysis tool's code: no
fetch, no XMLHttpRequest, no WebSocket, no sendBeacon. The file is passed to the Web Worker by
an in-memory transfer, which is a browser-internal operation and not a network operation.
Three kinds of network traffic do occur while the tool is open. None of them carries file content.
Loading the analysis engine. On first use, the browser downloads the Pyodide runtime and its
scientific packages (NumPy, SciPy, pandas, matplotlib, scikit-learn, statsmodels and micropip) from
the public jsDelivr CDN. These are ordinary asset downloads, identical for every user, made before
any file is chosen. The openpyxl spreadsheet library and the analysis code itself are served from
our own origin rather than from a package index, specifically so that a network which filters PyPI
does not break the tool. See §7 for the reviewer decision this implies.
Signing in. Authentication is handled by Clerk. This exchanges account credentials and session tokens only.
Crash reports. If the tool encounters an unrecoverable error, an anonymised report is sent so the fault can be diagnosed. This deserves precision, because it is the one deliberate outbound channel from the tool, and its contents are constrained by design rather than by convention.
What a crash report contains
The application's source carries an explicit privacy contract governing this report. File names, worksheet names, column headers, cell values, row counts and anything typed by the user are prohibited from it.
That prohibition is enforced structurally, not by careful authoring. The Python analysis code writes
identifiers into its error messages, for example Worksheet 'X' is not in this workbook. So error
messages are never transmitted as text. Each message is matched in full against a fixed list of
known error shapes, and only a category label is sent. A message that matches nothing is reported as
the literal word unrecognized. It is not hashed, because a short and guessable message could be
recovered from a hash.
Stack traces are reduced to file-and-line positions within our own JavaScript bundle. Function names are discarded, and any frame that does not match an allowlisted path on our own origin is dropped entirely.
The complete set of transmitted fields is: an error class name, the category label described above, the reduced stack, the React component tree, which error boundary caught it, the build identifier, the analysis engine's version numbers, the device's reported memory and CPU core count, the browser user-agent string, the window dimensions, and a trail of up to twenty breadcrumbs recording which worker function was called and how long it took, never what it was called with.
Reports are capped at five per session, deduplicated, and limited to 64 KB. They are received by our own web server and written to its runtime log. They are not sent to any third-party error-tracking service, and are not stored in a database.
This contract is covered by an automated test that loads a file deliberately named with a patient-shaped name, a medical record number and a site name, triggers a genuine crash, captures the actual outbound request, and fails if any of those strings appear in it.
3. What information is stored, locally and remotely
On the user's device
| What | Where | Contents | Lifetime |
|---|---|---|---|
| Sign-in session | Browser cookies, set by Clerk | Session token and authentication state | Managed by Clerk |
| Page-transition flag | sessionStorage | The single character 1, used to sequence an animation | Deleted on read; gone when the tab closes |
| Research chat transcript | localStorage | Questions and answers from the /research page only. Never anything from the analysis tool | Until the user clears it or clears site data |
Nothing from the analysis tool is written to any of these. The application uses no IndexedDB, no Cache Storage and no service worker. An uploaded file exists only in memory. See §4.
On our servers
The only database is a single table recording the published posters and papers in the research library: title, authors, year, publication, file name, size, content hash, storage key and processing status. Those files are academic publications uploaded by an administrator. They contain no patient data, and no user of the analysis tool contributes to that table.
Specifically, the server stores no user accounts, no email addresses, no account identifiers, no IP addresses, no chat transcripts and no record of tool usage. User accounts exist in Clerk; the application does not mirror them.
Research chat conversations are held in the browser and re-sent with each question. They are not written to any database.
4. How uploaded files are handled during and after analysis
During. Files up to 50 MB are read into memory as a byte array and transferred to the Web Worker.
The transfer detaches the array from the main thread, so exactly one copy exists. Larger .xlsx files
are handled differently: rather than loading the whole workbook into memory, a reference to the file
is passed to the worker, which reads ranges from disk on demand. Either way the data reaches Python
inside the browser and no further.
Results are returned as a graph image encoded directly into the page. "Save Analysis" invokes the browser's own print dialogue against a print-only stylesheet, so the PDF is produced by the browser on the user's machine, with no server-side rendering and no upload.
After. Nothing is written to disk and nothing is persisted, so there is nothing to erase. Closing the tab destroys the worker and everything it held.
Two details a reviewer should know precisely:
- "Restart" does not clear the analysis engine. Restarting the wizard resets the interface, but the worker still holds the parsed file in memory until a different file is loaded, which releases it, or the tab is closed. This is a deliberate trade: rebuilding the engine takes around a minute. It means a workstation left open on a restarted tool still has the previous file resident in browser memory.
- "Start the tool over", offered after a crash, does terminate the worker, discarding the Python environment and the file with it.
Neither state survives closing the tab, and neither is ever written to storage.
5. External services and APIs
Services contacted while the analysis tool is in use:
| Service | Purpose | What it receives |
|---|---|---|
| jsDelivr (CDN) | Delivers the Pyodide runtime and scientific packages | An asset request. The originating IP address and user-agent, as with any web request. No file data |
| Clerk | Authentication | Sign-in credentials and session tokens |
| Vercel | Hosts the web application | Ordinary web requests, and crash reports as described in §2 |
Services contacted by other parts of the site, listed for completeness because they are on the same domain:
| Service | Used by | What it receives |
|---|---|---|
| OpenRouter, routing to Google Gemini | Research chat | The typed question, recent conversation turns, and retrieved passages from the published corpus |
| Voyage AI | Research chat | The question text, converted to a numeric embedding |
| Qdrant Cloud | Research chat | The embedding and search filters |
| NIH / NLM (MedlinePlus, PubMed) | Research chat, for general medical questions | The typed question, as a search term |
| Deepgram | Voice input on the research chat | The recorded audio |
| OpenRouter | Voice input, transcript tidy-up | The transcript text, sent with retention and training explicitly disabled |
| Neon (PostgreSQL), AWS S3, AWS Lambda, OpenAI | Publication library and its ingestion | Published papers and posters uploaded by an administrator |
| YouTube | The interview page only | A standard video embed |
| Render | Hosts the backend service | Research chat and publication requests |
None of the services in the second table is reachable from the analysis tool. They belong to different pages, and the analysis tool makes no network calls at all.
The application sets a no-retention, no-training flag on the voice transcript request. It does not set that flag on research chat requests, which are therefore subject to the account's default terms.
Retention at each provider is governed by that provider's own terms and account settings, which sit outside this application. None of them receives laboratory data from the analysis tool.
6. Browser and platform requirements
- A modern browser with WebAssembly support. Current Chrome, Edge, Firefox and Safari all qualify. No minimum version is declared or tested against, so a specific lower bound cannot be stated.
- Web Workers must be permitted.
- Outbound HTTPS access to
cdn.jsdelivr.neton first load, and thereafter whenever the browser cache is cold. Without it the analysis engine cannot start. This is the requirement most likely to matter on a restricted hospital network. - Roughly a minute for the first load on a given machine, while the engine downloads and initialises. Subsequent loads are served from browser cache.
- For files over 50 MB, the browser must support synchronous file reads inside a worker. If it does not, the tool says so and asks for a smaller file rather than failing obscurely.
- No plugin, extension, installation or administrator privilege is required. Nothing is installed on the workstation.
- No
SharedArrayBufferand no cross-origin isolation are required. The application does not use multi-threading, and deliberately does not set theCOEPandCOOPheaders that would demand it. - The research chat's voice input additionally needs microphone permission and a secure (HTTPS) origin. It is not part of the analysis tool.
7. Known limitations of the security model
Stated plainly, because a reviewer will find them anyway.
- The analysis engine is fetched from a third-party CDN. Every cold start downloads Pyodide and its packages from jsDelivr. No file data is exposed, but it is a runtime dependency on an external provider, and jsDelivr observes the requesting IP address. An institution whose policy forbids third-party CDNs will need to decide on this specifically. Self-hosting these assets is possible and is the obvious remedy.
- The application sets no Content-Security-Policy, and no
Strict-Transport-Security,X-Frame-OptionsorReferrer-Policyheaders. TLS is terminated and enforced by the hosting platforms. - Client-side source maps are published. The original TypeScript is readable by anyone. This is intentional, since the analysis code is already served in the clear as readable Python, but it means the client implementation should be treated as public.
- There is no audit log of tool usage. Clerk records when an account signs in. Nothing records what an account then did. A question of the form "who ran an analysis, and when" cannot currently be answered.
- A user who has been granted access has no way to sign out. The sign-out control appears only on the pending-approval screen. On a shared workstation the session persists until the browser's cookies expire or are cleared.
- Restarting the tool leaves the previous file in browser memory until another file is loaded or the tab is closed. See §4.
- Access is granted by hand. An administrator edits a value in the Clerk dashboard. There is no approval workflow and no four-eyes check. The application sends no email of any kind and has no mail service behind it, so requesting access is a direct conversation rather than an automated queue. This is a deliberate choice for a licensed application with a small, known user base. See the Access Management document.
- The research chat is deliberately open to anyone, with no sign-in. It is a public research tool. It should not be used for anything patient-identifying, and its voice feature sends audio to a third-party transcription service.
- Error messages shown on screen may contain worksheet or column names taken from the loaded file. That is local display only, and those strings are never transmitted (§2). But they are also written to the browser's own console, where a session-recording tool or managed-browser extension installed by the institution could observe them. Nothing in this application reads the console.
8. Technical architecture
The analysis tool.
Browser tab
├── User interface React / Next.js
└── Web Worker <<< the file never leaves this box
└── Pyodide (Python compiled to WebAssembly)
├── pandas, NumPy, SciPy, matplotlib, scikit-learn, statsmodels
├── openpyxl served from our own origin
└── sdd_analysis.py the SDD implementation
The user's file passes from the file input to the Web Worker by an in-memory transfer and is parsed inside Pyodide. Results return as a base64-encoded PNG. There is no server in this path.
The wider application. A Next.js front end on Vercel serves all pages. A separate FastAPI service
on Render answers the research chat and serves the publication library. It holds a PostgreSQL table of
publication metadata (Neon), a vector index of the published corpus (Qdrant Cloud), and the PDFs
themselves (AWS S3, delivered by short-lived pre-signed links valid for five minutes). An AWS Lambda
function in us-west-2 ingests newly uploaded publications. The analysis tool touches none of
this.
Authentication. Clerk issues session tokens with a lifetime of about sixty seconds, continuously refreshed. Access to the analysis tool is decided on the server when the page is requested. The back-end service independently verifies each token's RS256 signature against Clerk's public keys and checks the issuer and the authorised party. It holds no Clerk secret and can only verify, never mint. Because permission is read from the token rather than cached, revoking an account's access takes effect everywhere within about a minute.
Isolation of the tool from the back end. There is no endpoint on the back end that could receive laboratory data. The only endpoints accepting a file upload are administrator-only and reject any file that does not begin with a PDF signature, so a CSV or spreadsheet cannot be submitted to them even with an administrator's credentials.
Cross-origin access to the back end is restricted to an explicit allowlist of origins. The service refuses to start if that allowlist is empty outside development, and likewise refuses to start without a valid HTTPS authentication issuer configured.
Hosting. Front end on Vercel; back end on Render; ingestion on AWS Lambda in us-west-2;
database on Neon; vectors on Qdrant Cloud; files in AWS S3.
Logging. The back-end service contains no request-logging middleware and never reads the client IP address. What it does log is limited to service lifecycle events, the author identifier and elapsed time for a chat request (never the question itself), authentication failures, and administrator document operations. Credentials are held in a form that cannot be printed, so a configuration error cannot spill them into a deploy log.
None of this infrastructure ever receives laboratory data, because the analysis tool sends none.
Verifying these claims independently
A reviewer does not have to take this document's word for any of it:
- Watch the network. Open developer tools, go to the Network tab, load a spreadsheet and run an analysis. No request carries the file.
- Disconnect. Load the tool, wait for it to finish starting, then disable networking. The analysis still runs.
- Read the analysis code. It is served in the clear at
/sdd_analysis.py. - Inspect storage. Application, then Storage, in developer tools shows what is kept: cookies, the animation flag, and the research chat transcript if that page has been used. Nothing from the tool.
Questions about this document
Technical or privacy questions about anything above, and requests for access to the analysis application, go to Dr. George Cembrowski, cembr001@gmail.com.
If you are assessing BAPa on behalf of an institution, the companion Access Management document sets out how individual access is requested and granted.