Privacy Policy

Status: DRAFT. Not yet in force. This document has been written from the source code of the product it describes, and has not yet been reviewed by a lawyer. Every place a fact could not be established from the software is marked ⚠️ DECISION NEEDED. Do not publish it, link to it, or rely on it until those are resolved and a qualified reviewer has signed it off.

Last updated: ⚠️ DECISION NEEDED: set the date this is approved, not the date it was drafted.


The short version

HeyThula is customer-support software. Businesses install it, and their customers chat through it. That means we hold two very different kinds of information: a little about the businesses who buy the product, and a lot about their customers — who never chose us and mostly have never heard of us.

We take the second kind seriously. This policy says exactly what we hold, where it physically sits, what leaves our systems, who else touches it, and for how long. Where the answer is unflattering, it still says it.


1. Who is who

Three different parties appear in this document, and mixing them up is how privacy policies go wrong. They are:

  • Us. Learn App (Pty) Ltd, a private company registered in South Africa, registration number 2020/142526/07, registered address 173 Oxford Road, Rosebank, 2196, South Africa. We trade as HeyThula and operate heythula.com, app.heythula.com and *.help.heythula.com.
  • The customer. A business that has a HeyThula account and installs our widget on its own website or product.
  • The visitor. A person who uses that business's website or product and talks to it through our widget, reads its help centre, or replies to one of its emails.

The distinction matters legally. For a customer's own account data — the name and email of the people who log in, the billing details, the workspace settings — we decide what is collected and why. We are the responsible party under South Africa's Protection of Personal Information Act (POPIA) and the controller under the EU/UK General Data Protection Regulation (GDPR).

For everything a visitor sends through the widget — their messages, their email address, whatever the customer chooses to attach to their contact record — the customer decides. They are the responsible party / controller. We are the operator (POPIA) / processor (GDPR). We hold and process that information on the customer's instructions and for no purpose of our own.

So: if you are a visitor and you want your conversation deleted, the business you were talking to is who decides. We will help them do it — see section 10.

⚠️ DECISION NEEDED: how the operator/processor relationship above gets papered. POPIA s21 requires a written contract with an operator; GDPR Art. 28 requires specific terms — sub-processor consent, help with data-subject requests, deletion or return at the end of the contract, audit rights. There is no DPA in the product, in the signup flow, or anywhere in the repository, so today this section describes a relationship that exists in fact and not on paper. Three ways to close it:

  • A standalone Data Processing Addendum, incorporated by reference into the Terms and accepted with them. Cheapest, and what most self-serve SaaS does. Needs the acceptance mechanism the Terms flag as missing, or there is nothing recording that anyone agreed to it.
  • The same terms written into the Terms of Service. One document to maintain — but an enterprise buyer's counsel expects a separate DPA they can route and file, and will ask for one anyway.
  • A signed DPA per customer. What large buyers want, and unworkable alongside a signup page.

The first is the usual answer at this size. Whichever is chosen, it must name the sub-processors in section 8, and it must not promise anything the software cannot do — see section 10 for what that still is.


2. What we hold about a customer

When someone signs up for a HeyThula account we store:

  • Their name and email address, and their password or the fact that they signed in with Google. Sign-in credentials are held by Firebase Authentication (Google Identity Platform), not by us — we never see or store a password.
  • Their profile photo, if their identity provider supplied one. Google accounts do; password accounts do not.
  • The account name, the workspaces in it, and each member's role and workspace access.
  • The size of their support team, self-reported once at signup. It is used for onboarding and segmentation, never as a limit on anything.
  • Their plan, usage counters, and Stripe customer id.
  • Their referral code, and the code they signed up with, if any.
  • Whether their email address has been confirmed, and — if they turned on two-factor authentication — that it is on, how many backup codes they have left, and a salted SHA-256 hash of each unused code. The authenticator secret itself is held by Firebase Authentication, not by us, and we never store a backup code in a form we could read back.

We also store what they configure: widget branding and colours, help-centre articles and collections, AI settings, lead-capture copy, and the help-centre subdomain they chose.

We do not run any analytics, product-telemetry, session-recording or advertising script in the application. There is no Google Analytics, no Segment, no Mixpanel, no PostHog, no Hotjar and no Sentry in the codebase.


3. What we hold about a visitor

