{
  "slug": "persist/encryption",
  "title": "End-to-End Encryption",
  "description": "Enable P2P encryption and zero-knowledge data protection in Persist stores.",
  "url": "https://cuitty.com/docs/persist/encryption",
  "markdown_url": "https://cuitty.com/docs/persist/encryption.md",
  "json_url": "https://cuitty.com/docs/persist/encryption.json",
  "frontmatter": {
    "title": "End-to-End Encryption",
    "description": "Enable P2P encryption and zero-knowledge data protection in Persist stores.",
    "order": 4,
    "section": "Persist",
    "updatedAt": "2026-05-12"
  },
  "headings": [
    {
      "depth": 1,
      "slug": "end-to-end-encryption",
      "text": "End-to-End Encryption"
    },
    {
      "depth": 2,
      "slug": "enable-encryption",
      "text": "Enable encryption"
    },
    {
      "depth": 2,
      "slug": "how-it-works",
      "text": "How it works"
    },
    {
      "depth": 2,
      "slug": "key-management",
      "text": "Key management"
    },
    {
      "depth": 3,
      "slug": "device-keys",
      "text": "Device keys"
    },
    {
      "depth": 3,
      "slug": "key-rotation",
      "text": "Key rotation"
    },
    {
      "depth": 2,
      "slug": "encrypted-sync",
      "text": "Encrypted sync"
    },
    {
      "depth": 3,
      "slug": "sharing-keys-between-peers",
      "text": "Sharing keys between peers"
    },
    {
      "depth": 2,
      "slug": "zero-knowledge-architecture",
      "text": "Zero-knowledge architecture"
    }
  ],
  "body_markdown": "# End-to-End Encryption\n\n## Enable encryption\n\n```ts\nimport { createStore } from \"@cuitty/persist\";\n\nconst store = await createStore({\n  name: \"my-app\",\n  adapter: \"sqlite\",\n  path: \"./data/my-app.db\",\n  encrypt: true,\n});\n```\n\nWhen `encrypt: true` is set, all data is encrypted on the device before it is written to disk or sent over the network. The sync server, storage backend, and any intermediary never see plaintext.\n\n## How it works\n\nPersist generates a 256-bit device key on first run and stores it in the OS keychain (or a local keyfile as fallback). Every record is encrypted with AES-256-GCM using a per-record nonce derived from the key and the record's path.\n\n```\nplaintext --> AES-256-GCM encrypt --> ciphertext --> store / sync\n```\n\nReads reverse the process transparently. Application code works with plain objects -- encryption and decryption are invisible to the caller.\n\n## Key management\n\n### Device keys\n\nEach device generates its own key on first use. The key never leaves the device unless explicitly exported.\n\n```ts\n// Export the device key (for backup or migration)\nconst exportedKey = await store.crypto.exportKey();\n\n// Import a key on a new device\nconst store = await createStore({\n  name: \"my-app\",\n  adapter: \"sqlite\",\n  path: \"./data/my-app.db\",\n  encrypt: true,\n  key: exportedKey,\n});\n```\n\n### Key rotation\n\nRotate the encryption key without downtime. Persist re-encrypts existing records in the background.\n\n```ts\nawait store.crypto.rotateKey();\n```\n\nAfter rotation the old key is kept in a sealed keyring so previously-synced peers can still decrypt historical data until they receive the new key.\n\n## Encrypted sync\n\nWhen encryption and sync are both enabled, peers exchange ciphertext. Decryption happens only on the receiving device using a shared key.\n\n```ts\nconst store = await createStore({\n  name: \"my-app\",\n  adapter: \"sqlite\",\n  path: \"./data/my-app.db\",\n  encrypt: true,\n  sync: {\n    remote: \"postgres\",\n    strategy: \"last-write-wins\",\n  },\n});\n```\n\n### Sharing keys between peers\n\nPeers use a Diffie-Hellman key exchange to establish a shared secret. No key material is transmitted in the clear.\n\n```ts\n// On device A: generate an invite\nconst invite = await store.crypto.createInvite();\n// --> send `invite.code` to device B out-of-band\n\n// On device B: accept the invite\nawait store.crypto.acceptInvite(invite.code);\n```\n\nOnce paired, both devices derive the same data key and can decrypt each other's records.\n\n## Zero-knowledge architecture\n\nThe sync server stores only ciphertext and opaque metadata (record size, sync timestamps). It cannot:\n\n- Read record contents\n- Identify record types or schemas\n- Correlate records across stores (keys are encrypted too)\n\nEven if the server is compromised, an attacker gains no usable data without the device keys.",
  "body_html": "<h1 id=\"end-to-end-encryption\">End-to-End Encryption</h1>\n<h2 id=\"enable-encryption\">Enable encryption</h2>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"ts\"><code><span class=\"line\"><span style=\"color:#F97583\">import</span><span style=\"color:#E1E4E8\"> { createStore } </span><span style=\"color:#F97583\">from</span><span style=\"color:#9ECBFF\"> \"@cuitty/persist\"</span><span style=\"color:#E1E4E8\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\">const</span><span style=\"color:#79B8FF\"> store</span><span style=\"color:#F97583\"> =</span><span style=\"color:#F97583\"> await</span><span style=\"color:#B392F0\"> createStore</span><span style=\"color:#E1E4E8\">({</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">  name: </span><span style=\"color:#9ECBFF\">\"my-app\"</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">  adapter: </span><span style=\"color:#9ECBFF\">\"sqlite\"</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">  path: </span><span style=\"color:#9ECBFF\">\"./data/my-app.db\"</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">  encrypt: </span><span style=\"color:#79B8FF\">true</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">});</span></span></code></pre>\n<p>When <code>encrypt: true</code> is set, all data is encrypted on the device before it is written to disk or sent over the network. The sync server, storage backend, and any intermediary never see plaintext.</p>\n<h2 id=\"how-it-works\">How it works</h2>\n<p>Persist generates a 256-bit device key on first run and stores it in the OS keychain (or a local keyfile as fallback). Every record is encrypted with AES-256-GCM using a per-record nonce derived from the key and the record’s path.</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"plaintext\"><code><span class=\"line\"><span>plaintext --> AES-256-GCM encrypt --> ciphertext --> store / sync</span></span></code></pre>\n<p>Reads reverse the process transparently. Application code works with plain objects — encryption and decryption are invisible to the caller.</p>\n<h2 id=\"key-management\">Key management</h2>\n<h3 id=\"device-keys\">Device keys</h3>\n<p>Each device generates its own key on first use. The key never leaves the device unless explicitly exported.</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"ts\"><code><span class=\"line\"><span style=\"color:#6A737D\">// Export the device key (for backup or migration)</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">const</span><span style=\"color:#79B8FF\"> exportedKey</span><span style=\"color:#F97583\"> =</span><span style=\"color:#F97583\"> await</span><span style=\"color:#E1E4E8\"> store.crypto.</span><span style=\"color:#B392F0\">exportKey</span><span style=\"color:#E1E4E8\">();</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D\">// Import a key on a new device</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">const</span><span style=\"color:#79B8FF\"> store</span><span style=\"color:#F97583\"> =</span><span style=\"color:#F97583\"> await</span><span style=\"color:#B392F0\"> createStore</span><span style=\"color:#E1E4E8\">({</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">  name: </span><span style=\"color:#9ECBFF\">\"my-app\"</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">  adapter: </span><span style=\"color:#9ECBFF\">\"sqlite\"</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">  path: </span><span style=\"color:#9ECBFF\">\"./data/my-app.db\"</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">  encrypt: </span><span style=\"color:#79B8FF\">true</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">  key: exportedKey,</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">});</span></span></code></pre>\n<h3 id=\"key-rotation\">Key rotation</h3>\n<p>Rotate the encryption key without downtime. Persist re-encrypts existing records in the background.</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"ts\"><code><span class=\"line\"><span style=\"color:#F97583\">await</span><span style=\"color:#E1E4E8\"> store.crypto.</span><span style=\"color:#B392F0\">rotateKey</span><span style=\"color:#E1E4E8\">();</span></span></code></pre>\n<p>After rotation the old key is kept in a sealed keyring so previously-synced peers can still decrypt historical data until they receive the new key.</p>\n<h2 id=\"encrypted-sync\">Encrypted sync</h2>\n<p>When encryption and sync are both enabled, peers exchange ciphertext. Decryption happens only on the receiving device using a shared key.</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"ts\"><code><span class=\"line\"><span style=\"color:#F97583\">const</span><span style=\"color:#79B8FF\"> store</span><span style=\"color:#F97583\"> =</span><span style=\"color:#F97583\"> await</span><span style=\"color:#B392F0\"> createStore</span><span style=\"color:#E1E4E8\">({</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">  name: </span><span style=\"color:#9ECBFF\">\"my-app\"</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">  adapter: </span><span style=\"color:#9ECBFF\">\"sqlite\"</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">  path: </span><span style=\"color:#9ECBFF\">\"./data/my-app.db\"</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">  encrypt: </span><span style=\"color:#79B8FF\">true</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">  sync: {</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    remote: </span><span style=\"color:#9ECBFF\">\"postgres\"</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    strategy: </span><span style=\"color:#9ECBFF\">\"last-write-wins\"</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">  },</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">});</span></span></code></pre>\n<h3 id=\"sharing-keys-between-peers\">Sharing keys between peers</h3>\n<p>Peers use a Diffie-Hellman key exchange to establish a shared secret. No key material is transmitted in the clear.</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"ts\"><code><span class=\"line\"><span style=\"color:#6A737D\">// On device A: generate an invite</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">const</span><span style=\"color:#79B8FF\"> invite</span><span style=\"color:#F97583\"> =</span><span style=\"color:#F97583\"> await</span><span style=\"color:#E1E4E8\"> store.crypto.</span><span style=\"color:#B392F0\">createInvite</span><span style=\"color:#E1E4E8\">();</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\">// --> send `invite.code` to device B out-of-band</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D\">// On device B: accept the invite</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">await</span><span style=\"color:#E1E4E8\"> store.crypto.</span><span style=\"color:#B392F0\">acceptInvite</span><span style=\"color:#E1E4E8\">(invite.code);</span></span></code></pre>\n<p>Once paired, both devices derive the same data key and can decrypt each other’s records.</p>\n<h2 id=\"zero-knowledge-architecture\">Zero-knowledge architecture</h2>\n<p>The sync server stores only ciphertext and opaque metadata (record size, sync timestamps). It cannot:</p>\n<ul>\n<li>Read record contents</li>\n<li>Identify record types or schemas</li>\n<li>Correlate records across stores (keys are encrypted too)</li>\n</ul>\n<p>Even if the server is compromised, an attacker gains no usable data without the device keys.</p>",
  "links_out": []
}