Private messaging
Softchat private messages have two distinct layers:
- a rumor carries the author, event kind, timestamp, exact tags, content, and deterministic ID without a signature; and
- a NIP-59 seal and gift wrap authenticate and privately route that rumor to one recipient.
Never expose a rumor recovered from the network until unwrapGiftWrap
authenticates the outer route, outer signature, ciphertext, seal signature,
seal shape, rumor ID, and author binding.
Create a one-to-one message
The minimum NIP-17 helper creates kind 14 with exactly one canonical p tag.
Group messages, replies, attachments, and extension tags use the richer
chat event API.
The lower-level createRumor operation is available when the application has
already assembled another accepted event draft:
- Rust
- Kotlin
- Android
- Swift
- TypeScript
- Rust
- Kotlin
- Android
- Swift
- TypeScript
let message = alice.create_nip17_text_message(
&bob.public_key(),
1_700_000_000,
"hello Bob",
)?;
val message = alice.createNip17TextMessage(
recipient = bob.publicKey,
createdAt = 1_700_000_000,
content = "hello Bob",
)
Use createNip17TextMessage on a bounded CPU dispatcher. The returned rumor is
not ready for relay publication; wrap a distinct recipient copy and sender
history copy first.
let message = try alice.createNip17TextMessage(
for: bob.publicKey,
createdAt: 1_700_000_000,
content: "hello Bob"
)
const message = alice.createNip17TextMessage(
bob.publicKey,
1_700_000_000,
"hello Bob",
);
- Rust
- Kotlin
- Android
- Swift
- TypeScript
Wrap recipient and sender copies
Call giftWrap separately for every recipient and for the author’s own
recoverable history. Each call creates a fresh wrapper key, exactly one outer
p tag, empty seal tags, and independently randomized seal/wrapper timestamps
within the previous two days.
- Rust
- Kotlin
- Android
- Swift
- TypeScript
let recipient_copy = alice.gift_wrap(
&bob.public_key(),
message.rumor(),
Nip59EnvelopeKind::Durable,
now,
)?;
let sender_copy = alice.gift_wrap(
&alice.public_key(),
message.rumor(),
Nip59EnvelopeKind::Durable,
now,
)?;
val recipientCopy = alice.giftWrap(
bob.publicKey, message.rumor, Nip59EnvelopeKind.DURABLE, now
)
val senderCopy = alice.giftWrap(
alice.publicKey, message.rumor, Nip59EnvelopeKind.DURABLE, now
)
Persist both publish intents before handing frames to a WebSocket. Durable kind 1059 is retryable and persistable; ephemeral kind 21059 is not durable history.
let recipientCopy = try alice.giftWrap(
message.rumor, for: bob.publicKey, kind: .durable, now: now
)
let senderCopy = try alice.giftWrap(
message.rumor, for: alice.publicKey, kind: .durable, now: now
)
const recipientCopy = alice.giftWrap(
bob.publicKey, message.rumor, "durable", now,
);
const senderCopy = alice.giftWrap(
alice.publicKey, message.rumor, "durable", now,
);
- Rust
- Kotlin
- Android
- Swift
- TypeScript
Receive and classify
- Rust
- Kotlin
- Android
- Swift
- TypeScript
let envelope = bob.unwrap_gift_wrap(&recipient_copy)?;
let received = Nip17TextMessage::from_rumor(envelope.into_rumor())?;
assert_eq!(received.content(), "hello Bob");
val envelope = bob.unwrapGiftWrap(recipientCopy)
val received = Nip17TextMessage.fromRumor(envelope.rumor)
check(received.content == "hello Bob")
Parse the relay event, unwrap it, classify the authenticated rumor, and commit both the raw event and the typed projection in one Room transaction. Confirm relay ingestion only after that transaction succeeds.
let envelope = try bob.unwrapGiftWrap(recipientCopy)
let received = try Nip17TextMessage(rumor: envelope.rumor)
precondition(received.content == "hello Bob")
const envelope = bob.unwrapGiftWrap(recipientCopy);
const received = Nip17TextMessage.fromRumor(envelope.rumor);
console.assert(received.content === "hello Bob");
- Rust
- Kotlin
- Android
- Swift
- TypeScript
Durable and ephemeral envelopes
| Wrapper | Event kind | Use | Persistence |
|---|---|---|---|
| Durable | 1059 | messages, account copies, durable changes | persist and retry under host policy |
| Ephemeral | 21059 | typing and other transient signals | do not persist as history |
A wrong recipient, altered wrapper, altered seal, or mismatched rumor produces the same stable invalid-envelope failure. Do not reveal which layer failed.