This is the part that matters. Everything below is stored per-workspace and belongs to the customer whose widget the visitor used.

The contact record

  • A visitor id — a random string the widget generates in the browser the first time it loads. It is not derived from anything about the person or their device.
  • An anonymous sign-in id from Firebase Authentication. The widget signs every visitor in anonymously so that security rules can guarantee one visitor cannot read another's conversations.
  • A city, inferred from the browser's reported time zone. It is not derived from an IP address and it is not a location fix — a browser reporting Africa/Johannesburg is recorded as "Johannesburg" and nothing more.
  • The name of the website the widget loaded on — the hostname, such as shop.example.com, and nothing else. Never the page address, never the path, never anything after the ?, and never the site the visitor came from. This matters: a page address routinely carries an order number or a person's own name, and none of it is read. Two are kept — the site where the visitor first loaded the widget, and the site where they most recently loaded it — because they can differ. It exists so the customer can check on their own dashboard that their widget is really live on their own site, rather than only on the help centre we host for them; a hostname is what makes that a fact rather than a guess. Contacts created before this was added do not have it, and we do not fill it in retrospectively.
  • An email address and name, once the visitor gives them — by filling in a lead form, or because the customer's own site identified them (see section 6).
  • Whether that email address is verified — which means the customer's own backend vouched for the identity the record belongs to, not that anybody proved they can read that mailbox. See section 6 for exactly what the signature covers.
  • Custom attributes — arbitrary key/value pairs the customer chooses to attach. We do not define these and cannot know what a customer puts in them.
  • First seen and last seen timestamps.

The conversation

  • Every message, in full, from both sides, with who sent it and when — the visitor's messages, the human agent's replies, and the AI's answers.
  • Which of the customer's help articles the AI cited for each answer it gave.
  • Whether the AI or a human is handling it, whether it has been flagged for a human, who it is assigned to, whether it is open or closed.
  • A satisfaction rating, if the visitor left one.
  • Typing indicators and last-seen timestamps, so both sides can see presence.
  • Email delivery state for any notification about that message: the address it went to and whether it was sent, delivered, opened or bounced.

Separately, and readable only by the customer's agents, we store why the AI handed the conversation to a human, plus — when the reason was "nothing in the help centre matched" — how many articles were searched and the best score any of them got. The reasons the product records today are: the visitor asked for a person; the account is on the Free plan and at its resolution limit; the AI had already replied the maximum number of times allowed in that conversation; nothing in the help centre matched the question; something matched but not well enough to answer from; and the account is on the Free plan and its owner has not yet confirmed their email address (see section 5). That list is deliberately given without a count — it grows when the product does, and a number in a policy goes stale without anyone noticing.

Documentation-gap signals

When the AI cannot answer from the customer's help centre, we record the visitor's question so the customer can see what their documentation is missing. This is stored twice:

  • A raw signal, holding the visitor's question verbatim (whitespace tidied and truncated to 300 characters), a numeric embedding of it, and the conversation and contact ids.
  • A rolled-up backlog item, clustering questions that mean the same thing. It keeps one representative question, up to five verbatim excerpts of real visitor questions, and up to 200 contact ids.

Retention for these is not what you would assume. See section 9.

Lead submissions

When a visitor fills in a lead form, we store their email address, their name if they gave one, the contact and workspace it belongs to, and the time. The email address is then copied onto the contact record and the contact is marked as a lead. It is not marked verified — a typed-in address proves nothing.

What we never collect from a visitor

  • We do not read the visitor's IP address anywhere in our application code. Google Cloud records request IPs at the load-balancer and platform layer, as any host does, and we can see those in infrastructure logs. We do not put an IP on a contact, a conversation or a message, and we do not use IPs to locate anyone.
  • We do not record which pages a visitor looks at. We keep the name of the site the widget loaded on (see the contact record) and nothing finer than that: no page address, no path, no query string, no page title, and no record of the page they arrived from. There is no browsing history on a contact.
  • We do not track visitors across sites. The visitor id is stored in one browser's storage on one origin and is never shared between customers or read from another site.
  • We set no advertising or analytics identifiers of any kind.

4. Where your data physically lives

We are specific about this because the answer is unusual and because "the cloud" is not an answer.

