Export & portability
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) + theas-*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
exportCapabilitygrant 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
.noydbcontainer format in depth.