| |

SQL 115 🛢️ Data Vault 2.0 Modeling Mechanics

⏱️ Estimated read: 19 min | |

Data Vault 2.0 is not a dimensional model with different names. It is an integration architecture that separates identity, relationship, and descriptive history into three distinct table types—hubs, links, and satellites—and keys them with deterministic hash values rather than sequential surrogate keys. The separation is what makes the model resilient to source-system changes: adding a new source means attaching a new satellite to an existing hub, not redesigning the hub itself. The hash keys are what make parallel loading possible: multiple processes can independently compute the same key from the same business key without coordinating through a central sequence generator .

The mechanics are deliberately narrow. A hub contains only the business key, the hash key, the load date, and the record source. A link contains the hash keys of the hubs it connects, the load date, and the record source. A satellite contains the parent hash key, the load date, the descriptive payload, and a hash diff for change detection. No descriptive attributes live in hubs or links; no relationships live in satellites. Every table is insert-only. Nothing in the raw vault is updated or deleted .

This chapter covers the three table types and their columns, hash key generation and the normalization rules that make hashes deterministic, the satellite splitting decisions that determine query performance and storage growth, the hash diff mechanism for change detection, and the point-in-time tables that address the raw vault’s most significant query limitation. It assumes familiarity with the star and snowflake schemas from the preceding chapters.

Key point: A Data Vault model is three table types with strict column contracts. Hubs hold business keys; links hold relationships between hubs; satellites hold descriptive attributes and history. Every table is keyed by a deterministic hash, every table is insert-only, and the raw vault has no concept of “current state”—that is the job of point-in-time tables or a downstream dimensional mart.


Why Data Vault 2.0 exists as an integration architecture

The multi-source problem breaks dimensional models. A star schema that integrates customer data from a CRM and an ERP must reconcile conflicting attribute definitions before loading the dimension. When a third source arrives, the dimension is redesigned, and every downstream report that depends on it must be rebuilt. Data Vault handles the same scenario by attaching a new satellite to the existing customer hub. The hub’s business key is unchanged; the new source’s attributes live in their own satellite with their own record source and load date. No existing structure is modified .

Sequential keys break parallel loading. Data Vault 1.0 used surrogate sequence keys, which introduced a dependency: every loader had to wait for the sequence generator to assign an ID before it could write a row. On MPP platforms and big data systems, this dependency eliminates the parallelism that the platform was designed to provide. Data Vault 2.0 replaced sequences with hash keys computed deterministically from the business key. Two independent loaders computing the hash of the same customer ID produce the same value, so they can write to the hub in parallel without coordination .

The audit trail is a first-class requirement. Regulated industries need to know not just what a value is, but when it arrived, from which source system, and what it looked like before. Every row in every Data Vault table carries a load date and a record source. Satellites are append-only, so the history of a customer’s address is a sequence of rows with different load dates, not a single row that was overwritten. The audit trail is the data model, not a separate logging system .

The trade-off. Data Vault is an integration layer, not a consumption layer. A query that returns a customer’s current name and address requires joining a hub to its satellites, filtering to the most recent load date per key, and handling the case where different attributes have different most-recent dates. This is more complex than querying a star schema dimension, which is why every Data Vault implementation includes a presentation layer—a star schema or data mart built from the vault—for BI tools and analysts. Querying the raw vault directly is a common misuse that produces the “multiple rows per business key” symptom and the conclusion that Data Vault is too complex .


a. Hubs, links, and satellites: the three table types

A hub stores unique business keys. Its columns are the hash key (primary key), the business key itself, the load date, and the record source. The business key is the natural identifier from the source system—a customer ID, an order number, a product code. The hub is insert-only: a business key appears once, and the load date records when it first arrived .

-- Hub: unique customer business keys.
CREATE TABLE hub_customer (
    customer_hk   VARBINARY(32) NOT NULL PRIMARY KEY,  -- SHA-256 hash
    customer_bk   VARCHAR(50)   NOT NULL,              -- business key
    load_date     TIMESTAMP     NOT NULL,
    record_source VARCHAR(100)  NOT NULL
);