At rest, in South Africa. The database (Google Cloud Firestore) and the file storage that holds images, article media and guide screenshots (Google Cloud Storage) are both in Google's africa-south1 region — Johannesburg. That is where your conversations, contacts, articles and uploads actually sit.

Processed in the United States. The background services that make the product work — the functions that call the AI, send email, run the article importer, meter usage and process Stripe events — run in Google's us-central1 region (Iowa). To do their job they read and write the South African database, so the content passes through the United States in the course of being processed.

Processed in the Netherlands. The web application itself — the agent dashboard at app.heythula.com and every public help centre at *.help.heythula.com — runs on Google Cloud Run in europe-west4. Help-centre pages are rendered there on every request, and the agent dashboard is served from there.

AI processing. Requests to the language model go to Google's Vertex AI global endpoint; requests to the text-embedding model go to Vertex AI in us-central1. See section 5.

So a single visitor message can be stored in Johannesburg, read by a function in Iowa, embedded in Iowa, answered by a model reached through a global endpoint, and displayed to an agent by a server in the Netherlands. All of it inside Google Cloud.

[DEFAULT — confirm] Transfers out of South Africa rest on POPIA s72(1)(a): every recipient is bound by a written agreement that upholds principles of reasonable processing comparable to POPIA's. Transfers out of the EEA and the UK rest on the European Commission's Standard Contractual Clauses, as incorporated into each provider's own data processing addendum.

Why this basis rather than another: the entire recipient list is Google, Postmark and Stripe, and all three publish an SCC-backed processing addendum that a customer signs by using the service. Consent (POPIA s72(1)(b)) is the wrong instrument here — a visitor never chose us, and a consent that can be withdrawn is not a foundation for infrastructure. A lawyer should confirm each addendum is actually in force for our account, and name the version and date here.


5. What the AI does with a message

The AI is the part of the product most likely to worry someone, so here is exactly what happens when a visitor sends a message and the AI is switched on.

First, the message is checked without leaving our systems. If the visitor asked for a human, if the account is a Free account at its monthly limit, if the AI has already replied too many times in that conversation, or if the account is a Free one whose owner has not yet confirmed their email address and has already used its first few answers, it hands over to a human immediately. Nothing is sent to any AI model in those cases — no embedding, no model request, nothing leaves. The last of those is an anti-abuse gate on self-serve signups rather than a privacy control, and it is described in the Terms of Service.

Then the message is embedded. The visitor's message text is sent to Google's text-embedding-005 model on Vertex AI in us-central1, which returns a list of numbers representing its meaning. This is used to find matching help articles.

Then, only if a published article actually matches, a request goes to Anthropic's Claude, running on Google Cloud's Vertex AI. That request contains, and contains only:

  1. A fixed instruction telling the model to answer only from the articles provided, never to invent policy or pricing, and to hand over if it cannot answer.
  2. Up to five of the customer's own published help articles, in full.
  3. Up to the last six messages of that conversation.
  4. The visitor's question.

That is the entire payload. The visitor's name, email address, contact id, custom attributes, city and visitor id are never sent to the model. Neither is anything from any other workspace.

If no article matched, the model is never called at all. The conversation goes to a human and the question is recorded as a documentation gap.

Two other features use the same model.

Help-centre search embeds the search phrase to rank articles. We do not write the phrase into the database: there is no search history on a contact or a conversation, and nothing in the product keeps a record of what a given visitor searched for. It does not follow that the phrase disappears, and we would rather say so than let the previous sentence do more work than it can. The phrase travels in the search page's own address (/search?q=…) and again in the query string of the search request itself (apps/admin/components/help/center/SearchClient.tsx), so it is kept in that visitor's browser history like any other page they visited, and it appears in ordinary Google Cloud request logs on the retention described in section 9. A search phrase is whatever somebody typed into a box, so it can contain personal information — people do type their own name, their order number or their email address into a help-centre search.

Guide drafting sends the text of a recorded walkthrough — the steps, the button labels, the values typed into non-sensitive fields — to Claude to draft an article. Screenshots are not sent to the model; they stay in Cloud Storage.

The AI always says it is an AI

This is a product commitment, not a marketing line, and it is enforced in the widget's code. The assistant is labelled "AI Assistant" in the chat header. If a customer gives it a name, the label "AI Assistant" is shown beneath that name. There are no stock photographs of invented support agents. The "online" indicator appears only while the AI is genuinely answering and disappears the moment a human takes over.

