TaifaSupport Docs
Security and compliance

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.

Nothing is written to the device and no visitor data is sent until consent applies. Three modes, covered in full on consent and privacy:

  1. data-consent="required": no request, no storage, no UI until grantConsent() is called from the Institution's own cookie banner.
  2. data-consent="granted": the site asserts it handles consent upstream.
  3. 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

KeyWhereLifetimePurpose
ts_vidFirst-party cookie, SameSite=Lax, Secure on HTTPS1 yearAn opaque token that recognises a returning browser
ts.vidlocalStorageUntil clearedFallback where cookies are blocked
ts.consentlocalStorageUntil clearedRemembers 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:

TableContains
customersName, email, phone, company, the attributes bag, notes
visitorsThe opaque token, coarse country, counts, notes, is_blocked
visitor_sessionsReferrer, UTM, device, browser, OS, country, city, timings
page_viewsEvery path with timestamps
visitor_eventsThe human-readable timeline
conversations, messagesThe correspondence itself
tickets, ticket_eventsThe obligation and its audit trail

Two practical points:

  • Blocking is usually the right first response. Visitor.is_blocked stops 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, conversations and messages on 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.

DELETE FROM page_views WHERE entered_at < now() - interval '180 days';
DELETE FROM visitor_events WHERE occurred_at < now() - interval '365 days';

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.

On this page