What is kept, and what leaves
BetaEvents carry more than you mean to send: an email in an error message, an IP address in a request, a card number in a log line. The data policy decides, for each kind of data and each place it can go, whether it is kept as written, masked, or kept from AI altogether. One policy covers every product in the workspace.
Where to find it
Under Data policy in the workspace sidebar, or with tilldev data-policy show. Everyone in the workspace can read it; owners and admins can change it. Someone holding time-boxed admin can tighten it but not loosen it.
The Try it panel shows what each channel would send for any text you paste, under the policy you are editing, before you save.
What it recognises
| Class | Covers |
|---|---|
Credentials (credential) | Private keys, cloud and service tokens, bearer tokens, JWTs, webhook URLs and database URLs with a password. |
Contact details (contact) | Email addresses and phone numbers. |
Payment data (financial) | Card numbers and M-Pesa transaction codes. |
Government IDs (government_id) | National ID numbers. |
IP addresses (network) | IPv4 and IPv6 addresses. |
Credentials are always masked, everywhere. Fields whose name says they hold a secret, such as password or authorization, are masked whatever they contain.
Where it applies
| Channel | Covers |
|---|---|
Stored events (storage) | What TillPulse keeps from each event, before anything is written. |
Alerts (alerts) | Alert messages on every destination, and the event they carry. |
Issue trackers (trackers) | Titles and descriptions of tickets opened in Linear or Jira. |
Webhooks (webhooks) | TillAuth and TillForge webhooks to your endpoints. |
AI (ai) | Questions and context sent to a model: the assistant, questions about your data and TillMind’s analysis of new issues. |
Each cell of the policy is one of Keep (allow), Mask (redact) or Block (block). Masking swaps the value for a marker such as [email] or [ip], so the rest still reads. Blocking only exists for AI: a request that contains a blocked class is refused before anything is sent, and the person asking is told which kind of data to take out, never the value itself.
Where a new workspace starts
| Class | Stored events | Alerts | Issue trackers | Webhooks | AI |
|---|---|---|---|---|---|
| Credentials | Mask | Mask | Mask | Mask | Mask |
| Contact details | Mask | Mask | Mask | Keep | Mask |
| Payment data | Mask | Mask | Mask | Keep | Mask |
| Government IDs | Mask | Mask | Mask | Keep | Mask |
| IP addresses | Mask | Mask | Mask | Keep | Mask |
Webhooks keep contact details by default because the endpoints receiving them are yours and usually need the address, for example to email a user who just signed up. Mask them if your endpoint doesn’t.
Your own patterns
Add up to 10 patterns for data only you know the shape of: customer numbers, internal account ids, a partner’s reference format. Each has a name, which becomes its marker ([customer_no]), a regular expression of at most 256 characters, and the class it belongs to, which decides where it is masked. A pattern with no class counts as a credential and is masked everywhere.
- Names are up to 40 lowercase letters, digits or underscores, starting with a letter.
- Patterns run in time proportional to the text, never more, so a pattern can’t slow ingestion down. Backreferences and lookaround aren’t supported, and a pattern that is too complex is refused with its size and the limit.
- A pattern that can match empty text is refused.
Stored events and the SDKs
The TillPulse SDKs mask the built-in classes on the device, before an event leaves it. The stored-events column then applies on our side before anything is written, along with your own patterns, so a pattern you add today covers events from app versions already in the field.
To keep a class in stored events, allow it here and turn off on-device masking with disableDefaultPiiScrubbing in the SDK (React Native, Flutter). Credentials are masked on our side either way.
A user id that looks like an email address, phone number or IP address is always replaced with a stable stand-in unique to your workspace, whatever the policy says. Counts of affected users still work; the address is never stored.
What reaches a model
Every request to a model on behalf of the workspace passes the AI column first: the assistant, questions about your data, answers grounded in your workspace’s data, and TillMind’s analysis of new issues. This holds whichever model and key you use.
TillMind analyses new and regressed issues on its own. Turn off AI event analysis to stop that; you can still ask about an issue yourself, and that question follows the policy like any other.
What reached AI, at the foot of the page, lists every hand-off that masked or refused something, every TillMind analysis and every TillForge AI request, with where it came from, how much was sent and how much was taken out. It never keeps what was sent. Owners and admins can read it, or use tilldev data-policy egress.
What it caught
Each channel counts what it masked and refused, per class and day, for the last 90 days. The counts carry no values. Read them on the page, with tilldev data-policy hits --days 30, or from the API.
The record
Every change is in the workspace audit log as data_policy.updated, with who made it and each cell it loosened. The page warns you before you save a change that loosens anything.
From the terminal
tilldev data-policy show # the matrix, your patterns and what changed last
tilldev data-policy set contact.webhooks redact # mask email and phone in webhooks
tilldev data-policy set network.ai block # refuse any AI request that contains an IP address
tilldev data-policy ai-analysis off # stop TillMind analysing new issues on its own
tilldev data-policy pattern add customer_no 'CUST-[0-9]{6}' --class contact
tilldev data-policy pattern rm customer_no
echo "jane@acme.io paid from 41.90.12.3" | tilldev data-policy test --stdin
tilldev data-policy hits --days 7
tilldev data-policy egress --refusedFrom the API
The endpoints are under Data policy in the API reference. An API key needs org.read to read the policy, preview it, count hits or read the AI ledger, and org.policy.write to change it.
curl -s https://tilldev.dev/api/org/data-policy -H "authorization: Bearer $TOKEN"
# Fields you leave out keep their value
curl -s -X PATCH https://tilldev.dev/api/org/data-policy \
-H "authorization: Bearer $TOKEN" -H "content-type: application/json" \
-d '{"rules":{"network":{"ai":"block"}},"custom":[{"name":"customer_no","pattern":"CUST-[0-9]{6}","class":"contact"}]}'
# What each channel would send, under the saved policy or a draft
curl -s -X POST https://tilldev.dev/api/org/data-policy/preview \
-H "authorization: Bearer $TOKEN" -H "content-type: application/json" \
-d '{"text":"jane@acme.io from 41.90.12.3","policy":{"rules":{"contact":{"ai":"allow"}}}}'