Training

We do not train, fine-tune or otherwise improve any model on customer or visitor content, and nothing in our Vertex AI configuration opts into any training or model-improvement programme. We send a request and use the response.

⚠️ DECISION NEEDED — a fact to look up, not a judgement call. The paragraph above is true of our configuration, which is all the code can prove. What it does not cover is what Google and Anthropic do at their end. Three things to read off the current documents and then state here by name and date:

  1. The Google Cloud Service Specific Terms for Vertex AI — confirm they say Google does not use customer data submitted to the service to train its models, and quote the clause.
  2. The abuse-monitoring retention window Google applies to Vertex AI generative requests, and whether it is switched on for our project. This is the one number that matters, because it is the only place a visitor's message sits outside our own systems, and we cannot state a retention period in section 9 that ignores it.
  3. Anthropic's terms for Claude served through Vertex AI — Google is the processor of record there, but confirm what Anthropic receives and retains rather than assuming it is nothing.

Do not publish this section without the citations. "We believe our provider does not train on your data" is exactly the sentence that becomes a misrepresentation.


6. Identified visitors

A customer can tell us who a visitor is, so that a logged-in user of their product sees their own conversation history rather than starting from scratch.

This is done with a signature, not a claim. The customer's own backend computes an HMAC-SHA256 using a secret that only they and our servers hold. The widget sends that signature along with the user id, and optionally an email and name. Our server recomputes the signature and only accepts the identity if it matches.

The point is to stop somebody typing another person's user id into a browser console and reading their support history. Without the signature, an identity claim is ignored and the visitor stays anonymous.

What the signature covers depends on which version of it the customer sends, and the difference decides what the word verified is allowed to mean on a contact record.

  • The current signature (v2) covers the user id and the email address together — a length-prefixed join of the two, so no pair of values can be rearranged into another pair (packages/shared/src/hmac.ts). When an identity arrives this way and we record the address as verified, what we are entitled to say is that the customer's own backend put its name to that specific address for that specific user. That is still not "somebody proved they can read this mailbox" — no message was sent to it — but it is a statement about the address itself.
  • The original signature (v1) covers the user id alone. The address travelled beside it, unsigned, so anyone holding a valid signature for their own user id could present a different address with it. We still accept v1, because every integration built before this change computes it and cutting it off would silently stop identifying those customers' visitors. What we no longer do is call the address verified: an identity that arrives on a v1 signature has its address stored unverified, exactly like an address typed into a lead form. Either version still does the job the signature was built for, which is stopping one person reading another's conversations.

So a support agent reading verified next to an address is reading a claim about that address, never merely about the account it arrived with.

The display name is not covered by either signature and is never marked verified. It is a label for the agent's benefit, and we make no claim about it.

When an identity verifies, the anonymous contact is merged into the customer's existing record for that person, and the conversations move with it.


7. Cookies and browser storage

The widget sets no cookies. It uses your browser's local storage, which is not sent to any server with requests, and only on the origin where the widget is embedded.

What the widget stores in a visitor's browser:

  • support_visitor_id — the random visitor id described above. It has no expiry and persists until the browser's storage is cleared.
  • A Firebase Authentication session for the anonymous sign-in, stored by Google's Firebase SDK.
  • Small flags remembering that the visitor dismissed a prompt, skipped a pre-chat form, or voted on whether a help article was useful — so they are not asked twice.

If storage is unavailable (private browsing, storage disabled), the widget carries on; the visitor simply gets a new id each session.

The agent dashboard, which only logged-in staff see, sets one cookie (sidebar_state) to remember whether the sidebar is open, and uses local storage to remember the last workspace and dismissed tips.

Our own site runs our own widget. app.heythula.com — including this page — loads the HeyThula widget, pointed at our own support workspace. So visiting our site creates a contact record in our workspace, on exactly the terms described in section 3.

