Data protection
The Kenya Data Protection Act 2019 posture, why raw IP addresses are never persisted, and what a data subject request actually touches.
The posture is built for the Data Protection Act 2019. This page states what the system actually does, not what it intends to.
Raw IP addresses are never persisted
The server derives a coarse country from the request IP and does not store the address itself.
That is not a policy statement, it is how the code is arranged. The address is a parameter to the geo lookup and never a return value. Callers hand one in, take a country back, and drop it on the floor. Nothing in that module stores, logs or echoes the address it was given.
What survives a request is country_code, and sometimes city. That is enough
to put a flag on a live visitor row, which is what an agent needs, and it is
materially less than what a raw address would let anyone do.
GeoIP is optional. An Institution installing this on its own hardware will not have a MaxMind licence on day one, so a missing or unreadable database degrades to "country unknown" rather than to an error, and the analytics totals still add up because unknown is a bucket rather than a discard.
X-Forwarded-For is trusted because the deployment always terminates at our
own nginx, which overwrites it. In a direct-to-backend setup it is spoofable,
and the worst a spoofer achieves is a wrong flag on their own row.
Consent
Nothing is written to the device and no visitor data is sent until consent applies. Three modes, covered in full on consent and privacy:
data-consent="required": no request, no storage, no UI untilgrantConsent()is called from the Institution's own cookie banner.data-consent="granted": the site asserts it handles consent upstream.- Neither: one bootstrap request carrying the site key only, to learn the site's policy. No visitor token, no identifiers, nothing set on the device.
Withdrawal is one call. revokeConsent() stops the tracker, closes the
websocket, unmounts the UI and erases the cookie and both localStorage keys.
Proactive contact requires consent, and the check is on the server.
POST /v1/visitors/{id}/invite answers 409 with the code consent_required
rather than sending, so an Institution cannot configure its way past it from
the console.
What is stored on a visitor's device
| Key | Where | Lifetime | Purpose |
|---|---|---|---|
ts_vid | First-party cookie, SameSite=Lax, Secure on HTTPS | 1 year | An opaque token that recognises a returning browser |
ts.vid | localStorage | Until cleared | Fallback where cookies are blocked |
ts.consent | localStorage | Until cleared | Remembers that consent was granted |
No third-party cookies. No fingerprinting. The token is random and is not derived from anything about the person, the device or the network.
Data residency
On a self-hosted install, nothing has to leave the Institution's own infrastructure. With the AI profile enabled, nothing has to leave the building at all.
What a data subject request touches
If a person asks what is held about them, or asks for it to be erased, these are the rows:
| Table | Contains |
|---|---|
customers | Name, email, phone, company, the attributes bag, notes |
visitors | The opaque token, coarse country, counts, notes, is_blocked |
visitor_sessions | Referrer, UTM, device, browser, OS, country, city, timings |
page_views | Every path with timestamps |
visitor_events | The human-readable timeline |
conversations, messages | The correspondence itself |
tickets, ticket_events | The obligation and its audit trail |
Two practical points:
- Blocking is usually the right first response.
Visitor.is_blockedstops all tracking for that browser without deleting rows that may be part of a correspondence record. - Erasure of correspondence is a records question, not an engineering one.
tickets,ticket_events,conversationsandmessageson a government tenant are very likely subject to a retention obligation that outranks a deletion request. Check before deleting, and let the records officer answer.
Retention
page_views is the table designed to be pruned first: dense, high-volume, and
the least valuable row for row.
Keep visitor_events longer. It is the sparse timeline an agent actually
reads, and it is a hundredth of the size.
See pruning.
No metering, and what that has to do with privacy
There is deliberately no conversation counter anywhere in the schema. That is a pricing decision, but it has a privacy consequence worth stating: a system that bills per conversation has a commercial reason to count and retain conversation-level activity in perpetuity. This one does not, which makes the retention conversation a records conversation rather than a billing one.