30817:nip-xx-encrypted-workspace

NIP-XX: Encrypted Workspace

Jared Logan

published
2026-03-20

A protocol for private, encrypted productivity workspaces synced via Nostr relays. All content is end-to-end encrypted with NIP-44 before leaving the client — relay operators see only opaque ciphertext.

Kinds 30800, 30801, and 30802 were defined by NIP-XX: Encrypted File Sync. This document extends that foundation with encrypted structured data via kind 30078, a formal vault index manifest format with delete/restore/orphan semantics, and a defined security model.

Summary

Kind Name Type Purpose
30800 File Content Addressable Encrypted document body
30801 Vault Index Addressable Encrypted workspace manifest
30802 Shared Document Addressable Document encrypted to a recipient
30078 Structured Data Addressable Encrypted database rows, tasks, preferences

Kind 30800 — File Content

Stores the encrypted content of a single document. The content field is always a NIP-44 ciphertext — never plaintext.

Tags:

Tag Required Description
d Yes Stable UUID-style file identifier (e.g. doc-m3xk7f2)
encrypted Yes Always "nip44"
alt Yes NIP-31 fallback: "Encrypted note - NIP-44"

Decrypted payload:

{
  "title": "Meeting Notes",
  "path": "/notes/meeting-notes.md",
  "content": "# Meeting Notes\n\nDiscussion points...",
  "modified": 1742483921,
  "version": 5,
  "checksum": "a3f1",
  "contentType": "text/markdown"
}
Field Type Required Description
title string Yes Human-readable document title
path string Yes Virtual filesystem path (e.g. /notes/meeting-notes.md)
content string Yes Document body (typically Markdown)
modified number Yes Unix timestamp of last modification
version number No Incrementing version counter
checksum string No Short content integrity checksum
contentType string No MIME type, defaults to text/markdown

Encryption:

event.content = nip44.encrypt(authorPubkey, JSON.stringify(payload))

Self-encryption — the conversation key is derived from the author's own keypair. Only the author can decrypt their own documents.


Kind 30801 — Vault Index

The authoritative manifest of all documents in a workspace. One event per user per vault. Clients treat this as the source of truth for workspace state.

  • A file is active if it appears in files[]
  • A file is deleted if it appears in deleted[]
  • A file is orphaned if it exists on the relay but in neither array (recoverable via relay scan)

Tags:

Tag Required Description
d Yes Vault identifier (e.g. "default-vault")
encrypted Yes Always "nip44"
alt Yes NIP-31 fallback: "Encrypted vault index - NIP-44"

Decrypted payload:

{
  "name": "My Workspace",
  "description": "Private encrypted documents synced via Nostr",
  "files": [
    {
      "d": "doc-m3xk7f2",
      "path": "/notes/meeting-notes.md",
      "title": "Meeting Notes",
      "modified": 1742483921,
      "eventId": "abcdef1234..."
    }
  ],
  "deleted": [
    {
      "d": "doc-a1b2c3",
      "path": "/notes/old-draft.md",
      "title": "Old Draft",
      "deletedAt": 1742480000,
      "lastEventId": "abcdef5678..."
    }
  ],
  "updated": 1742483921
}

files[] entry:

Field Type Required Description
d string Yes Matches the d tag of the corresponding kind 30800 event
path string Yes Virtual filesystem path
title string Yes Display title
modified number Yes Unix timestamp of last modification
eventId string No Event ID of the most recent kind 30800 event

deleted[] entry (tombstone):

Field Type Required Description
d string Yes Matches the d tag of the deleted kind 30800 event
path string Yes Last known path before deletion
title string Yes Last known title before deletion
deletedAt number Yes Unix timestamp of deletion
lastEventId string No Event ID of the final kind 30800 version

Top-level fields:

Field Type Required Description
name string No Workspace display name
description string No Workspace description
files array Yes Active file references
deleted array No Tombstones for deleted files
updated number Yes Unix timestamp of last index update
settings object No Workspace-level client settings

Delete / Restore semantics:

  • Delete — remove the entry from files[], add a tombstone to deleted[]. The underlying kind 30800 event on the relay is never modified (Nostr is append-only).
  • Restore — remove the tombstone from deleted[], re-add the entry to files[].
  • Orphan recovery — kind 30800 events that exist on the relay but appear in neither array can be discovered by scanning and re-imported into the index.

Kind 30802 — Shared Document

A document shared from one user to another. Uses the same decrypted payload schema as kind 30800, but the NIP-44 conversation key is derived from the author's private key and the recipient's public key rather than self-encryption.

Tags:

Tag Required Description
d Yes Stable document identifier
p Yes Recipient's pubkey
encrypted Yes Always "nip44"
alt Yes NIP-31 fallback: "Encrypted shared document - NIP-44"

Encryption:

event.content = nip44.encrypt(recipientPubkey, JSON.stringify(payload))

Kind 30078 — Structured Data (NIP-78)

Encrypted structured data such as database rows, tasks, kanban cards, and application preferences. Reuses the NIP-78 kind with t tags for namespacing and relay-level filtering.

Tags:

Tag Required Description
d Yes Entry identifier (e.g. task-m3xk7f2)
t Yes Entry type: "task", "preference", "row", etc.
t No Status slug: "todo", "in-progress", "done"
encrypted Yes Always "nip44"
alt Yes NIP-31 fallback: "Encrypted structured data - NIP-44"

Example decrypted task payload:

{
  "id": "task-m3xk7f2",
  "type": "task",
  "task": "Write release notes",
  "status": "in-progress",
  "assignee": "npub1...",
  "due": "2026-04-01",
  "modified": 1742483921
}

Encryption Model

All private content uses NIP-44 self-encryption unless otherwise specified:

event.content = nip44.encrypt(authorPubkey, JSON.stringify(payload))

The conversation key is derived from the author's own keypair. Only the author can decrypt their own data. Relay operators, other users, and network observers see only ciphertext.

Kind 30802 (shared documents) uses the recipient's pubkey instead:

event.content = nip44.encrypt(recipientPubkey, JSON.stringify(payload))

Key properties:

  • No plaintext content is ever stored in event fields visible to relays
  • Tags contain only UUID-style identifiers, the "nip44" signal, and generic NIP-31 descriptions — never user content
  • Works with any NIP-07 compatible signer; the private key is never exposed to the client application

Security Requirements

Author filtering

All queries for private workspace data MUST include authors: [userPubkey]. Nostr is permissionless — without this filter, any actor can publish events with these kind numbers and inject content into a workspace.

// ✅ Correct
nostr.query([{
  kinds: [30800, 30801],
  authors: [user.pubkey],
}]);

// ❌ Unsafe — accepts events from any publisher
nostr.query([{
  kinds: [30800, 30801],
}]);

No sensitive data in tags

Tags must never contain document titles, file paths, or any user-generated content. Only UUID-style d identifiers, the "nip44" encryption signal, recipient pubkeys (p), category t tags, and NIP-31 alt descriptions are permitted in plaintext tags.


Standard NIPs Referenced

NIP Purpose
NIP-44 Versioned encryption used for all private content
NIP-31 alt tags on all custom events for client discoverability
NIP-65 Relay list management (user-configurable read/write relays)
NIP-78 Application-specific addressable data (kind 30078)
NIP-07 Signer interface for encryption/decryption
NIP-19 Bech32 identifiers for routing and linking

Cited links

Discussion

Connect a key to comment.