⚠️ DECISION NEEDED: does any of the above need a consent banner, and whose banner is it? The facts are settled — no cookies from the widget, no advertising or analytics identifiers, local storage only — so this is a reading of the law, not of the code. Three positions:

  • No banner: rely on the strictly-necessary exemption. The visitor id and the anonymous auth session exist only so the chat works and so a visitor can read their own history and nobody else's. That is the ordinary reading of ePrivacy Directive Art. 5(3), and it is what support widgets generally do.
  • A banner or a pre-chat notice, on the customer's site. Safer where regulators read "strictly necessary" narrowly — France's CNIL and the German authorities are the usual examples — and unavoidable the moment a customer's own counsel insists on one.
  • Make it the customer's contractual obligation. The Terms already require a customer to tell their own visitors that a support tool processes their messages and that an AI reads them. Extending that to "and obtain any consent your own law requires" is one sentence, and it puts the duty on the party who controls the page and knows their audience.

The first is the honest default for what the widget actually stores. The third costs nothing and covers the case where the first is wrong. They are not alternatives — do both. What is not in doubt: if a banner is needed it belongs on the customer's site, not on ours.


8. Who else touches the data

We use a small number of sub-processors. Each is listed with what it actually receives.

  • Google Cloud Platform — Firestore (database, africa-south1), Cloud Storage (files, africa-south1), Cloud Run and Cloud Functions (compute, us-central1 and europe-west4), Cloud Tasks (queues, us-central1), Cloud Build and Artifact Registry (deployment, europe-west4), Cloud Logging. Receives: everything, because it is where the product runs.
  • Firebase Authentication (Google) — receives account holders' email addresses, password hashes, and Google profile data; and anonymous session identifiers for visitors.
  • Google Cloud Vertex AI — receives what is described in section 5: visitor message text, recent conversation turns, and the customer's published articles. Includes Anthropic's Claude models, served by Google.
  • Postmark (ActiveCampaign) — email delivery. For an email to a visitor: their email address, the agent's reply in full, the customer's brand name, and the workspace, conversation and message ids. For an email to an agent: their address and a link only — the message body is not included. Postmark also receives inbound replies to those emails, which we turn into new messages in the conversation. Open tracking is on, so Postmark records when a notification email is opened.
  • Stripe — payments. Receives the account owner's email address, the account name, an internal account id, and one usage event per AI-resolved conversation carrying the Stripe customer id and the conversation id. Card details never touch our systems: payment is taken on Stripe's own hosted checkout and managed in Stripe's own billing portal. We have no card fields, no card-collection library, and no access to card numbers.
  • Intercom — article import only, and only when a customer starts one. We make read-only requests to Intercom's API with an access token the customer pastes in, and read their help centre's articles, collections and images. We never read Intercom conversations or contacts. The token is stored in a server-only location that no client can read, and is deleted when the import finishes, when it is cancelled, or by a cleanup job — with the deletion time recorded so the customer can see it.

[DEFAULT — confirm] We give customers at least 30 days' notice, by email to account owners, before a new sub-processor starts processing their data. A customer who objects on reasonable data-protection grounds can tell us within that window, and if we cannot resolve it they may cancel and stop paying for the remainder of the period rather than accept the change.

Why 30 days: GDPR Art. 28(2) requires the notice-and-object arrangement to be agreed but sets no number, and 30 days is the common SaaS figure — long enough that an objection is real rather than theatrical, short enough that we can still change infrastructure when we have to. There is no notification mechanism in the product: until there is, this is a list of account-owner addresses and a manual send, and somebody has to own it.


9. How long we keep things

Conversations, contacts, messages, articles and uploads are kept indefinitely. There is no automatic expiry on any of them and no clock running anywhere. They stay until somebody deletes them — either a customer deleting a whole workspace, or us deleting something by hand on a customer's instruction. Section 10 says exactly which of those a customer can do for themselves.

Documentation-gap signals are more complicated, and we would rather describe it accurately than round it off:

  • A raw gap signal — holding a visitor's verbatim question and its embedding — is eligible for deletion 180 days after it was recorded. That is implemented and runs on a schedule.
  • But only once it has been counted. A signal that has not yet been clustered is never deleted, however old it is. That is deliberate — deleting an uncounted question would destroy the evidence before it was ever used — but it means a signal can outlive 180 days.
  • The rolled-up backlog item is kept indefinitely, and it retains the representative question and up to five verbatim excerpts of real visitor questions, plus contact ids. There is no expiry on it.

Backups. The database has point-in-time recovery enabled, giving a 7-day rewind window. On top of that we take daily backups kept for 14 days and weekly backups kept for 14 weeks (98 days). So content deleted from the live database can persist in backups for up to 14 weeks before ageing out.

