Most reporting tools answer “is our data safe?” with a policy. The honest version is structural: it depends entirely on what actually runs, and where. This page describes the two shapes of work I deliver and the one I would build next, including the one where the answer to “does our data leave our environment?” is not a flat no.
The short answers
If you were handed this to review and want the summary, it is this table. Everything below is the detail behind it.
| Does our data leave our environment? | No | No for the file and the dataset. No for the refresh either, when it runs in your tenant — the arrangement I argue for. If it has to run on mine, that is a yes, and it is contracted and registered as such. Where data goes, by shape |
|---|---|---|
| What access do you need? | One | One container, never a directory. On Microsoft that is Sites.Selected on
the one site, not Files.Read.All; on Google it is a service account with
one Drive folder shared to it, never domain-wide delegation. The credential is yours
and you can revoke it without asking me. Access |
| What happens when a refresh fails? | Holds | It keeps yesterday's good version rather than publishing something wrong, and it alerts. A missed run alerts too — silence is treated as failure. What happens when it fails |
| What do you hold at the end? | Nothing | Nothing. Deletion is confirmed in writing and every credential is revoked against the access register. End of the engagement |
| What if you disappear? | Yours | It runs on your infrastructure with your credentials, and you hold the full source, the documentation and a runbook. If I disappear |
| What code runs on our side? | Inlined | The dashboard's own, inlined, with no network access — and, where the deliverable includes a file reader, a small parser I wrote that reads the CSV or XLSX you choose. Nothing third-party, nothing fetched. What actually runs |
| What can't this shape do? | Limits | A delivered file cannot be revoked, does not log who opened it, and is not reached by my deletion commitments once it is inside your organisation. If you need those controls, say so early — it changes which shape is right. What you give up |
| What are you certified for? | Nothing | Nothing, and I do not imply otherwise — not ISO 27001, not SOC 2. Professional indemnity insurance is in place and you can have the certificate. What I do not claim |
| Can we see your actual process? | Yes | Yes — the runbook, the annex for each delivery shape, and every template you would receive and countersign are published in full. Not summarised, not on request. |
Where data goes, by shape
1 min
Which one applies is decided with you before anything is built, and written down. I do not let a project drift from the first into the second without saying so, because the security answer changes when it does.
| Shape | What it is | Does data leave your environment? |
|---|---|---|
| Self-contained file | One HTML file you open from your own file share. No server, no install. | No. There is nothing for it to leave through. |
| Automated refresh | A scheduled job reads your data, aggregates it, and republishes the file. | No, when it runs in your tenant — the arrangement I argue for. If it has to run on my infrastructure instead, that is a yes, and it is contracted and registered as such. |
| Explorable dataset (upgrade path) | Larger data converted to a columnar format and queried by an engine running in the browser tab rather than on a server. Where the first shape goes when the data outgrows it. Scoped as its own build, not an extension of a quote. | No, by the same construction as the first shape — the query engine runs in the tab and there is no server to send anything to. Assess it against the build you are quoted, not against this row. |
Shape one — the self-contained file
6 min
The deliverable is a single HTML file. It carries its own charting library and its own fonts. It has no script tags pointing anywhere, no stylesheet links, no images loaded from a server, no analytics, no error reporting, no telemetry. Opening it makes zero network requests of any kind.
- No infrastructure. Nothing to provision, patch, or pay for.
- No credentials. There is no account, no login, no session, no password to manage or revoke.
- No network egress. Not restricted egress — none.
- Works offline. From a local download, a network drive, or SharePoint.
- Nothing to breach on my side. Because I hold nothing.
Download a demo, then open it with your network disconnected. Every chart, filter and drill-down still works, and the devtools network tab records nothing at all — once the file is on your disk there is no server left for it to ask. Every demo carries a ↓ This dashboard link next to its file picker.
That is not a promise about my conduct. It is a property of how the file is built, and you can falsify it in about thirty seconds.
This website is the weaker version of the same claim, and worth stating
precisely: it is hosted, so it does make requests — the page, a stylesheet, three fonts, a
favicon, and on the site pages a page counter. Every one of them goes to this domain
and nowhere else. No CDN, no font service, no tag manager, and no counter hosted by
anybody else: it is served from /_vercel/insights/ on this domain, sets no
cookie, and is described in full on the privacy page. That is also
checkable in the network tab, and it is a different claim from the one above.
The demos are excluded from it deliberately. They carry no counter, so the paragraph above this one stays absolute rather than nearly true — which is the point, since the demo is the shape of the thing I actually deliver.
And it is enforced rather than promised. Read the
content-security-policy response header on a demo: it carries
connect-src 'none', so the browser will not let that page open a network
connection even if something on it tried. The site pages carry
connect-src 'self' instead — one directive weaker, because the counter has to
report a page view to this origin, and self is the whole difference between a page
that can talk to me and a page that can talk to anyone. Everywhere, every script and
stylesheet is allowlisted by SHA-256 rather than by 'unsafe-inline' — which is
the directive that makes most CSPs decorative. If a header scanner gives this site a grade,
that is the line doing the work.
What is inside the file
Usually the data itself, pre-aggregated. That matters: the file is the data, so wherever it gets forwarded, the data goes with it. Before anything is embedded I check whether it is personal data, whether it is commercially sensitive, whether every embedded column is actually read by a chart, and whether the grain is finer than the questions need. If a view is weekly-by-team, I embed weekly-by-team — not per-person-per-minute.
One file, one audience. If two teams should not see each other's numbers, they get two files with two slices of data, not one file with a filter someone can clear.
After delivery, distribution and access control are yours — your file share, your permissions. I say that explicitly at handover so it is never an assumption.
A note on sharing it: some mail systems quarantine HTML attachments as a matter of policy, so the reliable way to distribute it is your own file share, SharePoint or intranet rather than email. Once it is there, opening it is a double-click and there is nothing to install.
What you give up for that
The same property that makes this shape safe — no server, no account, nothing of yours held anywhere — takes four controls off the table. If your reviewer needs any of them, the honest answer is that this shape is the wrong one, and I would rather say so here than be found out in the review.
- No revocation. Once a file is delivered, everyone holding a copy holds all of the data in it, permanently. There is no access to withdraw, because there was never any access to grant.
- No audit trail. Nothing records who opened it, when, or how many times. A tool that could tell you that would need the server and the account this shape does not have.
- Deletion does not reach it. Everything I commit to deleting is on my side. A copy already inside your organisation — in a mailbox, a laptop, a backup — is genuinely difficult to find and remove, and an erasure request that touches it is your problem, not mine. That is worth knowing before you send personal data into one.
- Plain on disk, and quiet past DLP. The file is not encrypted at rest beyond whatever your storage does, and most data-loss tooling does not inspect data embedded in an HTML file — so it can travel where the equivalent spreadsheet would have been stopped.
Three things reduce this rather than solve it: embedding the coarsest grain that answers the question, one file per audience, and a visible “data as of” date so an old copy announces itself. Where the controls above are genuinely required, that is an argument for shape two with the dashboard behind your own single sign-on — or for a platform, which I will tell you.
What actually runs when you open it
Worth being precise about, because “it just opens in a browser” can be heard as “nothing executes”, and that is not quite right. The file renders charts, filters and drill-downs, so it runs code — its own, inlined, with no network access.
A dashboard can either carry its data baked in, or ship as a viewer you point at your own export. Which one you get is decided with you rather than defaulted, because the trade runs in both directions:
- Baked in — the file is the data, so wherever it is forwarded the data goes with it. Nothing parses anything; there is no input.
- A viewer — the file is no longer the data, so the four limits above largely stop applying: an empty viewer can be forwarded harmlessly and your export never travels with it. The cost is that a spreadsheet parser is code processing a file I did not generate.
So, plainly: nothing leaves, and the only thing that runs is a parser I wrote, on a file you chose, inside the browser sandbox. It is a few hundred lines using the browser's own decompression and XML parsing rather than a large third-party spreadsheet library — deliberately, so the code you are trusting is small enough to read.
Four behaviours follow from the same principle as the rest of this page, and you can watch all of them on the demos while the network tab stays empty:
- It says what it needs before you go and find a file — the expected columns are listed next to the picker.
- A missing column is refused by name and changes nothing on screen. It never half-loads.
- An ambiguous date column is refused rather than guessed.
01/02/2026is 1 February here and January 2 in the US; if nothing in the file settles it, you get told instead of charts built on the wrong months. - Where a view cannot be built from your export, it says which. Rather than leaving sample data sitting there looking like an answer.
- A European export reads as a European export. Semicolon separators, decimal commas, non-breaking-space thousands, currency symbols and Windows-1252 accents are all read from the file rather than assumed from your browser's locale. The convention is settled per column, from that column's own values.
- Every inference is shown, not assumed. After a file loads, a panel names the delimiter, the encoding, the decimal separator, the date order it settled on and any rows it skipped, with the reason. A dashboard that is confidently wrong is worse than one that refuses, because it gets forwarded and nobody re-checks a chart.
How much it will take. Measured through the picker on an Apple M3 with 8 GB, in Chrome 152, rather than estimated: a 48 MB CSV of 390,000 rows parses in 1.7 seconds, and a 9.3 MB workbook of 85,000 rows in 7.5 seconds. The published ceilings are 50 MB for CSV and 10 MB for XLSX, and a larger file is refused with the number rather than left to freeze the tab. XLSX is the lower of the two because unzipping a workbook and walking a DOM over every cell costs roughly twenty times more per megabyte than reading text — not because it needs more memory.
Values are text, never markup
Every value that comes from your data is written to the page as text, never as HTML. A spreadsheet cell containing a script tag is a realistic accident, not a hypothetical attack, and it renders as visible characters rather than executing. Dependencies are pinned to exact versions, taken from the official release artifact, and recorded in the file header so you can review them if an advisory lands later.
This is the part that stopped being hygiene and became load-bearing the moment a dashboard
could read a file you chose, so the test is now in the build rather than in a checklist. Two
paths get it, because they are genuinely separate: a hostile value baked in at build time, and
the same value arriving through the file reader. Two specific cases are covered by name — a cell
containing <img onerror>, and one containing </script>,
which would otherwise close the element the data sits in and have the remainder of the file
parsed as HTML. The build fails rather than writing a file that could do that.
Shape two — the automated refresh
5 min
This is where the honest answer changes, so it gets the most detail.
Where the code runs
In order of preference:
- Inside your own account — your Azure subscription or your Google Cloud project, your scheduled job, your storage. In practice that is an Azure Function or a Cloud Run job on a schedule; on Workspace an Apps Script trigger is often enough and needs no cloud project at all. Data never crosses a boundary, you can see it and kill it, and there is no bus-factor problem if I disappear. This is the default I argue for.
- My EU-region infrastructure, with credentials you own — used only when you have nowhere to run it. Data does cross to me, so it goes in the Article 30 register and a data-processing agreement is signed first. Nothing persists between runs: the source extract is deleted as soon as the aggregate is built, and every copy is gone within seven days of the engagement ending. The cost of that is worth stating — when a run produces a number you do not believe, there is no extract left here to check it against, so I will ask you to send it again.
- A VPS I administer — only when neither of the above fits.
Access — the exact permission to ask for
The pipeline reads. It does not write back to the source system and it does not hold a credential that could.
The same principle on either platform: grant access to one container, not to a directory. Both make the narrow grant harder to set up than the broad one, which is exactly why the broad one is so common.
| Microsoft 365 | Google Workspace | |
|---|---|---|
| What to grant | Sites.Selected as an application permission, on an app registration
you own. It grants nothing by itself — your
administrator then adds read on exactly the one site. |
A service account you own, with the target Drive folder shared to it like you would share with a colleague. The folder grant is the permission. |
| What to refuse | Files.Read.All or Sites.Read.All — easier to set up, and
they read every file in every site collection in the tenant. |
Domain-wide delegation. It lets the service account impersonate any user in the domain. It is the single most over-granted thing in Workspace. |
| Revoking | Delete the app registration, or remove the site grant. One click, without asking me, without breaking anything else you own. | Unshare the folder, or disable the key. Same property: you can cut access off without my involvement. |
| Where it falls down | Less than it used to. OneDrive for Business now does have a per-folder
application permission — Files.SelectedOperations.Selected, granted on
the one file or folder through a Graph call, GA in v1.0. Consumer OneDrive on a
personal Microsoft account has no application permissions at all, because app-only
access is an Entra-tenant flow, so the answer there is a dedicated account with one
shared folder rather than a tenant-wide grant. |
A shared drive is cleaner than a personal My Drive folder, because access outlives whoever created it. |
If a vendor asks for the broad option to read one folder, push back — including if the vendor is me. Credentials are per-project and never reused across clients. After consent I check the granted scopes in the console, because what was requested and what was granted are not always the same thing, and I recommend you check them too.
Secrets
A managed secret store in your own account, in an EU region, with access granted to the job's own identity rather than to a person: Azure Key Vault with a managed identity on Microsoft, Secret Manager with a service-account identity on Google. Elsewhere, that platform's native equivalent. The rule is the same in every case — a person's credentials are never what the job runs on.
Secrets are injected at runtime as environment variables. Never committed, never logged, never pasted into a chat message or a ticket. Rotation never requires a code change — if rotating a key means editing code, the design is wrong. Keys are rotated at handover and whenever anyone with access leaves.
What happens when it fails
A stale dashboard looks exactly like a fresh one. That is the failure mode that actually hurts, so it is designed against directly:
- Sanity checks run before publish — row count within an expected band, no all-null key column, totals within tolerance of the previous run.
- If a check fails, the job stops and keeps yesterday's good output. It does not publish something wrong.
- Every published file carries a visible “data as of” timestamp, so a stale copy is obvious to the person reading it and not just to me.
- Failures alert me the same day. A missed run alerts too — silence is treated as a failure, not as success.
- Logs record events, not payloads: what ran, when, how many rows, what failed. No personal data in log lines.
Who "me" is, and what happens when I am not there. Alerts go to my email and my phone. There is one person behind that, so the honest version is that I am occasionally on a plane or asleep, and there is no rota behind me — you should know that before you depend on it. What protects you is not my availability but the design above: a failed check does not publish, so the dashboard keeps serving yesterday's good output with its own “data as of” date visible on it. Nothing updates silently and nothing goes wrong quietly while I am unreachable; the worst case is that the numbers stop moving and everyone can see that they have. If you need a guaranteed response window, that is a support contract and I would rather tell you I do not offer one than sell you a promise I would break.
During setup I trigger a deliberate failure and confirm the alert actually arrives, rather than assuming it would.
Data handling, whatever the shape
2 min
Minimisation
I take the columns I need and leave the rest. If a file has names, emails, salaries or free-text notes the dashboard never shows, they get dropped at the first processing step rather than carried around. Where an identifier is needed only to join or count, it gets hashed or replaced. I verify this by diffing the columns in the source extract against the columns the deliverable actually uses — anything in the first list and not the second has to be justified or removed.
Residency
Processing and storage stay in the EU/EEA — westeurope or
northeurope on Azure, europe-west* on Google Cloud, EU-hosted
repositories and vaults, and no “wherever it lands” defaults. For each service I
record the actual configured region string from the console, not the vendor's marketing
claim, because the two differ more often than you would like. Any transfer outside the EEA is
flagged in the Article 30 register before it happens, not after.
Subprocessors
Any third party that touches your data — hosting, vault, error tracking, email — is listed by name and purpose in the Article 30 register. I do not add one mid-project without telling you first. Adding a service is a two-step change: register row first, integration second.
Anything hosted
Where a deliverable is hosted rather than handed over as a file, it sits behind your own single sign-on — Entra ID with your conditional access policies on Microsoft, Google sign-in with context-aware access on Workspace — using the identity provider you already run. No separate user list for someone to forget to deprovision, which is the failure that actually happens. HTTPS everywhere, HTTP redirects to HTTPS, and I verify the certificate and the redirect before a URL is sent to you.
Separation
Development never points at production data or production credentials — separate app registrations, separate secrets, separate storage. Test data is synthetic or a minimised sample. Anything I publish publicly, including every demo on this site, is synthetic-only, generated by a seeded script; no real extract ever becomes a demo, not even lightly edited.
If I disappear
1 min
The bus-factor answer has to be structural too, or it is worth nothing.
- It runs in your tenant, on your credentials.
- You get full source rights and the actual source — not a compiled artifact, not a licence to use something I keep.
- Code is documented and the delivered version is tagged in a repository you hold.
- You get a runbook covering what to do when an alert fires after I am gone.
- At least once per project, before handover, I rebuild the pipeline from backup into a scratch environment and run it end to end — then write down the date, what I restored, how long it took, and what was missing. A backup nobody has restored is a guess, not a backup.
- There is no platform to stay subscribed to, no per-viewer licence, and nothing that stops working if you stop paying me.
End of the engagement
1 min
When an engagement ends: credentials are rotated, my access is removed, and your data is deleted from my machines and from any service I stood up — the working folder, the backups that contain it, and the trash, with the specific backups named.
Ideally I am holding nothing to begin with, because the self-contained shape never required me to keep a copy. Where I did hold data, deletion is confirmed in a handover document that you countersign. The engagement is not finished until that is signed and every row in the access register has a revocation date against it.
What I do not claim
2 min
I am not ISO 27001 certified and I am not SOC 2 audited, and I do not imply either. My internal runbook is organised loosely along the ISO/IEC 27001:2022 Annex A control themes so you can map my answers onto your own framework — that is a self-assessed alignment by a one-person consultancy, and nothing more than that.
I also do not use the word “compliant” without naming what with. If you need a certified supplier, tell me and I will say so rather than talk around it.
Insurance and paperwork
Four separate things, so they are listed separately.
- Professional indemnity insurance is in place — RC Professionnelle, held in Belgium where I am established. You can have a copy of the certificate for your file; it names the insurer, the policy number and the period of cover. This is the one thing on this site that is available on request rather than published, and the reason is narrow: a certificate carries a live policy number, and putting that on a public page is an invitation to someone else to use it. Everything that can be published, is.
- The data processing agreement: I sign yours. Most legal teams already have a controller-to-processor template they would rather use than review someone else's, and I do not draft clauses myself. Where you have no paper of your own I start from a standard controller-to-processor model — or the European Commission's standard contractual clauses where they apply — reviewed by a qualified professional before I sign.
- It is signed before the first extract, not retro-fitted. Where I am processing personal data on your behalf, I do not accept a data file until the paperwork is in hand.
- There is a one-page incident plan — detect, contain, tell you, assess the 72-hour GDPR question, write it up — filled in per project with your named contact and an agreed notification window. I walk through it once at kickoff so I am not reading it for the first time during an incident.
Read the process, don't take my word for it
1 min
Everything above describes how I work. You have no way to check that from a description, and I have no client list to point you at instead — so the documents themselves are published in full: the baseline runbook, the annex for each delivery shape including the Google one, and every template you would actually receive and countersign — the access register, the data-flow diagram, the incident plan, the handover checklist, the Article 30 register.
Not a summary and not available on request. If something in there is weaker than your own standard, that is a conversation worth having before we start rather than after — and it is a great deal easier to have when you can read the thing.
Each document carries the date it last changed, taken from version control rather than typed in, so you can see for yourself whether this is maintained or was written once and abandoned.
And publishing it means a competitor can read my method. That seems the wrong thing to protect: the method is not the difficult part, doing it on every engagement is — and you cannot verify that about anyone without seeing what they actually work from.
Questions you should ask any vendor
2 min
Including me. If a supplier cannot answer these quickly and specifically, that is information.
- Exactly which permission are you asking for, and what else does it reach? “Read-only” is not a scope. Make them name it, then check what was actually granted in the console rather than what was requested.
- Who owns the credential — you or us? If you cannot revoke it yourself, in a click, without asking them, you do not control your own data.
- Where does the compute actually run, and in which region? Ask for the configured region string, not the marketing page.
- What happens when a refresh fails? If the answer is not “it keeps the last good version and tells someone”, you will eventually make a decision on stale numbers without knowing it.
- Does a missed run alert, or only a failed one? Most tools only catch the second. Silence is the expensive failure.
- Name every subprocessor that touches our data. Hosting, vault, error tracking, email. If the list takes them a while to produce, it is not being maintained.
- What do you hold at the end, and how is it destroyed? Ask for it in writing, countersigned.
- What happens if you disappear? Do you hold the source, the credentials and the ability to run it — or a licence to something they keep?
- What are you certified for? If the answer is a framework name without the word “certified” or an auditor attached, it is alignment, not certification. That can be fine — it just should not be sold as the other thing.
- Does our data ever go into an AI model, and which one? Almost every supplier now uses AI tooling somewhere. The useful question is not whether they use it but where the boundary is: a chat window, a coding assistant's context, a file upload, a vendor's own training. Ask for it as a sentence you could put in a contract. My answer is in the runbook: I use AI coding assistants, and client data does not go into a model — I work against synthetic or redacted samples, which is why every demo on this site is generated rather than borrowed.
This is a one-person consultancy. Everything on this page is what I commit to and how the work is built, stated so you can check it rather than take it on trust. Where a claim is verifiable, I have said how to verify it. Where I do not hold a certification, I have said so plainly rather than implying one.
Download this page as a PDF — same content, easier to attach to a ticket.
Found a problem with the site itself? There is a vulnerability disclosure policy with scope, response times and a no-legal-action commitment — it is what security.txt points at.
Version 1.0 · last reviewed 18 August 2026. If you are reading a copy that has been sitting in a ticket for a while, the current one is at andrewryan.consulting/security/. This page argues elsewhere that a document with no date on it cannot be checked for maintenance, so it carries one.
Still have a question this doesn't answer?
Send it over, or put twenty minutes in the calendar. If the answer is “this isn't a good fit”, I'll tell you that on the call.