API · Governance
Data governance in one plugin with @voltro/plugin-governance — AES-256-GCM field encryption for .encrypted() columns (handler sees plaintext, ciphertext at rest), admin-gated GDPR governance.export/erase over declared subjectScopes, a consent ledger, and retention TTL sweeps. Memory store, zero infra.
Four compliance primitives, one plugin. @voltro/plugin-governance gives you field encryption (AES-256-GCM for .encrypted() columns — handlers see plaintext, the column holds enc:v1:… at rest), GDPR export/erase (admin-gated, walking declared subject scopes), a consent ledger, and retention sweeps (delete/anonymise rows past a TTL). The template boots zero-infra with a dev encryption key. Template id: api-governance.
Scaffold
voltro create-project acme --api=api-governanceThe shipped .env carries a DEV VOLTRO_FIELD_ENCRYPTION_KEY. Generate your own (openssl rand -hex 32) and keep it in a secret store for production.
Field encryption
// database/schema.ts
export const profiles = table('profiles', {
id: id(), name: text(), email: text(),
ssn: text().encrypted(), // AES-256-GCM at rest
}).with(audit())
// app.config.ts
governancePlugin({ fieldEncryption: true /* reads VOLTRO_FIELD_ENCRYPTION_KEY */ })You pass plaintext; the store middleware encrypts on write and decrypts on read — handlers never touch the ciphertext. Boot fails loud if an .encrypted() column exists but no cipher is registered.
ID=$(curl -s localhost:4000/_voltro/inspect/invoke -H 'content-type: application/json' \
-d '{"tag":"profiles.create","input":{"name":"Ada","email":"ada@acme.com","ssn":"123-45-6789"}}' | python3 -c 'import sys,json;print(json.load(sys.stdin)["result"]["id"])')
curl -s localhost:4000/_voltro/inspect/invoke -H 'content-type: application/json' \
-d "{\"tag\":\"profiles.get\",\"input\":{\"id\":\"$ID\"}}"
# → { ok:true, result:{ …, ssn:"123-45-6789" } } (plaintext — the column stored enc:v1:…)GDPR, consent, retention
governancePlugin({
fieldEncryption: true,
subjectScopes: [{ table: 'profiles', subjectField: 'id' }], // GDPR walks these
retention: [{ table: 'profiles', ttlMs: 365 * 86_400_000, action: 'delete' }],
})- GDPR —
governance.export/governance.eraseare admin-gated (call as a subject withadmin:full— seeapi-rbac); they walksubjectScopesto bundle or erase everything belonging to a subject.GovernanceServiceexposes the same in-handler. - Consent —
governance.consent(a mutation) records a grant;governance.hasConsent(a reactive query) checks it — subscribe to it on the web, or callGovernanceService.hasConsent(...)in a handler. - Retention — the sweep deletes (or
anonymizes) rows whose age pastdateField(defaultcreatedAt) exceedsttlMs.
Rules
- Don't encrypt what you filter on. An
.encrypted()column is ciphertext on disk — noWHERE/ORDER BYin SQL. Encrypt fields you read back WHOLE (PII, tokens, notes). - The key is everything. GCM fails closed on a bad key — you get an error, never silent corruption. Back the key up; rotating it means re-encrypting.
- GDPR endpoints are admin-only by design. Don't remove the
admin:fullgate — an unauthenticated export/erase is a data-exfiltration / griefing hole.