{"id":"55fc742ebd4bc89a386c88bb8334ee45a07972766ad7256491b9b0266bd0dbcb","pubkey":"291c75d937a45f66a1209f8ea6611df7448c59b3526520c66ca2cdcd37f1bfbe","created_at":1774034494,"kind":30817,"tags":[["d","nip-xx-encrypted-workspace"],["title","NIP-XX: Encrypted Workspace"],["k","30800"],["k","30801"],["k","30802"],["k","30078"],["client","nostrhub.io"]],"content":"# NIP-XX: Encrypted Workspace\n\nA 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.\n\nKinds `30800`, `30801`, and `30802` were defined by [NIP-XX: Encrypted File Sync](https://nostrhub.io/naddr1qvzqqqrcvypzq0mhp4ja8fmy48zuk5p6uy37vtk8tx9dqdwcxm32sy8nsaa8gkeyqyv8wumn8ghj7urjv4kkjatd9ec8y6tdv9kzumn9wsqp5mnfwqkhs7pdv4hxxunewp6x2epdve5kcefdwdukucclhkuz8). 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.\n\n\n## Summary\n\n| Kind | Name | Type | Purpose |\n|---|---|---|---|\n| `30800` | File Content | Addressable | Encrypted document body |\n| `30801` | Vault Index | Addressable | Encrypted workspace manifest |\n| `30802` | Shared Document | Addressable | Document encrypted to a recipient |\n| `30078` | Structured Data | Addressable | Encrypted database rows, tasks, preferences |\n\n---\n\n## Kind 30800 — File Content\n\nStores the encrypted content of a single document. The `content` field is always a NIP-44 ciphertext — never plaintext.\n\n**Tags:**\n\n| Tag | Required | Description |\n|---|---|---|\n| `d` | Yes | Stable UUID-style file identifier (e.g. `doc-m3xk7f2`) |\n| `encrypted` | Yes | Always `\"nip44\"` |\n| `alt` | Yes | NIP-31 fallback: `\"Encrypted note - NIP-44\"` |\n\n**Decrypted payload:**\n\n```json\n{\n  \"title\": \"Meeting Notes\",\n  \"path\": \"/notes/meeting-notes.md\",\n  \"content\": \"# Meeting Notes\\n\\nDiscussion points...\",\n  \"modified\": 1742483921,\n  \"version\": 5,\n  \"checksum\": \"a3f1\",\n  \"contentType\": \"text/markdown\"\n}\n```\n\n| Field | Type | Required | Description |\n|---|---|---|---|\n| `title` | string | Yes | Human-readable document title |\n| `path` | string | Yes | Virtual filesystem path (e.g. `/notes/meeting-notes.md`) |\n| `content` | string | Yes | Document body (typically Markdown) |\n| `modified` | number | Yes | Unix timestamp of last modification |\n| `version` | number | No | Incrementing version counter |\n| `checksum` | string | No | Short content integrity checksum |\n| `contentType` | string | No | MIME type, defaults to `text/markdown` |\n\n**Encryption:**\n\n```\nevent.content = nip44.encrypt(authorPubkey, JSON.stringify(payload))\n```\n\nSelf-encryption — the conversation key is derived from the author's own keypair. Only the author can decrypt their own documents.\n\n---\n\n## Kind 30801 — Vault Index\n\nThe 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.\n\n- A file is **active** if it appears in `files[]`\n- A file is **deleted** if it appears in `deleted[]`\n- A file is **orphaned** if it exists on the relay but in neither array (recoverable via relay scan)\n\n**Tags:**\n\n| Tag | Required | Description |\n|---|---|---|\n| `d` | Yes | Vault identifier (e.g. `\"default-vault\"`) |\n| `encrypted` | Yes | Always `\"nip44\"` |\n| `alt` | Yes | NIP-31 fallback: `\"Encrypted vault index - NIP-44\"` |\n\n**Decrypted payload:**\n\n```json\n{\n  \"name\": \"My Workspace\",\n  \"description\": \"Private encrypted documents synced via Nostr\",\n  \"files\": [\n    {\n      \"d\": \"doc-m3xk7f2\",\n      \"path\": \"/notes/meeting-notes.md\",\n      \"title\": \"Meeting Notes\",\n      \"modified\": 1742483921,\n      \"eventId\": \"abcdef1234...\"\n    }\n  ],\n  \"deleted\": [\n    {\n      \"d\": \"doc-a1b2c3\",\n      \"path\": \"/notes/old-draft.md\",\n      \"title\": \"Old Draft\",\n      \"deletedAt\": 1742480000,\n      \"lastEventId\": \"abcdef5678...\"\n    }\n  ],\n  \"updated\": 1742483921\n}\n```\n\n**`files[]` entry:**\n\n| Field | Type | Required | Description |\n|---|---|---|---|\n| `d` | string | Yes | Matches the `d` tag of the corresponding kind 30800 event |\n| `path` | string | Yes | Virtual filesystem path |\n| `title` | string | Yes | Display title |\n| `modified` | number | Yes | Unix timestamp of last modification |\n| `eventId` | string | No | Event ID of the most recent kind 30800 event |\n\n**`deleted[]` entry (tombstone):**\n\n| Field | Type | Required | Description |\n|---|---|---|---|\n| `d` | string | Yes | Matches the `d` tag of the deleted kind 30800 event |\n| `path` | string | Yes | Last known path before deletion |\n| `title` | string | Yes | Last known title before deletion |\n| `deletedAt` | number | Yes | Unix timestamp of deletion |\n| `lastEventId` | string | No | Event ID of the final kind 30800 version |\n\n**Top-level fields:**\n\n| Field | Type | Required | Description |\n|---|---|---|---|\n| `name` | string | No | Workspace display name |\n| `description` | string | No | Workspace description |\n| `files` | array | Yes | Active file references |\n| `deleted` | array | No | Tombstones for deleted files |\n| `updated` | number | Yes | Unix timestamp of last index update |\n| `settings` | object | No | Workspace-level client settings |\n\n**Delete / Restore semantics:**\n\n- **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).\n- **Restore** — remove the tombstone from `deleted[]`, re-add the entry to `files[]`.\n- **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.\n\n---\n\n## Kind 30802 — Shared Document\n\nA 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.\n\n**Tags:**\n\n| Tag | Required | Description |\n|---|---|---|\n| `d` | Yes | Stable document identifier |\n| `p` | Yes | Recipient's pubkey |\n| `encrypted` | Yes | Always `\"nip44\"` |\n| `alt` | Yes | NIP-31 fallback: `\"Encrypted shared document - NIP-44\"` |\n\n**Encryption:**\n\n```\nevent.content = nip44.encrypt(recipientPubkey, JSON.stringify(payload))\n```\n\n---\n\n## Kind 30078 — Structured Data (NIP-78)\n\nEncrypted 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.\n\n**Tags:**\n\n| Tag | Required | Description |\n|---|---|---|\n| `d` | Yes | Entry identifier (e.g. `task-m3xk7f2`) |\n| `t` | Yes | Entry type: `\"task\"`, `\"preference\"`, `\"row\"`, etc. |\n| `t` | No | Status slug: `\"todo\"`, `\"in-progress\"`, `\"done\"` |\n| `encrypted` | Yes | Always `\"nip44\"` |\n| `alt` | Yes | NIP-31 fallback: `\"Encrypted structured data - NIP-44\"` |\n\n**Example decrypted task payload:**\n\n```json\n{\n  \"id\": \"task-m3xk7f2\",\n  \"type\": \"task\",\n  \"task\": \"Write release notes\",\n  \"status\": \"in-progress\",\n  \"assignee\": \"npub1...\",\n  \"due\": \"2026-04-01\",\n  \"modified\": 1742483921\n}\n```\n\n---\n\n## Encryption Model\n\nAll private content uses **NIP-44 self-encryption** unless otherwise specified:\n\n```\nevent.content = nip44.encrypt(authorPubkey, JSON.stringify(payload))\n```\n\nThe 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.\n\nKind 30802 (shared documents) uses the recipient's pubkey instead:\n\n```\nevent.content = nip44.encrypt(recipientPubkey, JSON.stringify(payload))\n```\n\n**Key properties:**\n\n- No plaintext content is ever stored in event fields visible to relays\n- Tags contain only UUID-style identifiers, the `\"nip44\"` signal, and generic NIP-31 descriptions — never user content\n- Works with any NIP-07 compatible signer; the private key is never exposed to the client application\n\n---\n\n## Security Requirements\n\n### Author filtering\n\nAll 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.\n\n```typescript\n// ✅ Correct\nnostr.query([{\n  kinds: [30800, 30801],\n  authors: [user.pubkey],\n}]);\n\n// ❌ Unsafe — accepts events from any publisher\nnostr.query([{\n  kinds: [30800, 30801],\n}]);\n```\n\n### No sensitive data in tags\n\nTags 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.\n\n---\n\n## Standard NIPs Referenced\n\n| NIP | Purpose |\n|---|---|\n| NIP-44 | Versioned encryption used for all private content |\n| NIP-31 | `alt` tags on all custom events for client discoverability |\n| NIP-65 | Relay list management (user-configurable read/write relays) |\n| NIP-78 | Application-specific addressable data (kind 30078) |\n| NIP-07 | Signer interface for encryption/decryption |\n| NIP-19 | Bech32 identifiers for routing and linking |","sig":"0c2c5d0dd5e68e54963d171e840e756c544d31d1e3f6b17c570baaf69955139c53d330d42799827a541525736d94e592f29c657fd547f53f2e16e293149d821e"}