A link stores relationships between hubs. Its columns are the link hash key, the hash keys of the connected hubs, the load date, and the record source. A customer-order relationship is a link between the customer hub and the order hub. A link can connect more than two hubs, and it can include a degenerate attribute when the relationship itself has a property that does not belong in a satellite—an ordinal for multiple occurrences of the same event, for example .

-- Link: customer-order relationships.
CREATE TABLE link_customer_order (
    customer_order_hk VARBINARY(32) NOT NULL PRIMARY KEY,
    customer_hk       VARBINARY(32) NOT NULL,
    order_hk          VARBINARY(32) NOT NULL,
    load_date         TIMESTAMP     NOT NULL,
    record_source     VARCHAR(100)  NOT NULL
);

A satellite stores descriptive attributes and their history. Its columns are the parent hash key (hub or link), the load date, the payload attributes, a hash diff for change detection, and the record source. The primary key is the parent hash key plus the load date. A satellite attached to a hub describes the entity; a satellite attached to a link describes the relationship .

-- Satellite: customer descriptive attributes.
CREATE TABLE sat_customer_details (
    customer_hk   VARBINARY(32) NOT NULL,
    load_date     TIMESTAMP     NOT NULL,
    hash_diff     VARBINARY(32) NOT NULL,
    record_source VARCHAR(100) NOT NULL,
    full_name     VARCHAR(200),
    email         VARCHAR(200),
    phone         VARCHAR(50),
    PRIMARY KEY (customer_hk, load_date)
);

b. Hash key generation and normalization

The hash key is computed from the business key using a deterministic function—typically MD5 or SHA-256—after normalizing the business key. The normalization is critical: two loaders that compute the hash of 'CUST-001' and 'cust-001 ' will produce different hash keys for what is the same business entity, creating duplicate hub rows. The normalization rules must be identical across every loader and every source system .

The standard normalization steps are: cast the business key to a string, trim leading and trailing whitespace, apply a consistent case (upper or lower), replace nulls with a defined sentinel value, and use a strong delimiter when concatenating multiple columns for a composite key. The delimiter must be a character that cannot appear in the business key itself, or the concatenation of ('AB', 'C') and ('A', 'BC') will produce the same hash .

-- Hash key generation with normalization.
-- SHA-256 produces a 32-byte binary key.
SELECT SHA2(
    UPPER(TRIM(COALESCE(customer_id, '^^'))),
    256
) AS customer_hk
FROM staging.customers;

The choice between MD5 and SHA-256 depends on the platform and the collision requirements. MD5 produces a 16-byte key and is faster; SHA-256 produces a 32-byte key and is collision-resistant. The Data Vault 2.0 specification allows either, as long as the choice is consistent across the implementation . The trade-off is debugging: a hash value cannot be read to identify the business entity it represents, which is why most implementations include the business key in at least one satellite despite the redundancy .


c. Satellite splitting, hash diff, and point-in-time tables

A satellite should not contain every attribute of an entity. The splitting decisions determine storage growth and query performance. The three standard splits are by source system, by rate of change, and by sensitivity .

By source system: CRM attributes and ERP attributes for the same customer live in separate satellites. When the ERP source changes its schema, only its satellite is affected. The hub and the CRM satellite are untouched.

By rate of change: A customer’s legal name changes rarely; a customer’s loyalty points balance changes daily. If both attributes are in the same satellite, every daily balance change creates a new row that duplicates the unchanged name. Splitting the volatile attributes into their own satellite prevents the stable attributes from being copied on every load .

By sensitivity: Personal data—name, email, date of birth—goes into its own satellite, separate from operational attributes. A GDPR erasure request then targets one satellite, not the entire model. Access controls apply to the sensitive satellite only .

The hash diff is the change detection mechanism for satellites. It is a hash computed over the payload columns. When a new row arrives, the loader computes the hash diff and compares it to the most recent row for that parent key. If the hashes match, the row is unchanged and is skipped. If they differ, a new row is inserted . The hash diff must exclude volatile technical fields—a last_synced_at timestamp from the source system will change on every extract and force a new satellite row even when the business attributes are unchanged, causing unbounded growth .

The raw vault has no concept of current state. A query that joins a hub to its satellites returns every historical version. To get current state, the query must filter to the most recent load_date per hub key, and when multiple satellites are involved, the most recent date may differ per satellite. This is the point-in-time (PIT) problem. A PIT table pre-computes the correct load date for each satellite at a given point in time, so a query can join through the PIT table rather than scanning every satellite for the latest row. PIT tables should be added reactively when query performance becomes a problem, not built upfront for every entity .


