Skip to content
IT Support11 min read

How a self-hosted endpoint monitoring platform is built

An agent on every desktop, a server that must never lose a capture, and data too sensitive to hand to anyone else. The architecture that follows from those three constraints, and why each piece is the shape it is.

C# / .NETNode.jsSocket.IOMySQLDocker

Endpoint monitoring is usually bought as a hosted product, and for most businesses that is a reasonable choice. Where it stops being reasonable is when the data is too sensitive to sit in someone else's infrastructure — and screen captures are about as sensitive as business data gets, because they collect whatever happened to be on screen rather than what anyone intended to collect.

Self-hosting removes that exposure and replaces it with an engineering problem. This is that problem, and the shape the answer takes.

Three constraints decide everything else

The constraints, and what each forces
ConstraintWhat it forces
Data must not leave infrastructure the client ownsSelf-hosted server, no vendor cloud in the path
Endpoints are laptops, not serversThe agent must survive sleep, network loss and being closed
A missed capture is a gap in a recordBuffering on the endpoint, not best-effort delivery

The third is the one that separates a working system from a demo. Everything else in this article is comparatively ordinary engineering; the offline case is where these systems are actually decided.

The agent has to be native, and that decides the language

The agent runs on Windows desktops and needs things a cross-platform runtime does not give you cheaply: reliable capture of the active window, the ability to read which browser tab is in front, and a service that starts before anyone logs in and keeps running when they lock the screen.

That points at a native Windows agent. Ours is C# on .NET, which is the pragmatic choice rather than an ideological one — it has first-class access to the Windows APIs this needs, it deploys as a service without a runtime the user has to install separately, and it is a stack a business can hire for.

Reading the active application without guessing

Activity logging is only useful if it produces something a person can read. A raw process identifier tells a supervisor nothing; the readable application name is what makes the report answerable.

Browser tabs are harder, and the obvious answer is the wrong one. A browser extension gives you the tab directly, and it also means deploying and maintaining an extension on every machine, in every browser, surviving every browser update, and being disabled by anyone who opens the extensions page.

Windows UI Automation — the accessibility framework, the same one screen readers use to understand what is on screen — exposes the active tab without an extension. It is a more stable integration point because it is a documented platform API rather than a browser's extension surface, and it works across browsers without a per-browser build.

Capture over RDP, which most agents cannot do

A large share of monitored work now happens inside a remote desktop session rather than on the physical machine, and this is where many monitoring agents quietly fail: they capture the console session and record a lock screen or a blank desktop while the actual work happens in a session they cannot see.

Handling this correctly means the agent has to be session-aware rather than machine-aware — capturing the session where the user actually is, and following them when that changes. It is not conceptually difficult and it is easy to get wrong, which is why it is worth testing explicitly rather than assuming, in any product being evaluated.

The offline queue is the part that matters

A laptop loses connectivity constantly — a lift, a train, a wifi handover, a VPN drop, a lid closed between meetings. An agent that captures only when it can immediately deliver produces a record with holes in exactly the periods a supervisor is most likely to ask about.

So capture and delivery have to be separated. The agent captures on its own schedule and writes to a local queue; a separate path drains that queue whenever a connection exists. Nothing is dropped because the network was unavailable at the moment of capture.

  1. 1

    Capture writes locally first, always

    Never conditionally on connectivity. The capture path should not know or care whether the server is reachable.

  2. 2

    The queue is bounded and has a policy

    A laptop offline for a week must not fill its own disk. Deciding what happens at the limit — stop capturing, or discard oldest — is a decision, and leaving it undefined means the disk decides.

  3. 3

    Delivery is idempotent

    A connection that drops mid-upload will be retried. Without a stable identifier per capture, retries produce duplicates, and duplicates in an activity report look like activity.

  4. 4

    Buffered data is protected at rest

    It is the same sensitive material as on the server, sitting on a laptop that may be lost.

  5. 5

    Sync order does not imply capture order

    Timestamps come from the capture, not from arrival. Otherwise a laptop that reconnects after a week reports a week of work as having happened this afternoon.

Two transports, because there are two problems

Presence and capture delivery look similar and have opposite requirements.

Why one transport does not serve both
Presence and live viewCapture upload
NeedsLow latency, always openThroughput, resumability
VolumeTiny messages, constantLarge payloads, bursty
If it failsStatus goes staleData is lost unless queued
ShapePersistent bidirectional connectionOrdinary request, retried

A persistent WebSocket connection gives instant online and offline state for every endpoint — which a polling design cannot do without either latency or a great deal of wasted traffic — and it is the same channel that makes live view possible, because streaming a screen needs a connection that is already open rather than one negotiated per frame.

Bulk capture upload goes over ordinary requests instead, where retry and resumption are straightforward and a failure costs one upload rather than the connection.

Storage: the decision is where, not how much

Screen recording generates volume continuously. The encoding itself is a solved problem — FFmpeg does it, and there is no reason to build that — but where the output lives is a real decision with real consequences.

  • On the endpoint: no network cost, and the footage is lost with the laptop.
  • On the server: centrally controlled and searchable, and every recording crosses the network.
  • Split: recent material central, older material retained locally or discarded.

