← All posts

What actually leaves your network when you run DocuCommit

securityself-hostingengineering

“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

DestinationWhenWhat is in the requestOpt-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.jsonOne 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/emailsServer 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 remoteClone, 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-trialhardware_fingerprint, email (the work email you typed), os, app_version, eula_accepted.
  • /functions/v1/activatelicense_key, surface (local-app or server), hardware_fingerprint, os, app_version.
  • /functions/v1/heartbeatactivation_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:

  1. Run the editor behind a default-deny egress firewall, or point it at an intercepting proxy (mitmproxy, Charles, Fiddler) and watch DNS and connections.
  2. 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.)
  3. Set DOCUCOMMIT_UPDATE_CHECK_ENABLED=false and restart. The docucommit.se request disappears; the licence host remains.
  4. 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.
  5. curl -I your server and read the Content-Security-Policy response header. Confirm connect-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.

See it for yourself

Docs as Markdown in your Git.

No database, no lock-in. Browse the live demo with no signup, or start a 14-day trial on your own infrastructure — no credit card required.