Personal Letters

Specifies: kind 8211

NIP: Personal Letters (Kind 8211)

Motivation

Existing 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.

Summary

Kind 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.

Privacy Model

Unlike 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.

Clients MAY wrap kind 8211 events in NIP-59 gift wraps for users who want metadata privacy, but this is not required by the protocol.

Event Structure

{
  "kind": 8211,
  "content": "<NIP-44 encrypted JSON>",
  "tags": [
    ["p", "<recipient-pubkey-hex>"],
    ["alt", "Personal letter"]
  ]
}

Tags

Tag Required Description
p Yes Recipient pubkey (hex). NIP-44 encryption target.
alt Yes NIP-31 fallback: "Personal letter"

No other tags are used. All letter data is inside the encrypted content.

Encrypted Content

The content field is a NIP-44 encrypted JSON string. When decrypted with the counterparty’s pubkey it yields:

{
  "body": "Dear friend,\n\nI wanted to write you...",
  "closing": "With love,",
  "signature": "Alice",
  "stickers": [
    {
      "url": "https://example.com/sticker.png",
      "shortcode": "heart_eyes",
      "x": 75,
      "y": 20,
      "rotation": 12,
      "scale": 1.5
    },
    {
      "url": "",
      "shortcode": "drawing",
      "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>",
      "x": 30,
      "y": 65,
      "rotation": -5
    }
  ],
  "stationery": {
    "color": "#C8E6C9",
    "fontFamily": "Caveat, cursive",
    "frame": "flowers",
    "frameTint": true,
    "event": { "kind": 3367, "id": "<hex>", "pubkey": "<hex>", "sig": "<hex>", "created_at": 1234567890, "content": "🌿", "tags": [["c", "#C8E6C9"], ["c", "#A5D6A7"], ["c", "#81C784"], ["layout", "horizontal"]] }
  }
}

At 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.

Field Required Description
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.
closing No Closing phrase (e.g. “With love,”, “Warmly,”)
signature No Sender’s chosen display name or signature
stickers Conditional Array of stickers placed on the letter card. Required if body is empty.
stationery No Visual stationery. See Stationery Object below.

Sticker Object

Each sticker in the stickers array has the following fields:

Field Required Description
url Yes Image URL of the sticker. Empty string "" for drawn stickers.
shortcode Yes NIP-30 shortcode name (without colons), or "drawing" for hand-drawn SVG stickers.
x Yes Horizontal position as percentage (0-100) from the left edge
y Yes Vertical position as percentage (0-100) from the top edge
rotation Yes Rotation in degrees (-180 to 180)
scale No Scale multiplier (default 1). Range 0.5-4.
svg No Raw SVG markup string for hand-drawn stickers. When present, clients render this inline instead of loading url.

Stickers come in two forms:

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.

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.

Clients 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.

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.

Stationery Object

The 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.

Field Required Description
color Yes Background color (hex). Always present.
emoji No Emoji character for backsplash or emblem display.
emojiMode No "tile" (faint repeating pattern) or "emblem" (single large centered glyph). Default: "tile".
fontFamily No CSS font-family string (e.g. "Caveat, cursive"). Applied to all letter text.
frame No Frame style ID. See Frame Styles below.
frameTint No When true, color-shift the frame emojis to match the stationery palette.
event No Source Nostr event (kind 36767 theme or kind 3367 color moment). See Source Event below.

Source Event

When present, event is a complete signed Nostr event JSON object. Clients read rendering attributes directly from its tags:

  • 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.
  • Kind 36767 (Theme Definition): c tags with markers (background, text, primary), optional bg tag (background image URL and mode), title tag.

Presets (built-in stationery) have no source event, only color and optionally emoji.

Frame Styles

The 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".

When 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.

Font

The 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.

Card Dimensions

Clients 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.

Property Value Notes
Aspect ratio 5:4 Landscape orientation, like a postcard
Body max chars 220 Clients SHOULD enforce this limit

Stickers 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.

Text Color Rendering

Clients SHOULD derive letter body text color from the stationery:

  • Theme (event kind 36767): use the text color from the event’s c tags directly.
  • 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.
  • Preset or no event: WCAG luminance of color. If > 0.5, use dark text; else use light text.

Ruled lines on the letter SHOULD use the same light/dark logic at reduced opacity.

Client Behavior

Sending

  1. 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.
  2. Merge the user’s font and frame choices into the stationery.
  3. Compose the encrypted content object: body (always present, may be empty string), plus optional closing, signature, stickers, stationery.
  4. Encrypt the JSON with NIP-44 using the recipient’s pubkey.
  5. Publish kind 8211 with p and alt tags.

Receiving

  1. Query { kinds: [8211], "#p": ["<my-pubkey>"] } for inbox.
  2. Query { kinds: [8211], authors: ["<my-pubkey>"] } for sent.
  3. Decrypt content with NIP-44 using the counterparty’s pubkey.
  4. Extract body, closing, signature, and stickers for letter text rendering.
  5. If stationery is present, render the background using its attributes. Apply fontFamily if provided.
  6. Apply frame and frameTint from the stationery if present.
  7. 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.

Appendix A: Suggested Frame Catalog

The following frame IDs are suggested defaults. Clients MAY support additional frames or a subset of these.

Frame ID Emojis Default Background
none No frame (plain)
flowers 🌸🌺🌼🌷🌻🌹 #3a7a3a
autumn 🍂🍁🍃🌾🍄🌰 #8b5e3c
ocean 🐚🌊🐠🐙🦀🐬 #1a5276
celestial 🪐🌙⭐🌕☄️🔭 #1a1a3e
hearts ❤️💕💗💖💝💘 #8b2252
garden 🦋🐝🌿🌱🐞🍀 #2d5a27
winter ❄️⛄🌨️🏔️🎿🧣 #4a6d8c
fruit 🍊🍋🍓🍑🍒🫐 #6b4226
sparkle ✨💎🔮🪩⚡🌈 #4a2d6b

Appendix B: Reference Implementation Sizing

The following values are used by the reference web implementation (CSS container query units). Native clients SHOULD achieve equivalent proportions using platform-appropriate units.

Property Reference Value Notes
Body font size ~4.8cqw Container query width units; yields ~25 characters per line
Line height ~8.4cqw Produces ruled-notebook-style line spacing
Padding ~5cqw All sides

The 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.


Looking for comments…

Searching Nostr relays. This may take a moment the first time this article is opened.