Complete Example Session

-- ============================================
-- PART 1: HUB — CUSTOMER BUSINESS KEYS
-- ============================================
CREATE TABLE hub_customer (
    customer_hk   VARBINARY(32) NOT NULL PRIMARY KEY,
    customer_bk   VARCHAR(50)   NOT NULL,
    load_date     TIMESTAMP     NOT NULL,
    record_source VARCHAR(100)  NOT NULL
);

-- Insert new business keys only.
INSERT INTO hub_customer
SELECT
    SHA2(UPPER(TRIM(customer_id)), 256),
    customer_id,
    CURRENT_TIMESTAMP,
    'crm_system'
FROM staging.crm_customers s
WHERE NOT EXISTS (
    SELECT 1 FROM hub_customer h
    WHERE h.customer_hk = SHA2(UPPER(TRIM(s.customer_id)), 256)
);

-- ============================================
-- PART 2: LINK — CUSTOMER-ORDER RELATIONSHIP
-- ============================================
CREATE TABLE link_customer_order (
    customer_order_hk VARBINARY(32) NOT NULL PRIMARY KEY,
    customer_hk       VARBINARY(32) NOT NULL,
    order_hk          VARBINARY(32) NOT NULL,
    load_date         TIMESTAMP     NOT NULL,
    record_source     VARCHAR(100)  NOT NULL
);

-- Link hash is computed from the connected hub keys.
INSERT INTO link_customer_order
SELECT
    SHA2(CONCAT(customer_hk, '|', order_hk), 256),
    customer_hk,
    order_hk,
    CURRENT_TIMESTAMP,
    'shopify'
FROM staging.orders o
JOIN hub_customer c ON c.customer_bk = o.customer_id
JOIN hub_order ord ON ord.order_bk = o.order_id
WHERE NOT EXISTS (
    SELECT 1 FROM link_customer_order l
    WHERE l.customer_hk = c.customer_hk AND l.order_hk = ord.order_hk
);

-- ============================================
-- PART 3: SATELLITE — CUSTOMER ATTRIBUTES
-- ============================================
CREATE TABLE sat_customer_details (
    customer_hk   VARBINARY(32) NOT NULL,
    load_date     TIMESTAMP     NOT NULL,
    hash_diff     VARBINARY(32) NOT NULL,
    record_source VARCHAR(100) NOT NULL,
    full_name     VARCHAR(200),
    email         VARCHAR(200),
    PRIMARY KEY (customer_hk, load_date)
);

-- Insert only changed rows (hash diff comparison).
INSERT INTO sat_customer_details
SELECT
    h.customer_hk,
    CURRENT_TIMESTAMP,
    SHA2(CONCAT(COALESCE(s.full_name,''), '|', COALESCE(s.email,'')), 256),
    'crm_system',
    s.full_name,
    s.email
FROM staging.crm_customers s
JOIN hub_customer h ON h.customer_bk = s.customer_id
LEFT JOIN sat_customer_details cur
    ON cur.customer_hk = h.customer_hk
    AND cur.load_date = (
        SELECT MAX(load_date) FROM sat_customer_details
        WHERE customer_hk = h.customer_hk
    )
WHERE cur.customer_hk IS NULL
   OR cur.hash_diff <> SHA2(CONCAT(COALESCE(s.full_name,''), '|', COALESCE(s.email,'')), 256);

-- ============================================
-- PART 4: SPLIT SATELLITE — VOLATILE ATTRIBUTES
-- ============================================
-- Loyalty points change daily; separate from stable details.
CREATE TABLE sat_customer_loyalty (
    customer_hk   VARBINARY(32) NOT NULL,
    load_date     TIMESTAMP     NOT NULL,
    hash_diff     VARBINARY(32) NOT NULL,
    record_source VARCHAR(100) NOT NULL,
    loyalty_points INT,
    tier           VARCHAR(20),
    PRIMARY KEY (customer_hk, load_date)
);

