What actually leaves your network when you run DocuCommit
“Self-hosted” is usually an adjective. It tells you where the binary runs, not what the binary dials. Plenty of self-hosted tools still ship an analytics SDK, a crash reporter, a font CDN, and a telemetry endpoint that nobody mentions on the pricing page.
So here is the complete outbound inventory for DocuCommit — every host either surface will ever open a connection to, what is in each request, and how to turn it off where that is possible. Nothing is omitted for tidiness.
One honesty note up front: DocuCommit’s source is not public, so you cannot read
along with the file references below. What you can check is the behaviour,
which is the part that matters — put the editor behind a proxy or a default-deny
firewall and watch the hosts it resolves, and read the Content-Security-Policy
header the app serves to its own browser surface. Both are described at the end.
The file paths are here so that when you ask us a question, we are pointing at
the same code.
The table
| Destination | When | What is in the request | Opt-out |
|---|---|---|---|
mvqqcojcaklzqqehyduz.supabase.co (licensing backend, EU, eu-north-1) | Trial start, activation, deactivation — all user-initiated. Heartbeat at boot and every 6 hours. Validation on demand. | Five JSON POSTs. Fields per call listed below. Never document content, filenames, or hostname. | No. A licence is required; see the limitation below. |
docucommit.se/download/latest.json | One GET, once, when the editor’s backend finishes starting. | An HTTP GET with Accept: application/json. No body, no identifiers, no query string. | Yes — DOCUCOMMIT_UPDATE_CHECK_ENABLED=false. |
docucommit.se/download/latest.json (desktop shell updater) | Once at desktop shell startup, in parallel with the above. | The Tauri updater plugin’s check request against the same endpoint. | Not separately configurable today. Install is user-confirmed. |
api.github.com/user/emails | Server only, during a GitHub OAuth sign-in, only if you configured GitHub sign-in. | The signing-in user’s own GitHub access token, to read their primary email. | Yes — do not configure GitHub OAuth. |
| Your Git remote | Clone, fetch, and push, when a user runs Get updates or Publish. | Ordinary Git protocol traffic to the host you configured. | Not applicable — this is your own infrastructure. |
That is the whole list. There is no sixth row.
Two of those rows point at the same URL because two independent components check
it: the Java backend’s UpdateChecker (local-app/.../update/UpdateChecker.java)
and the Tauri desktop shell (desktop/src-tauri/tauri.conf.json, key
plugins.updater). The Java checker is notify-only — it parses version and
pub_date, and if the published version is newer it sets a flag the UI reads. It
never downloads or installs anything. It uses a 3-second connect timeout and a
5-second request timeout, skips itself entirely on dev builds, and swallows every
failure at debug level, so an air-gapped machine sees no error and no delay. The
Tauri updater is the component that can actually install: its config pins a
minisign public key and the single docucommit.se endpoint, and the shell code
calls check(), shows a dialog with “Install and restart” / “Later”, and only
calls download_and_install() if you press the first button. No download happens
before that click.
The GitHub row is server-side only, fires only inside an OAuth sign-in you enabled, and exists because GitHub’s user profile does not reliably include an email address. Configuring any OAuth provider also means the standard OAuth exchange — token and userinfo endpoints at that provider — happens on sign-in. That is inherent to OAuth, not something DocuCommit adds.
What is deliberately absent
No analytics. No product telemetry. No crash or error reporting. No CDN for scripts or styles. No web fonts are loaded. No document content leaves either surface, ever — not in a licence call, not in an update check, not in a diagnostic.
One blemish, disclosed because this post is only worth anything if it is
complete: the HTML shell in current builds still carries leftover Google Fonts
preconnect hints and a stylesheet link from an earlier design pass. The CSP
blocks the stylesheet and any font download — nothing is fetched and nothing is
sent — but preconnect sits outside CSP, so the browser does open idle
DNS/TLS connections to two Google hosts. The fix is deleting three dead tags,
and it is queued for the next release. Until then, that is the one connection
you will see that carries no request.
We verified the absence the boring way: there is no analytics, telemetry, or
crash-reporting SDK anywhere in the dependency graph. Not in the Gradle builds
(build.gradle at the root and in core, local-app, server), not in the
frontend package.json workspaces, not in the desktop shell’s Cargo.toml. You
cannot accidentally not-send data through a library you never depended on.
The browser surfaces are additionally fenced in by policy, not just by
discipline. Both the server (server/.../auth/SecurityConfig.java) and the
editor (local-app/.../web/SecurityHeadersFilter.java) send the same
Content-Security-Policy:
script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;
font-src 'self' data:; connect-src 'self'; object-src 'none'; base-uri 'self'
connect-src 'self' is the load-bearing directive. The page cannot open an XHR,
a fetch, a WebSocket, or an EventSource to any origin other than the one it was
served from. Not to us, not to anyone. If a dependency update ever smuggled in a
tracking beacon, the browser would refuse the request and log a violation rather
than send it. font-src 'self' data: closes the font-CDN route the same way.
This is a control that survives our own mistakes, which is the only kind worth
writing about.
The licence call, in detail
This is the row people ask about, so here is the whole of it. All five calls are
JSON POSTs made with the JDK’s built-in HttpClient — no HTTP framework, no
interceptor stack — from core/.../license/LicenseClient.java, to the paths in
LicenseEndpoints.java:
/functions/v1/start-trial—hardware_fingerprint,email(the work email you typed),os,app_version,eula_accepted./functions/v1/activate—license_key,surface(local-apporserver),hardware_fingerprint,os,app_version./functions/v1/heartbeat—activation_id,hardware_fingerprint,app_version./functions/v1/validate— the stored token, nothing else./functions/v1/deactivate— the stored token, nothing else.
os is System.getProperty("os.name") — the string “Windows 11”, not a machine
description. Activation and heartbeat return an Ed25519-signed token, which the
client verifies locally against a public key compiled into the binary before
trusting a single field in it.
The hardware_fingerprint deserves its own paragraph, because “fingerprint” is a
word that has earned suspicion. It is a SHA-256 hex digest of a fixed salt plus
one OS-provided machine identifier: the MachineGuid registry value on Windows,
IOPlatformUUID on macOS, /etc/machine-id on Linux. That is the entire input.
No MAC address, no IP, no hostname, no username, no serial number, no plaintext
machine label — see core/.../license/HardwareFingerprint.java. It is a stable
64-character hash that says “the same machine as last time” and nothing else.
Your IP address is visible to the licensing backend for the same reason it is visible to every server you have ever connected to. The client cannot prove what happens to it after that, so we will not pretend otherwise here: the privacy policy is the binding statement, and it says the IP address is HMAC-hashed before storage and the raw IP is not retained.
Cadence and failure behaviour: LicenseScheduler on both surfaces sends a
heartbeat once at startup and then on a 6-hour fixed delay, and only while the
licence is Active or InGrace — an expired or tampered licence stops the schedule
rather than retrying forever. If the endpoint is unreachable, each call retries
three times with 1s/3s backoff and then gives up quietly. The app keeps working
for a 30-day grace window measured from the last successful heartbeat.
The honest limitation: that call is required. There is no offline licence file and no perpetual key. A machine that can never reach the licensing host will run for 30 days and then stop. Fully air-gapped deployment is not supported. If that is your constraint, DocuCommit is the wrong tool and we would rather you learn it here than in week five of a pilot.
draw.io is bundled, not embedded
Diagram editors are a classic hidden-egress vector, because the easy way to ship
one is to iframe embed.diagrams.net. We vendored it instead — roughly 38 MB of
draw.io v30.2.6, served by the editor’s own Spring Boot instance from /drawio/.
From local-app/src/main/resources/static/drawio/VERSION.txt:
Served offline by Spring Boot from /drawio/. Never contacts embed.diagrams.net (or any network host) at runtime — verified with a headless browser driving the full embed init->load->export(xmlsvg) round trip with zero external requests.
Verify it yourself
Do not take the table on faith. It is cheap to check:
- Run the editor behind a default-deny egress firewall, or point it at an intercepting proxy (mitmproxy, Charles, Fiddler) and watch DNS and connections.
- Over a normal session — start, edit, save, snapshot — you should see exactly
two external hosts carrying requests:
docucommit.se(once, at startup) and the Supabase licence host, plus your own Git remote when you Publish or Get updates. (Current builds also open idle preconnections to two Google Fonts hosts — the leftover tags described above; no request is sent on them.) - Set
DOCUCOMMIT_UPDATE_CHECK_ENABLED=falseand restart. Thedocucommit.serequest disappears; the licence host remains. - Block everything except your Git remote and leave it running. The editor keeps working for 30 days on the grace window, with no error dialogs.
curl -Iyour server and read theContent-Security-Policyresponse header. Confirmconnect-src 'self'is there.
If you find a connection this post does not account for, that is a bug and we want the report.
Where to go next
For the procurement-shaped version of this — data residency, what lives where, what a DPA needs to cover — read data sovereignty. For the full picture including authentication, the token model, and hardening the deployment, read the self-hosting security guide. Elsewhere in the engineering series, two writers, one Markdown file covers how concurrent edits merge without a sync engine.