Data residency for fintech: the pipeline runs where your data lives
Before your security team reads a single feature of an event pipeline, they ask where it runs and whose servers it shares. When policy keeps customer data inside your own cloud account, a shared SaaS pipeline fails that review, and the alternative is building and running one in-house.
Jitsu runs inside that boundary, as Private Cloud in a sub-account of yours or as On Prem. Events from your website, apps and backend go to that deployment, and one function on the way to your warehouse writes a masked copy for analysts and an unmasked copy for a short access list.
TL;DR
- Private Cloud and On Prem both come with the Enterprise plan, which includes a security and compliance review, SSO via OIDC and an uptime SLA.
- The masked copy hashes email with SHA-256, sets the last IPv4 octet to 0, and removes phone, name and address.
- Platform teams that want to run Jitsu themselves can deploy the open-source version on Kubernetes.
Private Cloud or On Prem
Private Cloud is a single-tenant deployment that Jitsu manages on a dedicated GCP or AWS sub-account, yours or Jitsu's, and its servers are not shared with other customers.
On Prem runs Jitsu on your own infrastructure, with any cloud provider or on your own servers.
Two copies of every event
Reconciliation and investigations need events unmasked, while analysts can do their work without anyone's email or full IP address. Monzo's data stack write-up describes this pattern, built in-house on BigQuery.
Jitsu does it with one function, placed last on the warehouse connection behind the guardrail. The fields the guardrail lists by name, and any value outside an ID field that reads as a card number, reach neither copy; add your own field names to its list. This is what each copy holds:
| Event | Sent from | Required properties | In events_raw | In events (masked) |
|---|---|---|---|---|
identify | Browser, app, backend | userId, trait email | traits unmasked | email hashed with SHA-256; phone, name, first_name, last_name, address removed |
page | Browser | url | full IP address | IPv4 address with the last octet set to 0 in context.ip and requestIp; IPv6 removed |
Account Created | Backend | signup_method | unmasked | traits masked as above |
KYC Rejected | Backend | kyc_provider, reason_category | unmasked | traits masked as above |
Deposit Completed | Backend | deposit_id, amount, currency, funding_method | unmasked | amounts unchanged |
The fintech tracking plan (CSV) marks the PII handling for all 35 events.
Add it in the Jitsu UI function editor:
import { hash } from "crypto";
const RAW_TABLE = "events_raw";
const TEST_USERS = new Set(["integration-tests"]);
const HASHED_TRAITS = ["email"];
const REMOVED_TRAITS = ["phone", "name", "first_name", "last_name", "address"];
function maskTraits(traits) {
if (!traits || typeof traits !== "object") return undefined;
const out = { ...traits };
for (const key of REMOVED_TRAITS) delete out[key];
for (const key of HASHED_TRAITS) {
if (typeof out[key] === "string") out[key] = hash("sha256", out[key].trim().toLowerCase());
}
return out;
}
// 203.0.113.42 becomes 203.0.113.0. Anything that is not an IPv4 address is removed.
function truncateIp(ip) {
return typeof ip === "string" && /^\d{1,3}(\.\d{1,3}){3}$/.test(ip) ? ip.replace(/\.\d{1,3}$/, ".0") : undefined;
}
// Must be the last function on the connection: it returns two events.
export default async function rawAndMaskedCopy(event) {
if (TEST_USERS.has(String(event.userId))) return "drop";
const context = event.context ?? {};
const masked = {
...event,
requestIp: truncateIp(event.requestIp),
context: { ...context, ip: truncateIp(context.ip), traits: maskTraits(context.traits) },
};
if (event.traits) masked.traits = maskTraits(event.traits);
return [{ ...event, JITSU_TABLE_NAME: RAW_TABLE }, masked];
}
It returns two events, so it has to be the last function on the connection: only the last function may return more than one event (advanced functions).
- Jitsu records the sender's IP address in
requestIpon every event, separately fromcontext.ip, so the masked copy truncates both. - An unsalted SHA-256 of an email is pseudonymous, not anonymous: anyone holding a list of emails can match it. Treat the masked table as personal data, and restrict
events_rawto the people who need plaintext. - Event backups are a Jitsu Cloud feature, so on a self-hosted install
events_rawis also your archive of the events that reach the warehouse.
Checking the two copies
Run this on ClickHouse every day.
-- ClickHouse. Daily row counts in the raw and masked copies, and any plaintext email or full IPv4 address left in the masked one.
SELECT
day,
raw_rows,
masked_rows,
raw_rows - masked_rows AS difference,
plaintext_emails,
full_ips
FROM
(
SELECT toDate(timestamp) AS day, count() AS raw_rows
FROM events_raw FINAL
WHERE timestamp >= today() - 30
GROUP BY day
) AS raw
FULL OUTER JOIN
(
SELECT
toDate(timestamp) AS day,
count() AS masked_rows,
countIf(position(context_traits_email, '@') > 0) AS plaintext_emails,
countIf(match(request_ip, '^\\d+\\.\\d+\\.\\d+\\.\\d+$') AND NOT endsWith(request_ip, '.0')) AS full_ips
FROM events FINAL
WHERE timestamp >= today() - 30
GROUP BY day
) AS masked USING (day)
ORDER BY day;
A non-zero difference that stays after the day's loads finish means one copy failed to load; check that destination's loads in Live Events. If an earlier function on the connection sends some events to their own table, as the KYC page's contract function does with kyc_contract_violations, their masked copies land in that table, so add its daily count to masked_rows before comparing.
When plaintext_emails is above zero, events reached events without passing the function, for example from a connection that does not run it.
Running the open-source version yourself
The production deployment guide covers what a self-hosted install needs:
| Component | What it does | Needed |
|---|---|---|
| Kubernetes cluster | Runs every service | Always |
| Jitsu operator | Provisions function servers and keeps their routing table | Always, even with no custom functions |
| PostgreSQL | Configuration and metadata | Always |
| Kafka or Redpanda | Transport between services | Always |
| ClickHouse | Jitsu's own events log and metrics | Always |
| MongoDB | Function key-value store, and matching anonymous IDs to user IDs | For those features |
| Redis | Alternative functions state store | Optional |
| Syncctl | Connector syncs as Kubernetes CronJobs | Only with connectors |
It also lists the Kubernetes permissions the operator needs, and the quick start deploys every service to Minikube for a local trial. The self-hosted vs managed comparison covers what it costs to run. Events reach a self-hosted install the same way they reach Jitsu Cloud, at your own domain.
Questions people ask
Can Jitsu Cloud keep data in a specific region? Jitsu Cloud runs in the United States. To keep data inside your own cloud account or network, run Private Cloud in your sub-account, On Prem, or a self-hosted install.
What can a security review get from Jitsu? The security page covers SOC 2, and the report is available on request. Questions the report does not answer go into the security and compliance review that comes with the Enterprise plan.
Is self-hosted Jitsu free? Yes. Jitsu is open source under the MIT license, with no usage limits on a self-hosted install.
Get started with Jitsu
Private Cloud and On Prem start with a demo call that scopes the deployment and the security review. The fintech hub covers the other fintech pipelines. To see the pipeline before that, follow the self-hosting quick start, or send test events to a free Jitsu Cloud workspace.