{"id":"5e709057dee1310656d7f883e62b1cd06422e492836582ef6affd4e3e5973432","pubkey":"86184109eae937d8d6f980b4a0b46da4ef0d983eade403ee1b4c0b6bde238b47","created_at":1774861838,"kind":30817,"tags":[["d","personal-letters"],["title","Personal Letters"],["k","8211"],["client","nostrhub.io"]],"content":"# NIP: Personal Letters (Kind 8211)\n\n## Motivation\n\nExisting encrypted messaging on Nostr (NIP-04, NIP-17) is designed for chat-style conversations: fast, ephemeral, back-and-forth. This NIP defines a different interaction pattern: crafted, visual, one-off letters with decorative presentation, closer to physical postcards than instant messages. A letter carries not just text but the sender's choice of stationery, frame, font, and hand-placed stickers, all encrypted so only the recipient sees the full composition.\n\n## Summary\n\nKind 8211 is a regular event for sending encrypted personal letters between Nostr users. The entire letter (text, visual presentation, stickers) is encrypted using NIP-44 and stored in the `content` field. Only the recipient `p` tag and an `alt` fallback are stored in cleartext.\n\n## Privacy Model\n\nUnlike NIP-17/NIP-59, this scheme does **not** hide sender/recipient metadata or provide deniability. The event's `pubkey` identifies the sender and the cleartext `p` tag identifies the recipient. Timestamps are real. This is intentional: letters are a visible act of correspondence, analogous to a physical postcard where the addresses are on the outside but the content is private. The letter's entire visual presentation (stationery, stickers, text, font) is inside the NIP-44 encrypted payload and visible only to the sender and recipient.\n\nClients MAY wrap kind 8211 events in NIP-59 gift wraps for users who want metadata privacy, but this is not required by the protocol.\n\n## Event Structure\n\n```json\n{\n  \"kind\": 8211,\n  \"content\": \"<NIP-44 encrypted JSON>\",\n  \"tags\": [\n    [\"p\", \"<recipient-pubkey-hex>\"],\n    [\"alt\", \"Personal letter\"]\n  ]\n}\n```\n\n## Tags\n\n| Tag  | Required | Description                                       |\n|------|----------|---------------------------------------------------|\n| `p`  | Yes      | Recipient pubkey (hex). NIP-44 encryption target.  |\n| `alt` | Yes     | NIP-31 fallback: `\"Personal letter\"`               |\n\nNo other tags are used. All letter data is inside the encrypted content.\n\n## Encrypted Content\n\nThe `content` field is a NIP-44 encrypted JSON string. When decrypted with the counterparty's pubkey it yields:\n\n```json\n{\n  \"body\": \"Dear friend,\\n\\nI wanted to write you...\",\n  \"closing\": \"With love,\",\n  \"signature\": \"Alice\",\n  \"stickers\": [\n    {\n      \"url\": \"https://example.com/sticker.png\",\n      \"shortcode\": \"heart_eyes\",\n      \"x\": 75,\n      \"y\": 20,\n      \"rotation\": 12,\n      \"scale\": 1.5\n    },\n    {\n      \"url\": \"\",\n      \"shortcode\": \"drawing\",\n      \"svg\": \"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" viewBox=\\\"42 58 120 95\\\"><path d=\\\"M50,60Q55.2,68.1,60,80L65,85\\\" fill=\\\"none\\\" stroke=\\\"#1a1a1a\\\" stroke-width=\\\"6\\\" stroke-linecap=\\\"round\\\" stroke-linejoin=\\\"round\\\"/></svg>\",\n      \"x\": 30,\n      \"y\": 65,\n      \"rotation\": -5\n    }\n  ],\n  \"stationery\": {\n    \"color\": \"#C8E6C9\",\n    \"fontFamily\": \"Caveat, cursive\",\n    \"frame\": \"flowers\",\n    \"frameTint\": true,\n    \"event\": { \"kind\": 3367, \"id\": \"<hex>\", \"pubkey\": \"<hex>\", \"sig\": \"<hex>\", \"created_at\": 1234567890, \"content\": \"🌿\", \"tags\": [[\"c\", \"#C8E6C9\"], [\"c\", \"#A5D6A7\"], [\"c\", \"#81C784\"], [\"layout\", \"horizontal\"]] }\n  }\n}\n```\n\nAt least one of `body` (non-empty string) or `stickers` (non-empty array) MUST be present. A letter with neither is invalid and clients SHOULD discard it.\n\n| Field        | Required | Description                                                              |\n|--------------|----------|--------------------------------------------------------------------------|\n| `body`       | Yes      | Main letter text. Always present; may be an empty string `\"\"` for sticker-only letters. Clients SHOULD limit to 220 characters. May contain newlines. |\n| `closing`    | No       | Closing phrase (e.g. \"With love,\", \"Warmly,\")                            |\n| `signature`  | No       | Sender's chosen display name or signature                                |\n| `stickers`   | Conditional | Array of stickers placed on the letter card. Required if `body` is empty. |\n| `stationery` | No       | Visual stationery. See Stationery Object below.                          |\n\n### Sticker Object\n\nEach sticker in the `stickers` array has the following fields:\n\n| Field       | Required | Description                                                    |\n|-------------|----------|----------------------------------------------------------------|\n| `url`       | Yes      | Image URL of the sticker. Empty string `\"\"` for drawn stickers. |\n| `shortcode` | Yes      | NIP-30 shortcode name (without colons), or `\"drawing\"` for hand-drawn SVG stickers. |\n| `x`         | Yes      | Horizontal position as percentage (0-100) from the left edge   |\n| `y`         | Yes      | Vertical position as percentage (0-100) from the top edge      |\n| `rotation`  | Yes      | Rotation in degrees (-180 to 180)                              |\n| `scale`     | No       | Scale multiplier (default `1`). Range `0.5`-`4`.              |\n| `svg`       | No       | Raw SVG markup string for hand-drawn stickers. When present, clients render this inline instead of loading `url`. |\n\nStickers come in two forms:\n\n**Emoji stickers** are sourced from NIP-30 custom emoji packs. The sender's kind 10030 emoji list references kind 30030 emoji packs; emojis from both inline tags and referenced packs are available. They have a `url` pointing to an image and a `shortcode` identifying the emoji.\n\n**Drawn stickers** are freehand SVG drawings created by the sender. They have `shortcode: \"drawing\"`, `url: \"\"`, and an `svg` field containing a self-contained SVG element string (with `xmlns`, `viewBox`, and `<path>` elements). The SVG is tightly cropped to the drawing's bounding box. Clients SHOULD render the SVG inline at the sticker's position and size. Points in the SVG paths are simplified with Ramer-Douglas-Peucker to keep the markup compact. Clients SHOULD sanitize SVG markup before rendering to prevent script injection from malformed payloads.\n\nClients SHOULD render both forms absolutely positioned on top of the letter card at the specified coordinates, with the given rotation and scale applied. Because stickers are inside the encrypted content, they are only visible to the sender and recipient.\n\n**Note:** Emoji sticker URLs reference external images with no content hash. The image at a URL could change after the letter is sent. NIP-30 emoji tags do not currently carry integrity metadata, so hash verification is not practical without fetching and hashing at compose time. Drawn stickers do not have this issue since the SVG data is self-contained.\n\n### Stationery Object\n\nThe `stationery` field carries user-chosen presentation options and an optional source event. When the source event is present, clients SHOULD read rendering attributes (palette colors, text color, background image, etc.) from the event's tags rather than expecting them as flat fields.\n\n| Field          | Required | Description |\n|----------------|----------|-------------|\n| `color`        | Yes      | Background color (hex). Always present. |\n| `emoji`        | No       | Emoji character for backsplash or emblem display. |\n| `emojiMode`    | No       | `\"tile\"` (faint repeating pattern) or `\"emblem\"` (single large centered glyph). Default: `\"tile\"`. |\n| `fontFamily`   | No       | CSS `font-family` string (e.g. `\"Caveat, cursive\"`). Applied to all letter text. |\n| `frame`        | No       | Frame style ID. See Frame Styles below. |\n| `frameTint`    | No       | When `true`, color-shift the frame emojis to match the stationery palette. |\n| `event`        | No       | Source Nostr event (kind 36767 theme or kind 3367 color moment). See Source Event below. |\n\n#### Source Event\n\nWhen present, `event` is a complete signed Nostr event JSON object. Clients read rendering attributes directly from its tags:\n\n- **Kind 3367** (Color Moment): `c` tags contain hex palette colors, optional `layout` tag, optional emoji in the content field. The stationery's `emoji` field takes precedence; the event content is used as a fallback only when `emoji` is not set on the stationery object.\n- **Kind 36767** (Theme Definition): `c` tags with markers (`background`, `text`, `primary`), optional `bg` tag (background image URL and mode), `title` tag.\n\nPresets (built-in stationery) have no source event, only `color` and optionally `emoji`.\n\n### Frame Styles\n\nThe `frame` field is a string ID selecting a decorative emoji border around the letter card. Clients MAY support any set of frame IDs; see Appendix A for suggested defaults. If a client encounters an unrecognized frame ID, it SHOULD fall back to `\"none\"`.\n\nWhen `frameTint` is `true`, clients SHOULD darken the frame background from the stationery's dominant color and shift the emoji hues to match the palette. The tint color is derived from: the `primary` color from a theme event's `c` tags if present, the first palette color from a color moment event's `c` tags if present, or the background `color` otherwise. When `frameTint` is absent or `false`, the frame's default background and natural emoji colors SHOULD be used.\n\n### Font\n\nThe `fontFamily` field is a CSS `font-family` string. Clients SHOULD apply it to all letter text. If the font is unavailable, clients SHOULD fall back to their default font.\n\n### Card Dimensions\n\nClients SHOULD render letters as physical postcards with a fixed **5:4 landscape aspect ratio**. The card is a self-contained visual object, not a reflowing text document. It scales like a physical object: all internal sizing SHOULD use proportional units so the card renders uniformly at any display size.\n\n| Property       | Value    | Notes |\n|----------------|----------|-------|\n| Aspect ratio   | 5:4      | Landscape orientation, like a postcard |\n| Body max chars | 220      | Clients SHOULD enforce this limit |\n\nStickers are positioned as percentage coordinates relative to the card bounds. The closing and signature sit at the bottom-right. Clients SHOULD preserve this spatial layout rather than reflowing content into a different shape. See Appendix B for reference sizing values.\n\n## Text Color Rendering\n\nClients SHOULD derive letter body text color from the stationery:\n\n- **Theme** (event kind 36767): use the `text` color from the event's `c` tags directly.\n- **Color moment** (event kind 3367): average WCAG 2.1 relative luminance across the palette colors from the event's `c` tags. If avg > 0.5, use dark text; else use light text.\n- **Preset or no event**: WCAG luminance of `color`. If > 0.5, use dark text; else use light text.\n\nRuled lines on the letter SHOULD use the same light/dark logic at reduced opacity.\n\n## Client Behavior\n\n### Sending\n\n1. Build the stationery object from the user's selection. For color moments (kind 3367), include the source event and set `color` from its first `c` tag. For themes (kind 36767), include the source event and set `color` from the `c` tag with marker `background`.\n2. Merge the user's font and frame choices into the stationery.\n3. Compose the encrypted content object: `body` (always present, may be empty string), plus optional `closing`, `signature`, `stickers`, `stationery`.\n4. Encrypt the JSON with NIP-44 using the recipient's pubkey.\n5. Publish kind 8211 with `p` and `alt` tags.\n\n### Receiving\n\n1. Query `{ kinds: [8211], \"#p\": [\"<my-pubkey>\"] }` for inbox.\n2. Query `{ kinds: [8211], authors: [\"<my-pubkey>\"] }` for sent.\n3. Decrypt `content` with NIP-44 using the counterparty's pubkey.\n4. Extract `body`, `closing`, `signature`, and `stickers` for letter text rendering.\n5. If `stationery` is present, render the background using its attributes. Apply `fontFamily` if provided.\n6. Apply `frame` and `frameTint` from the stationery if present.\n7. If `stickers` array is present, render each sticker absolutely positioned on top of the letter card at the specified `(x, y)` percentage coordinates with the given `rotation` and `scale`. For emoji stickers, load the image from `url`. For drawn stickers (where `svg` is present), render the SVG markup inline.\n\n---\n\n## Appendix A: Suggested Frame Catalog\n\nThe following frame IDs are suggested defaults. Clients MAY support additional frames or a subset of these.\n\n| Frame ID     | Emojis                       | Default Background |\n|--------------|------------------------------|--------------------|\n| `none`       | No frame (plain)             | --                 |\n| `flowers`    | 🌸🌺🌼🌷🌻🌹                | `#3a7a3a`          |\n| `autumn`     | 🍂🍁🍃🌾🍄🌰                | `#8b5e3c`          |\n| `ocean`      | 🐚🌊🐠🐙🦀🐬                | `#1a5276`          |\n| `celestial`  | 🪐🌙⭐🌕☄️🔭                | `#1a1a3e`          |\n| `hearts`     | ❤️💕💗💖💝💘                | `#8b2252`          |\n| `garden`     | 🦋🐝🌿🌱🐞🍀                | `#2d5a27`          |\n| `winter`     | ❄️⛄🌨️🏔️🎿🧣              | `#4a6d8c`          |\n| `fruit`      | 🍊🍋🍓🍑🍒🫐                | `#6b4226`          |\n| `sparkle`    | ✨💎🔮🪩⚡🌈                | `#4a2d6b`          |\n\n## Appendix B: Reference Implementation Sizing\n\nThe following values are used by the reference web implementation (CSS container query units). Native clients SHOULD achieve equivalent proportions using platform-appropriate units.\n\n| Property       | Reference Value | Notes |\n|----------------|-----------------|-------|\n| Body font size | ~4.8cqw         | Container query width units; yields ~25 characters per line |\n| Line height    | ~8.4cqw         | Produces ruled-notebook-style line spacing |\n| Padding        | ~5cqw           | All sides |\n\nThe intent is that the card reads like a small physical postcard (~5\" x 4\") with handwritten text. Font size and line height should scale proportionally with card width so the layout is consistent across display sizes. When `frameTint` is `true`, the reference implementation uses a CSS `mix-blend-mode: color` overlay to shift frame emoji hues to the stationery palette.","sig":"a0555f6c4fabc8cdf2ed585ebc4a251a404060c0c977630be0ddba88ba3a9f034b6253a030e132072b8617293b18cf036f8b36f73326f279a24c3a1c9b172ef1"}