-- ============================================
-- PART 5: QUERYING CURRENT STATE (THE HARD PART)
-- ============================================
-- Raw vault query: returns multiple rows per customer.
SELECT h.customer_bk, d.full_name, d.email, l.loyalty_points
FROM hub_customer h
JOIN sat_customer_details d ON d.customer_hk = h.customer_hk
JOIN sat_customer_loyalty l ON l.customer_hk = h.customer_hk;

-- Current-state query: latest row per satellite.
SELECT h.customer_bk, d.full_name, d.email, l.loyalty_points
FROM hub_customer h
JOIN sat_customer_details d ON d.customer_hk = h.customer_hk
    AND d.load_date = (SELECT MAX(load_date) FROM sat_customer_details WHERE customer_hk = h.customer_hk)
JOIN sat_customer_loyalty l ON l.customer_hk = h.customer_hk
    AND l.load_date = (SELECT MAX(load_date) FROM sat_customer_loyalty WHERE customer_hk = h.customer_hk);

-- ============================================
-- PART 6: POINT-IN-TIME TABLE
-- ============================================
-- Pre-computed snapshot for a given date.
CREATE TABLE pit_customer_daily (
    customer_hk         VARBINARY(32) NOT NULL,
    snapshot_date       DATE          NOT NULL,
    details_load_date   TIMESTAMP,
    loyalty_load_date   TIMESTAMP,
    PRIMARY KEY (customer_hk, snapshot_date)
);

-- Populate nightly: for each customer, the latest load_date
-- per satellite as of the snapshot date.
-- Query current state via PIT: join hub -> PIT -> satellites.

-- ============================================
-- PART 7: THE HASH NORMALIZATION TRAP
-- ============================================
-- Two loaders produce different hashes for the same customer:
-- Loader A: SHA2('CUST-001')    → hash A
-- Loader B: SHA2('cust-001 ')   → hash B
-- Result: duplicate hub rows. Fix: shared normalization function.

-- ============================================
-- PART 8: THE HASH DIFF TRAP
-- ============================================
-- Including last_synced_at in hash_diff:
-- Every extract changes the timestamp, hash differs,
-- new satellite row inserted even when business data unchanged.
-- Fix: exclude volatile technical fields from hash_diff.

-- ============================================
-- PART 9: PARALLEL LOADING
-- ============================================
-- Hub load: independent. Computes hash from staging.
-- Link load: depends on hub hashes, not hub load completion.
-- Satellite load: depends on hub hashes, not hub load completion.
-- All three can run in parallel once staging is ready.

-- ============================================
-- PART 10: THE COMPLETE FLOW
-- ============================================
-- 1. Stage raw extracts (persistent staging area).
-- 2. Normalize business keys, compute hash keys.
-- 3. Insert new keys into hubs (parallel).
-- 4. Insert new relationships into links (parallel).
-- 5. Compute hash diff, insert changed rows into satellites.
-- 6. Build PIT tables for current-state queries.
-- 7. Build star schema marts for BI consumption.

Quick Reference

Table Types and Columns

TableKey ColumnsPayloadInsert-Only
HubHash key, business keyNoneYes
LinkLink hash key, hub hash keysNoneYes
SatelliteParent hash key + load dateDescriptive attributes, hash diffYes

Hash Key Rules

RuleDetail
AlgorithmMD5 (16 bytes) or SHA-256 (32 bytes)
NormalizationTrim, case, null sentinel, delimiter
DeterminismSame input → same hash across loaders
Composite keysConcatenate with strong delimiter

Satellite Splitting

Split ByReason
Source systemIsolate source-specific schema changes
Rate of changePrevent stable attributes copied on volatile changes
SensitivityGDPR erasure targets one satellite

Change Detection

MechanismPurpose
Hash diffFingerprint payload; skip unchanged rows
ExcludeVolatile technical fields (timestamps)
CompareAgainst most recent row per parent key

Best Practices

✅ Do This:

-- Normalize business keys before hashing.                    // ✅
SHA2(UPPER(TRIM(COALESCE(customer_id, '^^'))), 256)

-- Split satellites by rate of change.                        // ✅
-- Stable details vs volatile loyalty points.

-- Exclude volatile fields from hash_diff.                    // ✅
-- last_synced_at belongs in its own satellite or nowhere.

-- Build PIT tables reactively.                               // ✅
-- Only when temporal queries are slow.

