Privacy
Privacy policy
What Toli stores, where, why, for how long, and what you can demand of us. Every statement on this page names a table, a column, a command or a file, so that you can go and check it. And wherever the code does not do what a policy usually promises, that is written here too, in as many words.
1. Who is responsible, who to write to, and for what
Toli is published and operated by Essabu Intelligence Provider, the controller for the data of the platform itself. Anything concerning data goes to toli@essabu.com.
- No data protection officer has been appointed. Requests are read and handled by the team that operates the platform, at that same address.
- We are the controller for what is ours: the API keys (table api_keys), the access tokens (oauth_access_tokens), the usage records (readings) and the server's technical logs.
- We are the processor for whatever a customer pushes through Toli: the content sent to /v1/ask and /v1/batch, the text of the services and incidents of a status page, the addresses of the people subscribed to that page. The customer is the controller there, it is theirs to disclose to their own users, and we act on that data only on their instruction.
- There is no self-service sign-up yet: a key is issued by hand with the toli:key:issue command, after a direct conversation. So we have no accounts table, no billing table, and no contact-address column.
2. What is actually stored, field by field
The list is exhaustive as of the last revision. A category missing from here is a category we do not have.
- Keys and access, table api_keys: name (free text naming the organisation or the application, and on its own the whole of a customer's identity in the database), hash (SHA-256 of the secret toli_ followed by 32 random characters), client_secret_hash (SHA-256 of a 40-character client secret), scopes, status_page_id, monthly_quota, revoked_at, created_at. No password, no person's name, no contact address: the column does not exist.
- Access tokens, table oauth_access_tokens: api_key_id, scopes, expires_at, revoked_at, and hash, the SHA-256 of a token toli_at_ followed by 40 characters. A token lives one hour (3,600 seconds).
- Readings, table readings: api_key_id, model as the engine reported it, questions which holds only the NAMES of your questions (strings you choose), input_tokens, output_tokens, latency_ms, client_ref and answers.
- The state you send (up to 128,000 characters of text or JSON) is written to no table. It goes to the judgement engine (section 5) and lives in memory only for the duration of the call. An unhandled exception can nevertheless carry it into a stack trace: see section 8.
- readings.answers holds the engine's answers array verbatim, unfiltered, and only on the /v1/batch path (BatchService.php, line 61). If the engine puts a written rationale or a quotation of your state in there, that is what comes to rest in our database and stays.
- readings.client_ref is your own reference, at most 128 characters, stored in clear under an index unique per key. Nothing validates its shape: if you put an e-mail address or a national id in it, an address or a national id is what we store. Put an opaque reference there.
- Status pages: status_pages (slug, organisation_name, default_locale, accent_colour, logo_url, custom_domain), status_services (slug, name, description, position, state), status_daily_statuses (day, state, uptime, one row per service per day), status_incidents (slug, title, impact, status, started_at, resolved_at, service_ids) and status_incident_updates (status, body, posted_at). That text is written by the customer; we do not review it and we do not edit it.
- Incident subscribers, table status_subscribers: email in clear, because writing to someone requires their address, locale (fr or en), confirmed_at, confirm_token_hash (SHA-256 of 48 hexadecimal characters, set back to NULL on confirmation) and unsubscribe_token_hash (same shape, kept for the life of the row). No name, no IP address, no measurement of whether a mail was opened.
- Support messenger: nothing is stored here today. There is no conversations table, no messages table, and no /v1/support route in the API. The widget shown in the interface does however set a cookie and, where relevant, values in browser storage: they are named one by one in section 11.
- Technical cache, table cache: an api-key:{digest} entry holding a key's resolved identity for five minutes (id, name, scopes, quota, page), an oauth-token:{digest} entry of sixty seconds, a quota:{key}:{YYYY-MM} counter of forty days, and the rate-limit signature described in section 8.
- Queues, tables jobs and failed_jobs: the serialised payload of a reading record (key id, model, question names, token counts, latency, client_ref). failed_jobs adds the full stack trace of the failure.
- Tables inherited from the Laravel skeleton, created and empty: users, password_reset_tokens and sessions, the last of which carries ip_address and user_agent columns. There is no model, no authentication guard and no session middleware in this code: nothing fills them. We name them because a database dump or an audit will find them.
3. What it is for, and on what legal basis
One purpose per category, and the basis that permits it. Nothing is processed for a purpose that is not listed here.
- Running the service, basis: performance of the contract. Covers api_keys, oauth_access_tokens, readings and every status_ table.
- Counting usage, holding the monthly quota and billing, basis: performance of the contract. Covers input_tokens, output_tokens and the quota:{key}:{YYYY-MM} counter.
- Telling a status page's subscribers, basis: consent. That consent is embodied in a double opt-in: an address receives an alert only once confirmed_at is set.
- Keeping the platform standing and limiting abuse, basis: legitimate interest. Covers the rate limit on /v1/oauth/token and /v1/oauth/revoke, the request id, and the error log.
- Diagnosing a failure, basis: legitimate interest. Covers failed_jobs and storage/logs/laravel.log.
- For the data our customers push through Toli, the legal basis is theirs and not ours: we process it on instruction, for no purpose of our own. We do not analyse it, we do not aggregate it, we draw no commercial statistic from it.
4. What we do not do
These sentences are commitments, not turns of phrase. Were one of them to stop being true, it would be removed from here first, not afterwards.
- We train no model on the contents of your calls, your incidents or your pages. We have no model to train in any case: Toli is a gateway, and the judgement is rendered by a third party (section 5).
- We cannot speak for that third party. What the engine's provider does with the requests it receives falls under its own contract, which we do not reproduce here and for which we do not vouch.
- We do not sell, rent or hand over your data to anyone, for any commercial or advertising purpose.
- There is no third-party tracker: no analytics, no tag manager, no external script, no remote webfont, neither in web/src nor in package.json. The page you are reading loads nothing from anywhere else.
- We claim no certification, no external audit, no ISO standard, no compliance badge, and no published penetration test. We have none.
5. Hosting, and who else sees your data go by
The full list of third parties, and it is longer than what this page used to say.
- Hosting: a dedicated server located in Germany, rented from Hetzner. PostgreSQL runs on that same machine, not on a third-party managed database.
- Judgement engine: every call to /v1/ask and /v1/batch sends the complete request body, your state included, to the address configured in ENGINE_BASE_URL, which defaults to https://api.typesafe.ai. That third party therefore receives the content you submit for judgement, the instructions of your questions and their names. It is the most consequential processor in the chain, and the previous version of this page did not mention it at all.
- Transactional e-mail: subscription confirmations and incident notifications go out through a sending provider, which therefore sees the recipient's address and the body of the message. The transport is chosen at deployment; the repository's default, MAIL_MAILER=log, sends nothing and writes the mail to a log file, which is dealt with in section 8.
- Nobody else. There is no analytics service, no third-party CDN, no external support tool, no data warehouse.
6. Transfers outside the European Union
What we know, and what we do not know.
- Data at rest is in Germany: the database, the files and the logs live on that one server.
- The content sent to the judgement engine leaves that server for a public address for which we publish no localisation guarantee. Until such a guarantee is obtained and written here, assume that a call may be processed outside the European Union.
- We rely on no adequacy decision and on no standard contractual clauses that we could show you today. Saying so is the only honest thing while that remains undone.
7. Retention, and where nothing enforces it
Two lists, because the difference between a written rule and an executed one is exactly what a privacy policy must stop blurring.
- Deleted by code: OAuth access tokens, removed once expires_at is more than seven days old, by the toli:oauth:tokens:purge command scheduled daily at 03:20.
- Deleted by code: a subscriber's row, genuinely deleted on unsubscribe; and confirm_token_hash, set back to NULL on confirmation.
- Expired by the cache: five minutes for a key's resolved identity, sixty seconds for a token, forty days for the monthly quota counter, sixty seconds for the rate-limit signature.
- Nothing deletes readings, including answers and client_ref: no scheduled job, no pruning rule, no delete path. They go only with the key, by cascade, and a revocation does not delete a key: it stamps revoked_at.
- The ninety days of uptime history are a READ WINDOW, not a retention period. The query reads only the last ninety days; day ninety-one is still in status_daily_statuses. The previous version of this page said otherwise, and it was wrong.
- Nothing deletes pages, services, incidents or their updates: they survive for as long as the status page exists.
- Nothing deletes a subscription that was never confirmed: the row and its token digest stay indefinitely.
- Nothing prunes failed_jobs: the pruning command exists in the framework, but it is not scheduled.
- Nothing rotates or purges storage/logs/laravel.log: it is a single file, with no rotation and no maximum count, and it grows without bound.
- A revoked key is never deleted: its digest stays so that past usage remains attributable.
- The six points above are open defects, not choices. Writing a period here that the code does not enforce would be exactly what this page refuses to do.
8. Logs, IP addresses and e-mail
The part usually left out, because it is the least flattering.
- A single logging call exists in the whole application: a warning, « status notifier failed », recording the page slug, the incident slug and the error message. No address, no IP.
- No application code reads the caller's IP address. The request-id middleware reads only the x-request-id header, or mints req_ followed by 12 characters, and echoes it back in the response.
- The only IP processing in the entire system is Laravel's rate limit on /v1/oauth/token and /v1/oauth/revoke: the address is digested with SHA-1 into a cache key that lives sixty seconds. No TrustProxies is configured, so behind a proxy that key carries the proxy's address and not yours.
- HTTP access logs come from the server in front (FrankenPHP and its Caddy), go to standard output, and contain the client IP, the path called and the user agent. They are configured nowhere in this repository: no retention is set for them, so we announce none here.
- MAIL_MAILER defaults to log. On any environment that has not overridden that value, every confirmation mail and every incident notification is written into storage/logs/laravel.log: the recipient's address, the confirmation link with its plaintext token, the body of the message. That is the file nothing purges, and it is the most urgent defect on this page.
- Unhandled exceptions go to the same file, at debug level in the example configuration. A stack frame there can carry the state argument, the very thing the database deliberately never stores.
9. Security: what the code actually does
The measures that exist, and after them the ones that are missing. A list holding only the first kind would be worth nothing.
- Everything travels over HTTPS.
- No secret is stored in clear: API key, client secret, access token, confirmation token and unsubscribe token are all held as SHA-256 digests and compared with hash_equals, that is, in constant time. We cannot give you back a lost key, and neither could anyone reading the database.
- Tenant separation is an application scope written at the bottom of every status query (where page_id = ?), doubled on the database side by composite foreign keys (page_id, service_id) and (page_id, incident_id): a row therefore cannot point at another page's service or incident. This is NOT a PostgreSQL row-level security policy: there is none in this repository.
- A status identifier is unique within a page and never globally, and an agent key is bound to exactly one page through status_page_id: it cannot reach another customer's page by changing an address segment.
- A key carries explicit scopes (ask, batch, usage, status, support), a monthly quota, and immediate revocation by stamping revoked_at.
- What is missing, said too: the agent console keeps a live API key, in clear, in browser storage, and that key is shared between agents. So there is no per-agent identity, a name shown in a conversation is a key's name and not a person, and one agent cannot be revoked without revoking them all.
- What is missing, again: there is no application-level encryption of content at rest. Readings, incidents and subscriber addresses sit in clear in PostgreSQL, protected by access to the machine rather than by a key.
10. Your rights, and how they are exercised in practice
Access, correction, erasure, restriction, objection, portability. One address: toli@essabu.com.
- We acknowledge receipt within five working days and answer on the substance within thirty days.
- Except for unsubscribing, none of these operations is automated: there is no export path and no deletion path in the code. A request is carried out by hand by an operator, query after query. That is a human promise and not a function, and you deserve to know it before relying on it.
- Unsubscribing, on the other hand, is immediate and genuinely deletes the row. The link is in the confirmation mail you received when you subscribed.
- Careful: incident mails carry NO unsubscribe link today, only the confirmation mail does. If you have lost it, write to toli@essabu.com and the address is removed. This is a known defect, contradicted by our own documentation, and it will be fixed.
- If you are an end user of one of our customers (subscribed to THEIR status page, or your data went through THEIR call), write to them first: they are the controller, we are only the processor. Write to us anyway if you do not know whom to ask, and we will pass it on.
- We name no supervisory authority here: until the competent authority for Essabu Intelligence Provider is settled, naming one would be one more claim we could not hold. Your right to complain to the authority of your country of residence is of course unaffected.
12. Children
Toli is an API sold to organisations, with no self-service sign-up: a key is issued by hand. We do not address children, we ask for no age, and no code verifies an age. The only field in the whole database that can hold the personal data of a natural person is status_subscribers.email, filled by whoever types an address into a public subscription form. If you believe a child's address is in there, write to toli@essabu.com: the row is deleted, with no further formality.
13. Changes to this page
This page lives in the repository, in the file web/src/lib/legal.ts: every revision is a commit, dated and readable. Last revised: 20 September 2026. A change that widens what we collect, or that adds a recipient, will be announced to you before it is applied, through the very address by which your key was handed to you: it is the only channel we have, for want of a contacts table. Nothing here will be widened by a silent revision.
© 2026 Toli. Essabu Intelligence Provider. All rights reserved.