There is no correct answer, which is why it belongs in configuration rather than in the design. What matters is that the choice is explicit and that retention is enforced automatically — an archive of screen captures with no expiry is the single largest liability this class of system creates, and it accumulates silently.

Tenancy through enrollment keys

One deployment frequently needs to serve several separate companies — a group, or a provider running it on clients' behalf. The question is how an agent, installed on a machine the server has never seen, proves which tenant it belongs to.

An enrollment key issued per company answers it at the only moment it can be answered reliably: install time. The agent presents the key, the server binds that installation to that tenant, and every capture from it is scoped from then on.

The alternative — inferring tenancy from network location or from a machine naming convention — fails the moment someone works from home, renames a machine, or a network is restructured. Tenancy has to be something the endpoint carries, not something the environment implies.

The isolation itself then has to be enforced server-side on every query, not by which dashboard a user is shown. That reasoning is the same as for any multi-tenant application and is covered separately rather than repeated here.

How it is deployed

The server side is deliberately unremarkable: the API, the dashboard and MySQL running in Docker behind nginx, with TLS terminated at the proxy. Containers make the deployment reproducible on infrastructure the client owns, which is the whole point — the alternative is a bespoke server build nobody can rebuild after the person who did it leaves.

  1. 1

    Stand up the server on infrastructure the client owns

    API, dashboard and database in Docker behind nginx, TLS at the proxy. Nothing about this should be unusual, because unusual infrastructure is infrastructure nobody can operate later.

  2. 2

    Issue an enrollment key per company

    The key is what binds an installation to a tenant, so it is issued before any agent is installed rather than configured afterwards.

  3. 3

    Install the agent, scoped at install time

    Tenancy is decided here. An agent installed without a key is an agent whose data has nowhere correct to go.

  4. 4

    Configure capture intervals, retention and idle threshold per company

    These are the settings that decide both storage cost and whether the resulting reports mean anything. They are per company because operating hours and storage budgets are.

  5. 5

    Hand over the runbook

    Updates, restores, and where the data lives. A self-hosted deployment the client cannot operate is a hosted deployment with extra steps.

The last step is the one that distinguishes a self-hosted deployment from a dependency. If the client cannot update it, restore it, or find their own data without calling the supplier, they have taken on the operational burden of self-hosting without gaining the control that justifies it.

What this architecture does not solve

  • It does not decide whether monitoring is appropriate, proportionate or disclosed. That is a separate question and a more important one, covered on its own.
  • It does not make the data less sensitive. Self-hosting moves the liability rather than removing it — the archive still needs access control, encryption and enforced retention.
  • It does not answer performance questions. It produces activity data; interpreting it is a management task, and the data does not carry the context that explains it.
  • It is not infrastructure monitoring. The server running it still needs the uptime, disk and certificate alerting any server needs.

Why does a monitoring agent need to buffer captures locally?

Because laptops lose connectivity constantly — lifts, trains, wifi handovers, VPN drops, closed lids — and an agent that captures only when it can immediately deliver produces a record with gaps in exactly the periods someone is most likely to ask about. Capture and delivery have to be separated: the agent writes to a local queue on its own schedule, and a separate path drains that queue whenever a connection exists.

Why capture browser tabs through UI Automation rather than a browser extension?

An extension is the obvious answer and the more fragile one. It has to be deployed and maintained on every machine in every browser, survive every browser update, and can be disabled by anyone who opens the extensions page. Windows UI Automation — the accessibility framework screen readers use — exposes the active tab without an extension, works across browsers without a per-browser build, and is a documented platform API rather than a browser's extension surface.

Why do monitoring agents often fail over RDP?

Because they capture the console session rather than the session the user is actually working in, so they record a lock screen or blank desktop while real work happens somewhere they cannot see. Handling it requires the agent to be session-aware rather than machine-aware, capturing the session the user is in and following them when it changes. It is easy to get wrong, so it is worth testing explicitly when evaluating any product in this category.

How should tenancy work when one deployment serves several companies?

Through an enrollment key issued per company and presented at install time, which binds that installation to that tenant for everything it subsequently sends. Inferring tenancy from network location or machine naming fails as soon as someone works from home, a machine is renamed, or a network is restructured — tenancy has to be something the endpoint carries rather than something the environment implies. Isolation must then be enforced server-side on every query, not by which dashboard a user is shown.

Does self-hosting make monitoring data safer?

It removes one exposure and does not remove the liability. The data no longer sits in a vendor's infrastructure, which is the point, but the archive still contains incidental sensitive material — whatever happened to be on screen — and still needs access restricted to a short named list, encryption at rest, and retention enforced automatically. An archive with no expiry is the largest liability this class of system creates, and it accumulates silently.

Sources and further reading

Services This Relates To

Written by KYCONNECTS Engineering. Client names are withheld under confidentiality.

Talk Through Your Requirements

We typically respond within 4–8 business hours.