-- Keep hubs business-key-only.                               // ✅
-- No descriptive attributes in hubs.

❌ Don’t Do This:

-- Compute hash with inconsistent normalization.              // ❌
SHA2(customer_id, 256)  -- No trim, no case, no null handling.

-- Put descriptive attributes in a hub.                       // ❌
-- Violates the model; breaks source isolation.

-- Include last_synced_at in hash_diff.                       // ❌
-- Every load inserts a new row; unbounded growth.

-- Query raw satellites directly for current state.           // ❌
-- Returns multiple rows per key; use PIT or mart.

-- Build PIT tables upfront for every entity.                 // ❌
-- Wastes storage; add when needed.

Common Pitfalls

PitfallWhy It HappensFix
Duplicate hub rowsInconsistent business key normalizationShared versioned normalization function
Satellite grows unboundedVolatile field in hash_diffSplit volatile fields; exclude from hash
Multiple rows per key in queriesQuerying raw vault without PITBuild PIT table or current-state view
Hash keys differ across platformsDifferent hashing input encodingDocument and version the normalization
New source breaks modelHub modeled around source schemaModel hubs around business concepts

Real-World Examples

1. Hub Table

CREATE TABLE hub_product (
    product_hk   VARBINARY(32) PRIMARY KEY,
    product_bk   VARCHAR(50) NOT NULL,
    load_date    TIMESTAMP NOT NULL,
    record_source VARCHAR(100) NOT NULL
);

2. Link Table

CREATE TABLE link_order_product (
    order_product_hk VARBINARY(32) PRIMARY KEY,
    order_hk         VARBINARY(32) NOT NULL,
    product_hk       VARBINARY(32) NOT NULL,
    load_date        TIMESTAMP NOT NULL,
    record_source    VARCHAR(100) NOT NULL
);

3. Satellite Table

CREATE TABLE sat_product_pricing (
    product_hk   VARBINARY(32) NOT NULL,
    load_date    TIMESTAMP NOT NULL,
    hash_diff    VARBINARY(32) NOT NULL,
    price        DECIMAL(10,2),
    currency     CHAR(3),
    PRIMARY KEY (product_hk, load_date)
);

4. Hash Key with Normalization

SHA2(UPPER(TRIM(COALESCE(order_id, '^^'))), 256)

5. Link Hash Key

SHA2(CONCAT(customer_hk, '|', order_hk), 256)

6. Hash Diff

SHA2(CONCAT(COALESCE(price,''), '|', COALESCE(currency,'')), 256)

7. Current-State Query

SELECT * FROM sat_product_pricing s
WHERE s.load_date = (SELECT MAX(load_date) FROM sat_product_pricing WHERE product_hk = s.product_hk);

8. PIT Table

CREATE TABLE pit_product_daily (
    product_hk       VARBINARY(32) NOT NULL,
    snapshot_date    DATE NOT NULL,
    pricing_load_date TIMESTAMP,
    PRIMARY KEY (product_hk, snapshot_date)
);

9. Satellite Split by Source

-- sat_customer_details_crm, sat_customer_details_erp

10. Insert-Only Load

INSERT INTO hub_customer SELECT ... WHERE NOT EXISTS (SELECT 1 FROM hub_customer WHERE customer_hk = ...);

Visual: The Three Table Types

┌──────────────────────────────────────────────┐
│  HUB_CUSTOMER                                │
│  customer_hk (PK) | customer_bk | load_date  │
│  │                                           │
│  │  Links connect hubs.                      │
│  ▼                                           │
│  LINK_CUSTOMER_ORDER                         │
│  customer_order_hk | customer_hk | order_hk  │
│  │                                           │
│  │  Satellites describe hubs or links.       │
│  ▼                                           │
│  SAT_CUSTOMER_DETAILS                        │
│  customer_hk | load_date | hash_diff | name  │
│                                              │
│  Hubs: identity only.                        │
│  Links: relationships only.                  │
│  Satellites: attributes + history.           │
│                                              │
└──────────────────────────────────────────────┘

Hash Key Determinism