Infrastructure logs. [DEFAULT — confirm] Google Cloud Logging keeps them for 30 days and then deletes them. Why 30: it is what the _Default log bucket already does without anyone configuring it, it is long enough to investigate an incident, and it is short enough that the personal information in there is not accumulated year on year. Two things in a request log can be personal, and both matter to this number: the request IP address, and the help-centre search phrase, which rides in the query string of a search request and is therefore logged with the URL — see section 5. The retention has never been set deliberately, so it must be set explicitly on the _Default bucket rather than left as an unconfigured default that a future project change can move without anyone noticing.

What we intend to keep, and for how long

[DEFAULT — confirm] None of this is implemented. It is the policy the software should be built to, written down so it can be argued with:

  • Conversations, messages and contacts are kept for as long as the customer's account is open, and deleted 90 days after it closes. The clock is tied to the relationship rather than to a fixed period because that is what this product is for — a support history that evaporates on an anniversary is a support history nobody can use, and a customer would be right to be furious. POPIA s14(1)(a) and GDPR Art. 5(1)(e) measure "no longer than necessary" against the purpose, not against a calendar, and while the account is open the purpose is live. The 90 days after closure is a recovery window, not a retention period in its own right.
  • What makes that lawful rather than merely convenient is that a customer can end it at any time: they can delete a whole workspace themselves, and ask us to delete anything smaller. Retention tied to a relationship only works if the relationship can be ended on demand.
  • Raw gap signals keep the 180-day expiry already implemented — and the sweep has to be changed so it deletes unclustered signals too. Today an uncounted question is skipped and lives forever, which is precisely backwards: it is still a verbatim thing a visitor typed.
  • Gap backlog items are kept for 24 months from their last update, and the verbatim excerpts inside them are dropped at 12 months, leaving the representative question and the counts. An excerpt is evidence for a documentation decision that is either taken or abandoned within a year; the cluster itself is a trend line and is worth keeping longer than its quotations.

Until code enforces these, this section describes an intention. The paragraphs above it describe what actually happens, and they are the ones that are true today.


10. Your rights

If you are a customer

You can see and change your account and workspace data in the product at any time.

If you are a visitor

Your rights are against the business you were talking to, not against us — they decide what happens to their customer records. Contact them. If they ask us to act, we will.

The rights themselves

Under POPIA you have the right to ask what personal information is held about you, to have it corrected or deleted where it is inaccurate, irrelevant, excessive, out of date, misleading or unlawfully obtained, to object to processing, and to complain to the Information Regulator of South Africa.

Under GDPR — where it applies — you additionally have rights of access, rectification, erasure, restriction, portability and objection, and the right to complain to your local supervisory authority.

What the product can and cannot do

We would rather say this plainly than let a policy imply otherwise.

What a customer can delete themselves. An account owner or admin can delete a whole workspace from its settings. It is not a confirm dialog — they have to type the workspace's name — and the workspace then carries on working for a 7-day grace period in which the deletion can be called off. After that it is purged: every conversation, contact, message, help article and uploaded file under it, in the database and in file storage, plus its help-centre subdomain and its entry in every registry that named it. The job refuses to report itself finished while anything is left, so "deleted" is a receipt rather than a hope. Customers can also delete help articles and collections at any time.

What they cannot. There is still no way to delete a single contact, conversation or message, no way to delete an account as opposed to a workspace, and no data-export function of any kind. Those requests are handled manually by us, against the database, on the customer's instruction.

[DEFAULT — confirm] We answer a data-subject request within 30 days. Why 30: GDPR Art. 12(3) allows one month and POPIA's prescribed-form process runs to 30 days, so the tighter of the two is a single clock instead of two — and a request that needs longer is better handled by telling the person early than by discovering the deadline twice.

Where to send a request. Data-subject requests, and anything else about privacy, go to our Information Officer, Greg Kockott, at greg@hellocrowd.net, or by post to the registered address in section 15.

