Personal Letters
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):
ctags contain hex palette colors, optionallayouttag, optional emoji in the content field. The stationery’semojifield takes precedence; the event content is used as a fallback only whenemojiis not set on the stationery object. - Kind 36767 (Theme Definition):
ctags with markers (background,text,primary), optionalbgtag (background image URL and mode),titletag.
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
textcolor from the event’sctags directly. - Color moment (event kind 3367): average WCAG 2.1 relative luminance across the palette colors from the event’s
ctags. 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
- Build the stationery object from the user’s selection. For color moments (kind 3367), include the source event and set
colorfrom its firstctag. For themes (kind 36767), include the source event and setcolorfrom thectag with markerbackground. - Merge the user’s font and frame choices into the stationery.
- Compose the encrypted content object:
body(always present, may be empty string), plus optionalclosing,signature,stickers,stationery. - Encrypt the JSON with NIP-44 using the recipient’s pubkey.
- Publish kind 8211 with
pandalttags.
Receiving
- Query
{ kinds: [8211], "#p": ["<my-pubkey>"] }for inbox. - Query
{ kinds: [8211], authors: ["<my-pubkey>"] }for sent. - Decrypt
contentwith NIP-44 using the counterparty’s pubkey. - Extract
body,closing,signature, andstickersfor letter text rendering. - If
stationeryis present, render the background using its attributes. ApplyfontFamilyif provided. - Apply
frameandframeTintfrom the stationery if present. - If
stickersarray is present, render each sticker absolutely positioned on top of the letter card at the specified(x, y)percentage coordinates with the givenrotationandscale. For emoji stickers, load the image fromurl. For drawn stickers (wheresvgis 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.
Looking for comments…
Searching Nostr relays. This may take a moment the first time this article is opened.