┌──────────────────────────────────────────────┐
│  HASH KEY GENERATION                         │
│                                              │
│  Loader A: 'CUST-001'                        │
│    → UPPER(TRIM(...)) → 'CUST-001'           │
│    → SHA-256 → hash X                        │
│                                              │
│  Loader B: 'cust-001 '                       │
│    → UPPER(TRIM(...)) → 'CUST-001'           │
│    → SHA-256 → hash X                        │
│                                              │
│  Same hash. No duplicate hub row.            │
│                                              │
│  If normalization differs:                   │
│    Loader A → hash X                         │
│    Loader B → hash Y                         │
│    → Duplicate business entity in hub.       │
│                                              │
└──────────────────────────────────────────────┘

Satellite Splitting

┌──────────────────────────────────────────────┐
│  ONE SATELLITE PER HUB (BAD)                 │
│                                              │
│  sat_customer: name, email, phone,           │
│                loyalty_points, last_synced   │
│                                              │
│  Daily loyalty change → new row duplicating  │
│  name, email, phone. Unbounded growth.       │
│                                              │
│  SPLIT SATELLITES (GOOD)                     │
│                                              │
│  sat_customer_details: name, email, phone    │
│  sat_customer_loyalty: loyalty_points        │
│  sat_customer_sync: last_synced              │
│                                              │
│  Stable attributes copied only when they     │
│  change. Volatile attributes isolated.       │
│                                              │
└──────────────────────────────────────────────┘

PIT Table Purpose

┌──────────────────────────────────────────────┐
│  POINT-IN-TIME TABLE                         │
│                                              │
│  Raw vault query for current state:          │
│    Join hub to 3 satellites.                 │
│    Each satellite: filter MAX(load_date).    │
│    Slow at scale.                            │
│                                              │
│  PIT table (pre-computed nightly):           │
│    customer_hk | snapshot_date |              │
│    details_load_date | loyalty_load_date     │
│                                              │
│  Query: join hub → PIT → satellites          │
│    on the pre-computed load dates.           │
│    Fast.                                     │
│                                              │
│  Build PIT reactively, not upfront.          │
│                                              │
└──────────────────────────────────────────────┘

Summary

ItemValue
HubBusiness key, hash key, load date, record source
LinkRelationship hash key, parent hub keys, load date, record source
SatelliteParent hash key, load date, hash diff, payload
Hash keyDeterministic from normalized business key
NormalizationTrim, case, null sentinel, strong delimiter
Hash diffPayload fingerprint; skip unchanged rows
Satellite splitBy source, rate of change, sensitivity
Raw vaultInsert-only, no current state
PIT tablePre-computed load dates for current-state queries
Consumption layerStar schema mart built from vault

Key takeaways:

  • Data Vault separates identity, relationship, and history. Hubs hold business keys, links hold relationships between hubs, and satellites hold descriptive attributes with full history. No table mixes these concerns .
  • Hash keys replace sequential surrogate keys. The hash is computed deterministically from the normalized business key, enabling parallel loading without a central sequence generator. Inconsistent normalization produces duplicate hub rows .
  • Satellites must be split. By source system to isolate schema changes, by rate of change to prevent stable attributes from being copied on every volatile update, and by sensitivity to scope GDPR erasure. A single wide satellite grows unboundedly and slows every query .
  • The hash diff is the change detection mechanism. It fingerprints the payload. A matching hash skips the row; a different hash inserts a new version. Volatile technical fields must be excluded, or every load inserts a row even when business data is unchanged .
  • The raw vault has no current state. Every query that needs “the latest value” must filter to the most recent load date per key, per satellite. PIT tables pre-compute these dates and should be added when query performance justifies them, not upfront .
  • Data Vault is an integration layer, not a consumption layer. BI tools and analysts query a star schema mart built from the vault. Querying the raw vault directly produces multiple rows per key and the conclusion that the model is too complex .

Remember: Data Vault 2.0 is three table types with strict column contracts. Hubs store business keys and nothing else. Links store relationships and nothing else. Satellites store descriptive attributes and their full history. Every table is keyed by a deterministic hash computed from normalized business keys, so independent loaders can write in parallel without coordination. Every table is insert-only; nothing is updated or deleted. The raw vault preserves everything but answers nothing directly—it has no concept of current state, and every query that needs the latest value must either filter to the maximum load date per satellite or join through a point-in-time table. The vault is the integration layer; the star schema is the consumption layer. Building the vault without the mart is building a warehouse that no one can query.

Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!