⚠️ DECISION NEEDED — an action to complete, not a judgement call. Naming an Information Officer here does not discharge POPIA s55. Section 55(2) says an Information Officer may take up their duties only once the responsible party has registered them with the Information Regulator of South Africa. So:

  1. Register Greg Kockott as Information Officer for Learn App (Pty) Ltd on the Information Regulator's portal, and record the date it was accepted. Until that is done, this section names someone who is not yet entitled to act, and the sentence above is a statement we cannot support.
  2. Deputy Information Officer — probably not required, but decide it out loud. POPIA s56 requires every public and private body to make provision for designating "such a number of persons, if any, as deputy information officers as is necessary". The "if any" is doing the work: for a company this size the honest answer is none, and the Regulator's guidance ties the number to the size, structure and complexity of the body. If one is designated it must be an employee, in writing, and given real time and budget for the role. Record the decision either way, because the registration form asks.
  3. Note that for a private body the Information Officer is, by default, the head of the private body — its chief executive or equivalent, or a person that head has duly authorised. Confirm Greg holds that position or has been authorised in writing by whoever does.

⚠️ DECISION NEEDED: whether to appoint an EU representative (GDPR Art. 27) and a UK representative (UK GDPR Art. 27) now or later. The duty bites when a controller or processor outside the EU/UK offers goods or services to people there — which a public signup page arguably already does. The Art. 27(2) exemption does not obviously help us: it is for occasional processing with no special categories and low risk, and ours is continuous and contains whatever a visitor decides to type.

  • Appoint both now. A representative service costs a few hundred euro or pounds a year each. Their address then has to be published in this policy, which means one more thing to keep current. What it buys is the removal of a question every EU enterprise buyer's counsel asks in procurement, before it costs a deal.
  • Defer until the first EU or UK customer. Cheaper, and defensible while there genuinely are none. It is a live exposure from the moment one signs up, and it is precisely the sort of gap a due-diligence questionnaire is written to find.

Deferring is reasonable if somebody owns the trigger. Nobody owns it today.


11. What we do not do

Naming limits is how the rest of this becomes believable.

  • We do not sell personal information. To anyone, ever, in any form.
  • We do not share data between customers. Every record is scoped to one workspace and the database security rules enforce it. One customer's AI never sees another customer's articles or conversations.
  • We do not train AI models on customer or visitor content, and nothing in our configuration enables it.
  • We do not run advertising, ad networks, or advertising identifiers.
  • We do not run third-party analytics, session recording, heatmaps or product telemetry. There is none in the codebase.
  • We do not track visitors across websites.
  • We do not use conversation content for our own marketing, case studies or examples without asking.
  • We do not read customer conversations except where it is necessary to operate the service — investigating a fault a customer has reported, or complying with a legal obligation. Stated precisely, because the difference matters: this is a rule our staff follow, not a control the software enforces. Anyone with production database access can read any workspace, and there is no per-access logging and no approval step today.

[DEFAULT — confirm] The commitment above is deliberately the narrow one — the rule, stated as a rule — rather than a claim of technical enforcement we cannot back. Why: of every sentence in this document, "we do not read your conversations" is the one most likely to become a misrepresentation, and it does so the moment it is read as describing a control. The control worth building first is Cloud Audit Logs data-access logging on Firestore, which turns "we don't" into "and here is who did, and when". Once that exists, this paragraph should be replaced by it.


12. Security

  • All traffic is served over HTTPS.
  • Data is encrypted at rest by Google Cloud, and in transit.
  • Access to a workspace's data is enforced by database security rules, not only by application code. A visitor's browser can read their own conversations and nothing else. Agents can read only the workspaces their account grants them.
  • Sensitive values — the email provider token, the payment keys, the signing secrets — are held in Google Secret Manager and bound to individual functions, not stored in the codebase.
  • An account can turn on two-factor authentication: a time-based one-time code from an authenticator app, with ten single-use backup codes for a lost phone. We never store a backup code. Only a salted SHA-256 of it is kept, in a document whose security rule denies every client read and every client write, so the only thing that can read it is server code.
  • Someone who signs up with an email address and password is sent a confirmation link, so the address on an account has been proven by whoever holds the mailbox. A Google sign-in arrives already proven and is not asked again.
  • Internal services that are not meant to be publicly callable are checked on every deploy, and the deployment fails if one of them has become world-invocable.
  • Guide recordings deliberately discard the values of fields that look sensitive — passwords, card numbers, security codes, one-time codes — and black out that area of the screenshot.

