portability
| Subpath | @noy-db/hub/portability |
| Factory | withPortability() |
| Layer | with-audit |
withPortability() opt-in. This page covers export — taking a non-destructive portable copy of your data. To remove your data (one-party self-serve, or a two-party request → approve ceremony), see withdrawal.What it does
A noy-db vault is an encrypted document store that several people can share: each
member unlocks it with their own secret-protected keyring, which holds a role
(owner, admin, operator, client, viewer, custodian) and the per-collection
data-encryption keys (DEKs) that role was granted (see team).
The withPortability() service adds a data-sovereignty surface to vault.user.*: a
non-owner can take a portable copy of their own accessible data (this page), or
withdraw it entirely (the withdrawal page). "Their own" is
enforced cryptographically, not by a runtime allowlist — the API operates only on
collections whose DEKs the caller's keyring holds, so a record outside the caller's keys
is undecryptable and can never enter the export.
Export — exportMyAccessibleData() — is the non-destructive half: it always
succeeds (there is no policy gate a firm could use to deny a member a copy of data they
can already decrypt) and is audited.
When you need it
- GDPR-style data portability (Art. 20) for the clients of a firm-run vault — a member takes a re-keyed, independently-openable copy of their records with them.
- Personal backups: a member snapshots the data they can decrypt, under their own secret, without touching the live vault.
- The first, always-safe step of an offboarding flow — the destructive follow-through (erase / hand-over) lives on withdrawal.
Prerequisites
- None for the read-only export surface —
withPortability()alone enablesvault.user.*. - In a multi-user vault:
teamStrategy: withTeam()provides the roles and grants the export scoping reads. - For destructive self-serve withdrawal: additional gates apply — see withdrawal.
Opt in
Pass portabilityStrategy: withPortability() to createNoydb(). Without it, every
vault.user.* portability method (export and withdrawal) throws
PortabilityNotEnabledError, and the export/withdraw/request engines stay out of your
bundle (they are loaded on demand behind the opt-in).
import { createNoydb } from '@noy-db/hub'
import { withTeam } from '@noy-db/hub/team'
import { withPortability } from '@noy-db/hub/portability'
const db = await createNoydb({
user: 'client1',
secret: 'client-pw-long-enough',
teamStrategy: withTeam(), // multi-user vault: roles + grants
portabilityStrategy: withPortability(), // enables vault.user.* export + withdrawal
})
const vault = await db.openVault('acme')
This one opt-in enables both halves of the service. The multi-user scenarios assume a
vault provisioned with withTeam(), where the firm (owner) has granted the
caller a keyring.
Export a portable copy
Non-destructive and always allowed — there is no policy gate a firm could use to deny a member a copy of the data they can already decrypt. The call is audited.
const bytes = await vault.user.exportMyAccessibleData({
reKey: { secret: 'my-portable-copy-secret' },
scope: { collections: ['invoices'] }, // optional narrowing
})
// → Uint8Array — a standard `.noydb` bundle, independently openable
// with the reKey secret (see the bundle service)
- Omit
reKeyto keep the bundle under the source keyring (a personal backup). - Omit
scopeto export everything the caller can decrypt.
Who can do what
Export is available to every role, always scoped to what the caller can decrypt:
| Role | exportMyAccessibleData scope |
|---|---|
owner / admin | all collections |
operator | granted collections |
client | granted collections |
viewer | all collections |
custodian | all collections |
Export is non-destructive, so there is no role that is denied it — a member exporting a copy of data they can already read reveals nothing new. The destructive side (who may delete vs. who must request) is governed separately on withdrawal → Who can do what.
API reference
The @noy-db/hub/portability subpath exports withPortability(), the
PortabilityStrategy type, the NO_PORTABILITY no-op default,
PortabilityNotEnabledError, and every option/result type across both halves of the
service.
vault.user.exportMyAccessibleData(opts?)
Re-keyed, access-scoped .noydb bundle of the caller's accessible data.
| Option | Type | Default | Meaning |
|---|---|---|---|
reKey | { secret: string } | inherit source keyring | Make the bundle independently openable by a new owner |
scope | { collections?: string[] } | all accessible | Narrow to a subset of the caller's collections |
compression | 'auto' | 'brotli' | 'gzip' | 'none' | 'auto' | Bundle compression |
The five withdrawal methods (unilateralWithdrawal, requestWithdrawal,
listWithdrawalRequests, approveWithdrawal, rejectWithdrawal) are documented on
withdrawal → API reference.
Errors
| Error | Thrown when |
|---|---|
PortabilityNotEnabledError | Any vault.user.* method called without portabilityStrategy: withPortability() |
Export has no other failure modes — it cannot be policy-denied. The withdrawal-specific
errors (ReadOnlyError, PolicyDeniedError, WithdrawalRequestError) are covered on
withdrawal → Errors.
Audit
An export appends an op: 'lifecycle' entry to the tamper-evident ledger when the vault
has history enabled (without it, auditing is a silent no-op). The entry
participates in the ledger's hash chain but carries no data payload — only a structured
reason:
| Operation | reason |
|---|---|
| Export | user-export:<userId> |
The withdrawal operations write their own ledger entries — see withdrawal → Audit.
Limits & gotchas
- Export is scoped to the caller's keys: a record whose DEK the caller's keyring does not hold is undecryptable and never enters the bundle. There is no way to export beyond your own access.
- Audit entries are only written when a history/ledger strategy is configured; enable
withHistory()for the compliance trail.
See also
- withdrawal — the destructive other half of
withPortability(): one-party and two-party removal, source dispositions, and the policy gates. - bundle — the
.noydbcontainer format;readPod()to open an exported bundle. - team — roles, grants, and per-collection permissions.
- history — the tamper-evident ledger the audit entry lands in.
- Proven by showcase
120-client-portability-withdrawal.