stable

Export & portability

Get data out, three ways: capability-gated CSV/XLSX exports, the sealed .noydb bundle, and a non-owner's re-keyed portable copy.

Audience: anyone answering "how do I get my data out?" — spreadsheet handoffs, encrypted backups for a recipient, or a member leaving with their slice. Bundle: core + withTeam() (grants carry the export capability) + withPortability() (the member-copy path) + the as-* format packages you need.

Verified by: showcases/src/recipe-export-portability.recipe.test.ts — the code below is the same code that runs there.

Three different exit doors, for three different trust situations: plaintext formats (CSV/XLSX) when the destination is a spreadsheet and the leak surface is accepted; the encrypted .noydb bundle when the data must travel sealed; and the portable member copy when a non-owner takes exactly what their keyring can decrypt, re-keyed under their own secret. What makes the first door safe to have at all is the gate in front of it.

Prerequisites

  • Packages: pnpm add @noy-db/hub @noy-db/as-csv @noy-db/as-xlsx @noy-db/as-noydb
  • teamStrategy: withTeam() — the plaintext-export capability is granted per keyring, per format; without the team engine there is nothing to carry the grant.
  • portabilityStrategy: withPortability() on the member's instance for the portable-copy path.
  • An exportCapability grant for any plaintext format — even the owner has no implicit one (see below).

The gate: plaintext export is a granted capability

Decrypted data leaving the vault is the one thing zero-knowledge can't protect after the fact — so it's refused by default, for everyone:

import { createNoydb, ExportCapabilityError } from '@noy-db/hub'
import { withTeam } from '@noy-db/hub/team'
import { withFormats } from '@noy-db/hub/as'
import { asCsv } from '@noy-db/as-csv'
import { toMemory } from '@noy-db/to-memory'

const db = await createNoydb({ store: toMemory(), user: 'alice', secret: 'export-recipe-pass-2026', teamStrategy: withTeam(), formatsStrategy: withFormats() })
const vault = await db.openVault('firm')
await vault.collection('invoices').put('a', { id: 'a', client: 'Globex', amount: 1500 })

await vault.export(asCsv(), { collections: ['invoices'] })
// → rejects: ExportCapabilityError — even the owner has no implicit grant
// (`collections` scopes the artefact; what it costs to produce depends on your
//  hub version — see "What `collections` costs" on the as-* family page.)

Grant the capability — per format — and re-open:

await db.grant('firm', {
  userId: 'alice', displayName: 'Alice', role: 'owner', secret: 'export-recipe-pass-2026',
  exportCapability: { plaintext: ['csv', 'xlsx'] },
})

Spreadsheet formats — CSV and XLSX

import { asCsv } from '@noy-db/as-csv'
import { toBytes as toXlsx } from '@noy-db/as-xlsx'

const csv = await vault.export(asCsv(), { collections: ['invoices'] })
// id,client,amount
// a,Globex,1500
// b,"Acme, Inc.",2400        ← RFC-4180 quoting, ready for any spreadsheet
// NB: through hub 0.8.0 `collections` filters the OUTPUT only — every collection
// is still read and decrypted. The next release narrows the read too. See
// "What `collections` costs" on the as-* family page.

const xlsx = await toXlsx(vault, {
  sheets: [{ name: 'Invoices', collection: 'invoices' }],
})
// a real Office Open XML workbook — one sheet per collection

The sealed door — the .noydb bundle

When the data must travel encrypted, export the whole vault as a single .noydb file. Anyone holding the file learns only its minimum-disclosure header:

import { toBytes as toNoydb, peek } from '@noy-db/as-noydb'

const bytes = await toNoydb(vault)
const header = peek(bytes) // no secret — this is ALL the file discloses
// → { formatVersion, handle, bodyBytes, bodySha256 }

The noydb CLI's inspect / verify work on exactly this header — integrity checks with no key. Re-keyed export to a recipient and whole-file pod stores are covered on the bundle page.

The member's door — a portable, re-keyed copy

A non-owner takes a .noydb copy of precisely what their keyring can decrypt — nothing more — re-keyed under a secret they choose. Non-destructive, and by design always allowed:

import { withPortability } from '@noy-db/hub/portability'

const op = await createNoydb({
  store, user: 'belle', secret: 'belle-operator-2026',
  portabilityStrategy: withPortability(),
})
const vault = await op.openVault('firm')

const bytes = await vault.user.exportMyAccessibleData({
  reKey: { secret: 'belle-takeaway-pass' },
})
// belle's scope, sealed under belle's new secret; the firm vault is untouched

The removal-flavored variants — export-and-erase, export-and-freeze — are the withdrawal ceremony built on the same surface.

Verify

Run the paired walkthrough: the gate's refusal, the RFC-4180 quoting, the PK magic of a real workbook, the header's non-disclosure (peek() output contains no record content), and the copy's non-destructiveness are each asserted.

See also

  • as-* family — all nine formats, including JSON/NDJSON/XML/SQL and the ZIP composite.
  • portability · withdrawal — the data-sovereignty pair.
  • bundle — the .noydb container format in depth.