NIP-XX: Encrypted Workspace
NIP-XX: Encrypted Workspace
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 todeleted[]. 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 tofiles[]. - 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 |