Two things we would rather you heard from us:

  • A customer's Intercom access token is stored in the database while an import runs, not in Secret Manager. It sits in a server-only location no client can read, and is deleted when the import ends.
  • Files in help media and branding storage are publicly readable by URL. That is by design for published articles, whose images must load for anyone. It also applies to guide screenshots as soon as they are captured — before the guide is reviewed or published. The URLs are long and random, so they are not guessable, but they are not access-controlled either. Do not record a walkthrough over data you would not publish.

If there is a breach

[DEFAULT — confirm] If we discover a compromise affecting personal information we hold, we notify every affected customer within 72 hours of becoming aware of it — not of confirming its full extent — and we tell them what they need in order to make their own notification: what happened, which categories of data and roughly how many records, the likely consequences, and what we are doing about it. Where the data is a customer's own account data, so that we are the responsible party rather than the operator, we notify the Information Regulator of South Africa and the affected people ourselves, as soon as reasonably possible.

Why 72 hours: POPIA s22 sets the standard at "as soon as reasonably possible" and gives no number. GDPR Art. 33 gives 72 hours to the controller, and Art. 28(3)(f) obliges a processor to tell its controller "without undue delay" — but the controller's 72-hour clock does not start until we do, so anything slower than 72 hours makes it impossible for our customers to comply. GDPR's is the tighter of the two floors; committing to it everywhere is one process instead of two, and it is the number every enterprise security questionnaire asks for.


13. Children

HeyThula is sold to businesses and is not directed at children. We do not knowingly collect personal information from children.

[DEFAULT — confirm] We treat 18 as the age below which a person is a child, and we do not knowingly collect personal information from anyone under it. If we learn that we hold a child's information without a competent person's consent, we delete it.

Why 18: it is POPIA's line, and POPIA treats a child's personal information as a special category needing consent from a competent person (s34, s35). GDPR Art. 8 sets 16, which member states may lower to 13. Taking the stricter of the two is one number to remember instead of a per-jurisdiction table, for a product that never asks anyone's age and could not check an answer if it did.

Where a customer's own audience includes minors, obtaining that consent is the customer's obligation and not ours: they are the responsible party for their visitors, they know who their audience is, and they control the page the widget sits on. The Terms should say so explicitly.


14. Changes to this policy

If we change this policy in a way that materially affects how we handle personal information, we will tell customers before it takes effect.

[DEFAULT — confirm] 30 days' notice, given both ways: an email to account owners and a notice in the product. Changes that are not material — a clarification, a correction, a sub-processor already covered by the 30-day notice in section 8 — take effect when they are published, and the date at the top of this document changes.

Why both channels and 30 days: an email alone reaches an address that may no longer be watched, an in-product notice alone reaches whoever happens to log in, and 30 days is the same period as every other notice in these documents, so there is one number to remember rather than four.


15. Contact us

  • The company: Learn App (Pty) Ltd, trading as HeyThula.
  • Registration number: 2020/142526/07.
  • Registered and postal address: 173 Oxford Road, Rosebank, 2196, South Africa. [CONFIRM] the postcode — see section 1.
  • Privacy questions and data-subject requests: Greg Kockott, greg@hellocrowd.net.
  • Information Officer: Greg Kockott, greg@hellocrowd.net.

[CONFIRM] the email domain. hellocrowd.net is not the entity's name and not the product's domain. That is common enough — a trading name, a group company, an address that predates the product — but a privacy policy that gives an address on an unrelated domain invites the reader to wonder who they are actually writing to. Confirm it is the right mailbox, or move it.

⚠️ DECISION NEEDED: whether these are role addresses rather than a personal one. This is a preference, not a legal question, and it is yours to make. The case for privacy@ and legal@ on our own domain, with a redirect to whoever holds the role today:

  • A personal address in a published policy gets scraped. This page is where address harvesters look, and once it is in a list it is in a list.
  • It ages badly. The first person to hold a role rarely holds it forever, and the address outlives them in every copy of the policy a customer has filed, in every DPA that names it, and in every procurement questionnaire that quotes it.
  • A regulator, a data subject or an enterprise buyer writing to privacy@ gets a mailbox that is still monitored in two years. That is the whole benefit; there is no other.

The cost is one mailbox and one forwarding rule. If role addresses are wanted, set them up first and then change this page — a policy that publishes an address nobody has created yet is worse than one that publishes a personal one.

If you are a visitor and want something done about a conversation you had, please contact the business you were talking to first. They control that record.