Chat and account events
The event facade authors canonical Softchat rumors and returns a typed view alongside the exact raw rumor. On receive, classifiers validate the raw rumor and derive the same view. Store the authenticated raw event as the identity source; use typed views for UI and projections.
Messages, relations, and attachments
chat.messageCreate and classify complete Softchat kind-14 messages.
- Inputs and exact bounds
- 1 to 256 participants
- at most 128 emoji tags
- at most 256 extension tags
- Output
- A typed view retaining the exact rumor.
- Trust and authentication
- Network rumors require authenticated NIP-59 provenance before projection.
- Stable errors
softchat_event_creation_failed,invalid_softchat_event· handling and retry rules
Ownership, lifecycle, and execution rules
- Ownership
- Rust owns protocol semantics, Android account SQLite/use-case state, and optional native relay I/O; the host owns account lifecycle, HTTP/media/background effects, files, and UI.
- Retry
- Retry only host transport or explicitly retryable session work; validation and authentication failures are permanent for the same input.
- Lifecycle
- Language ownership is automatic; explicitly cancel or erase only at the operation-specific authority boundary.
- Concurrency
- Serialize mutable session capabilities; immutable values follow the language type system.
- Cancellation
- The host owns async cancellation; use cancel, shutdown, close, or erase only where exposed.
Exact public symbols
- Rust
- Android
- Swift
- TypeScript
LocalIdentity::create_chat_messageclassify_chat_messageSoftchatIdentity.createChatMessageSoftchatEvents.chatMessageSoftchatIdentity.createChatMessageSoftchatEvents.chatMessageSoftchatIdentity.createChatMessageSoftchatEvents.chatMessageA chat message contains participants, content, an optional reply or forward
relation, attachment metadata, emoji tags, and bounded extension tags.
Replies emit a marked e tag; forwards emit q. Legacy unmarked Android
reply tags remain reader compatibility. NIP-18 kind 16 is receive-only and is
not a Softchat forward.
The text-message contract defines the account-level message identity, durable lifecycle, effective edits/deletions and query behavior. This guide owns the lower-level event-authoring recipes.
- Rust
- Android
- Swift
- TypeScript
let message = identity.create_chat_message(ChatMessageDraft {
created_at,
participants: vec![recipient],
content: "hello".into(),
relation: ChatRelation::None,
attachments: vec![],
emoji_tags: vec![],
extension_tags: vec![],
})?;
let received = classify_chat_message(message.rumor)?;
val message = identity.createChatMessage(
ChatMessageDraft(
createdAt = createdAt,
participants = listOf(recipient),
content = "hello",
),
)
val received = SoftchatEvents.chatMessage(message.rumor)
Keep waveform samples in Android’s local file/UI model; only bounded interoperable attachment metadata belongs in the rumor and Rust-owned account database.
let message = try identity.createChatMessage(
.init(createdAt: createdAt, participants: [recipient], content: "hello")
)
let received = try SoftchatEvents.chatMessage(message.rumor)
const message = identity.createChatMessage({
createdAt,
participants: [recipient],
content: "hello",
});
const received = SoftchatEvents.chatMessage(message.rumor);
Subjects
chat.subjectCreate and classify canonical group subject updates.
- Inputs and exact bounds
- 1 to 256 participants
- at most 128 emoji tags
- Output
- A typed subject view retaining the exact rumor.
- Trust and authentication
- Network rumors require authenticated provenance.
- Stable errors
softchat_event_creation_failed,invalid_softchat_event· handling and retry rules
Ownership, lifecycle, and execution rules
- Ownership
- Rust owns protocol semantics, Android account SQLite/use-case state, and optional native relay I/O; the host owns account lifecycle, HTTP/media/background effects, files, and UI.
- Retry
- Retry only host transport or explicitly retryable session work; validation and authentication failures are permanent for the same input.
- Lifecycle
- Language ownership is automatic; explicitly cancel or erase only at the operation-specific authority boundary.
- Concurrency
- Serialize mutable session capabilities; immutable values follow the language type system.
- Cancellation
- The host owns async cancellation; use cancel, shutdown, close, or erase only where exposed.
Exact public symbols
- Rust
- Android
- Swift
- TypeScript
LocalIdentity::create_subjectclassify_subjectSoftchatIdentity.createSubjectSoftchatEvents.subjectSoftchatIdentity.createSubjectSoftchatEvents.subjectSoftchatIdentity.createSubjectSoftchatEvents.subjectSubjects use the canonical subject tag. During icon migration the writer
emits matching imeta and image tags.
- Rust
- Android
- Swift
- TypeScript
let subject = identity.create_subject(SubjectDraft {
created_at,
participants: vec![recipient],
subject: "Roadmap".into(),
emoji_tags: vec![],
icon: None,
extension_tags: vec![],
})?;
let received = classify_subject(subject.rumor)?;
val subject = identity.createSubject(
SubjectDraft(createdAt, listOf(recipient), "Roadmap"),
)
val received = SoftchatEvents.subject(subject.rumor)
let subject = try identity.createSubject(
.init(createdAt: createdAt, participants: [recipient], subject: "Roadmap")
)
let received = try SoftchatEvents.subject(subject.rumor)
const subject = identity.createSubject({
createdAt, participants: [recipient], subject: "Roadmap",
});
const received = SoftchatEvents.subject(subject.rumor);
Reactions
chat.reactionCreate and classify private reactions.
- Inputs and exact bounds
- canonical parent id, author, and kind
- 1 to 256 participants
- Output
- A typed reaction retaining the exact rumor.
- Trust and authentication
- Target metadata is validated; network provenance must already be authenticated.
- Stable errors
softchat_event_creation_failed,invalid_softchat_event· handling and retry rules
Ownership, lifecycle, and execution rules
- Ownership
- Rust owns protocol semantics, Android account SQLite/use-case state, and optional native relay I/O; the host owns account lifecycle, HTTP/media/background effects, files, and UI.
- Retry
- Retry only host transport or explicitly retryable session work; validation and authentication failures are permanent for the same input.
- Lifecycle
- Language ownership is automatic; explicitly cancel or erase only at the operation-specific authority boundary.
- Concurrency
- Serialize mutable session capabilities; immutable values follow the language type system.
- Cancellation
- The host owns async cancellation; use cancel, shutdown, close, or erase only where exposed.
Exact public symbols
- Rust
- Android
- Swift
- TypeScript
LocalIdentity::create_reactionclassify_reactionSoftchatIdentity.createReactionSoftchatEvents.reactionSoftchatIdentity.createReactionSoftchatEvents.reactionSoftchatIdentity.createReactionSoftchatEvents.reactionReactions bind the parent event ID, author, and kind. A custom shortcode also requires its bounded HTTPS emoji URL.
- Rust
- Android
- Swift
- TypeScript
let reaction = identity.create_reaction(ReactionDraft {
participants: vec![recipient],
parent_id,
parent_author,
parent_kind: NostrEventKind::PRIVATE_DIRECT_MESSAGE,
reaction: "👍".into(),
custom_emoji_url: None,
created_at,
})?;
let received = classify_reaction(reaction.rumor)?;
val reaction = identity.createReaction(
listOf(recipient), parentId, parentAuthor,
NostrEventKind.PRIVATE_DIRECT_MESSAGE, "👍", createdAt = createdAt,
)
val received = SoftchatEvents.reaction(reaction.rumor)
let reaction = try identity.createReaction(
.init(
participants: [recipient], parentID: parentID,
parentAuthor: parentAuthor, parentKind: .privateDirectMessage,
reaction: "👍", createdAt: createdAt
)
)
let received = try SoftchatEvents.reaction(reaction.rumor)
const reaction = identity.createReaction({
participants: [recipient], parentId, parentAuthor,
parentKind: NostrEventKind.privateDirectMessage,
reaction: "👍", createdAt,
});
const received = SoftchatEvents.reaction(reaction.rumor);
Deletions
chat.deletionCreate and authorize private deletion requests.
- Inputs and exact bounds
- 1 to 256 unique event IDs
- 1 to 256 participants
- Output
- An authorized deletion view.
- Trust and authentication
- A deletion changes projections only after author validation.
- Stable errors
softchat_event_creation_failed,invalid_softchat_event· handling and retry rules
Ownership, lifecycle, and execution rules
- Ownership
- Rust owns protocol semantics, Android account SQLite/use-case state, and optional native relay I/O; the host owns account lifecycle, HTTP/media/background effects, files, and UI.
- Retry
- Retry only host transport or explicitly retryable session work; validation and authentication failures are permanent for the same input.
- Lifecycle
- Language ownership is automatic; explicitly cancel or erase only at the operation-specific authority boundary.
- Concurrency
- Serialize mutable session capabilities; immutable values follow the language type system.
- Cancellation
- The host owns async cancellation; use cancel, shutdown, close, or erase only where exposed.
Exact public symbols
- Rust
- Android
- Swift
- TypeScript
LocalIdentity::create_deletionclassify_deletionSoftchatIdentity.createDeletionSoftchatEvents.deletionSoftchatIdentity.createDeletionSoftchatEvents.deletionSoftchatIdentity.createDeletionSoftchatEvents.deletionOnly the original author can delete its events. Projection code applies valid deletion before every edit and never rewrites the original rumor.
- Rust
- Android
- Swift
- TypeScript
let deletion = identity.create_deletion(
vec![recipient], vec![original_id], "retracted".into(), created_at,
)?;
let received = classify_deletion(deletion.rumor)?;
val deletion = identity.createDeletion(
listOf(recipient), listOf(originalId), "retracted", createdAt,
)
val received = SoftchatEvents.deletion(deletion.rumor)
Enqueue authored rumors through account.outgoing.enqueueRumor and ingest
received wrappers through account.incoming.ingest. Rust stores exact truth
and applies deletion precedence in the same transaction.
let deletion = try identity.createDeletion(
participants: [recipient], eventIDs: [originalID],
reason: "retracted", createdAt: createdAt
)
let received = try SoftchatEvents.deletion(deletion.rumor)
const deletion = identity.createDeletion(
[recipient], [originalId], "retracted", createdAt,
);
const received = SoftchatEvents.deletion(deletion.rumor);
Edits
chat.editCreate and project author-bound text and emoji edits.
- Inputs and exact bounds
- original event id
- 1 to 256 participants
- at most 128 emoji tags
- Output
- A typed edit retaining compatibility tags.
- Trust and authentication
- Edits are untrusted until author and target validation succeeds.
- Stable errors
softchat_event_creation_failed,invalid_softchat_event,invalid_softchat_projection· handling and retry rules
Ownership, lifecycle, and execution rules
- Ownership
- Rust owns protocol semantics, Android account SQLite/use-case state, and optional native relay I/O; the host owns account lifecycle, HTTP/media/background effects, files, and UI.
- Retry
- Retry only host transport or explicitly retryable session work; validation and authentication failures are permanent for the same input.
- Lifecycle
- Language ownership is automatic; explicitly cancel or erase only at the operation-specific authority boundary.
- Concurrency
- Serialize mutable session capabilities; immutable values follow the language type system.
- Cancellation
- The host owns async cancellation; use cancel, shutdown, close, or erase only where exposed.
Exact public symbols
- Rust
- Android
- Swift
- TypeScript
LocalIdentity::create_editclassify_editSoftchatIdentity.createEditSoftchatEvents.editSoftchatIdentity.createEditSoftchatEvents.editSoftchatIdentity.createEditSoftchatEvents.editAn edit replaces text and emoji only. Author validation, (createdAt, eventID)
ordering, and deletion precedence are deterministic.
- Rust
- Android
- Swift
- TypeScript
let edit = identity.create_edit(
vec![recipient], original_id, "updated".into(), vec![], created_at,
)?;
let received = classify_edit(edit.rumor)?;
val edit = identity.createEdit(
listOf(recipient), originalId, "updated", createdAt = createdAt,
)
val received = SoftchatEvents.edit(edit.rumor)
Let SoftchatAccount ingest or enqueue the operation. Rust applies authorized
edit ordering without overwriting raw history.
let edit = try identity.createEdit(
participants: [recipient], originalEventID: originalID,
content: "updated", createdAt: createdAt
)
let received = try SoftchatEvents.edit(edit.rumor)
const edit = identity.createEdit(
[recipient], originalId, "updated", createdAt,
);
const received = SoftchatEvents.edit(edit.rumor);
Typing indicators
chat.typingCreate and classify ephemeral typing rumors.
- Inputs and exact bounds
- 1 to 256 participants
- portable explicit timestamp
- Output
- A typed ephemeral rumor.
- Trust and authentication
- Typing is transient and must not be persisted as durable history.
- Stable errors
softchat_event_creation_failed,invalid_softchat_event· handling and retry rules
Ownership, lifecycle, and execution rules
- Ownership
- Rust owns protocol semantics, Android account SQLite/use-case state, and optional native relay I/O; the host owns account lifecycle, HTTP/media/background effects, files, and UI.
- Retry
- Retry only host transport or explicitly retryable session work; validation and authentication failures are permanent for the same input.
- Lifecycle
- Language ownership is automatic; explicitly cancel or erase only at the operation-specific authority boundary.
- Concurrency
- Serialize mutable session capabilities; immutable values follow the language type system.
- Cancellation
- The host owns async cancellation; use cancel, shutdown, close, or erase only where exposed.
Exact public symbols
- Rust
- Android
- Swift
- TypeScript
LocalIdentity::create_typingclassify_typingSoftchatIdentity.createTypingSoftchatEvents.typingSoftchatIdentity.createTypingSoftchatEvents.typingSoftchatIdentity.createTypingSoftchatEvents.typingTyping is kind 21234 and must be wrapped as ephemeral kind 21059. Do not store it as durable conversation history.
- Rust
- Android
- Swift
- TypeScript
let typing = identity.create_typing(vec![recipient], created_at)?;
let received = classify_typing(typing.rumor)?;
val typing = identity.createTyping(listOf(recipient), createdAt)
val received = SoftchatEvents.typing(typing.rumor)
let typing = try identity.createTyping(
participants: [recipient], createdAt: createdAt
)
let received = try SoftchatEvents.typing(typing.rumor)
const typing = identity.createTyping([recipient], createdAt);
const received = SoftchatEvents.typing(typing.rumor);
Generic repost compatibility
chat.generic-repostClassify receive-only NIP-18 generic reposts.
- Inputs and exact bounds
- one verified signed kind-16 event at most 512 KiB with exactly one embedded signed event
- Output
- The exact repost and verified embedded event.
- Trust and authentication
- Both signatures and author/tag bindings are authenticated before output.
- Stable errors
invalid_softchat_event· handling and retry rules
Ownership, lifecycle, and execution rules
- Ownership
- Rust owns protocol semantics, Android account SQLite/use-case state, and optional native relay I/O; the host owns account lifecycle, HTTP/media/background effects, files, and UI.
- Retry
- Retry only host transport or explicitly retryable session work; validation and authentication failures are permanent for the same input.
- Lifecycle
- Language ownership is automatic; explicitly cancel or erase only at the operation-specific authority boundary.
- Concurrency
- Serialize mutable session capabilities; immutable values follow the language type system.
- Cancellation
- The host owns async cancellation; use cancel, shutdown, close, or erase only where exposed.
Exact public symbols
- Rust
- Android
- Swift
- TypeScript
classify_generic_repostSoftchatEvents.genericRepostSoftchatEvents.genericRepostSoftchatEvents.genericRepostNIP-18 kind 16 is receive-only and distinct from a Softchat q-tag forward.
The classifier verifies both the signed repost and its embedded event.
- Rust
- Android
- Swift
- TypeScript
let repost = classify_generic_repost(signed_repost)?;
let embedded = repost.embedded_event;
val repost = SoftchatEvents.genericRepost(signedRepost)
val embedded = repost.embeddedEvent
let repost = try SoftchatEvents.genericRepost(signedRepost)
let embedded = repost.embeddedEvent
const repost = SoftchatEvents.genericRepost(signedRepost);
const embedded = repost.embeddedEvent;
Metadata and contacts
account.metadataCreate private metadata and author-copy contact backups.
- Inputs and exact bounds
- metadata JSON at most 256 KiB
- at most 10,000 contacts
- Output
- Typed values retaining unknown metadata fields.
- Trust and authentication
- Only authenticated private rumors may update account projections.
- Stable errors
invalid_user_metadata,invalid_contact_list· handling and retry rules
Ownership, lifecycle, and execution rules
- Ownership
- Rust owns protocol semantics, Android account SQLite/use-case state, and optional native relay I/O; the host owns account lifecycle, HTTP/media/background effects, files, and UI.
- Retry
- Retry only host transport or explicitly retryable session work; validation and authentication failures are permanent for the same input.
- Lifecycle
- Language ownership is automatic; explicitly cancel or erase only at the operation-specific authority boundary.
- Concurrency
- Serialize mutable session capabilities; immutable values follow the language type system.
- Cancellation
- The host owns async cancellation; use cancel, shutdown, close, or erase only where exposed.
Exact public symbols
- Rust
- Android
- Swift
- TypeScript
LocalIdentity::create_user_metadataLocalIdentity::create_follow_listSoftchatIdentity.createUserMetadataSoftchatIdentity.createFollowListSoftchatIdentity.createUserMetadataSoftchatIdentity.createFollowListSoftchatIdentity.createUserMetadataSoftchatIdentity.createFollowListUser metadata is a private kind-0 rumor. Known profile fields are typed while
unknown JSON fields are retained. Follow lists are private author-copy kind-3
events with canonical p tags, relay hints, and optional local names.
Native Rust provides latest_user_metadata and latest_application_data.
SoftchatAccount applies the same (createdAt, eventID) ordering inside the
Android database. Other hosts apply it at their persistence boundary. An older
event must never replace a newer projection merely because it arrived later.
- Rust
- Android
- Swift
- TypeScript
let metadata = identity.create_user_metadata(
created_at,
Some("Alice".to_owned()),
None, None, None, None, None, None,
r#"{"future":true}"#,
)?;
let received_metadata = classify_user_metadata(metadata.rumor.clone())?;
let follows = identity.create_follow_list(
created_at + 1,
vec![Contact {
public_key: recipient.to_hex(),
relay_hint: "wss://relay.example".to_owned(),
local_name: String::new(),
}],
)?;
let received_follows = classify_follow_list(follows.rumor.clone())?;
Native Rust additionally exposes latest_user_metadata for deterministic
selection over a bounded candidate set. Foreign applications apply the same
documented (createdAt, eventID) ordering in their database projection.
val metadata = identity.createUserMetadata(
UserMetadataDraft(
createdAt = createdAt,
name = "Alice",
unknownJson = """{"future":true}""",
),
)
val receivedMetadata = SoftchatEvents.userMetadata(metadata.rumor)
val follows = identity.createFollowList(
createdAt + 1,
listOf(Contact(recipient, relayHint = "wss://relay.example")),
)
val receivedFollows = SoftchatEvents.followList(follows.rumor)
Pass the signed wrapper to account.incoming.ingest, or the authored rumor to
account.outgoing.enqueueRumor. Rust stores raw truth and updates the latest
projection atomically in (createdAt, eventID) order.
let metadata = try identity.createUserMetadata(
.init(
createdAt: createdAt,
name: "Alice",
unknownJSON: #"{"future":true}"#
)
)
let receivedMetadata = try SoftchatEvents.userMetadata(metadata.rumor)
let follows = try identity.createFollowList(
createdAt: createdAt + 1,
contacts: [
.init(publicKey: recipient, relayHint: "wss://relay.example")
]
)
let receivedFollows = try SoftchatEvents.followList(follows.rumor)
const metadata = identity.createUserMetadata({
createdAt,
name: "Alice",
unknownJson: "{\"future\":true}",
});
const receivedMetadata = SoftchatEvents.userMetadata(metadata.rumor);
const follows = identity.createFollowList(createdAt + 1, [{
publicKey: recipient,
relayHint: "wss://relay.example",
}]);
const receivedFollows = SoftchatEvents.followList(follows.rumor);
Application data
account.application-dataCreate direct NIP-78 data and private kind-30079 sync data.
- Inputs and exact bounds
- application JSON at most 256 KiB
- explicit supported context
- Output
- Verified direct data or authenticated self-decrypted sync data.
- Trust and authentication
- Encrypted sync data remains untrusted until signature, author, and decryption checks succeed.
- Stable errors
invalid_application_data· handling and retry rules
Ownership, lifecycle, and execution rules
- Ownership
- Rust owns protocol semantics, Android account SQLite/use-case state, and optional native relay I/O; the host owns account lifecycle, HTTP/media/background effects, files, and UI.
- Retry
- Retry only host transport or explicitly retryable session work; validation and authentication failures are permanent for the same input.
- Lifecycle
- Language ownership is automatic; explicitly cancel or erase only at the operation-specific authority boundary.
- Concurrency
- Serialize mutable session capabilities; immutable values follow the language type system.
- Cancellation
- The host owns async cancellation; use cancel, shutdown, close, or erase only where exposed.
Exact public symbols
- Rust
- Android
- Swift
- TypeScript
LocalIdentity::create_application_dataLocalIdentity::create_app_data_syncSoftchatIdentity.createApplicationDataSoftchatIdentity.createAppDataSyncSoftchatIdentity.createApplicationDataSoftchatIdentity.createAppDataSyncSoftchatIdentity.createApplicationDataSoftchatIdentity.createAppDataSync- Kind 30078 represents namespaced application data.
- Kind 30079 is signed, self-encrypted cross-device app-data synchronization.
decryptAppDataSyncauthenticates the self-authored event before exposing its JSON.
The SDK owns event shape, signing, encryption, and deterministic ordering. The application owns schema migration and conflict resolution inside the JSON payload.
- Rust
- Android
- Swift
- TypeScript
let direct = identity.create_application_data(
created_at,
"com.example.settings".to_owned(),
r#"{"enabled":true}"#.to_owned(),
)?;
let received_direct = classify_application_data(direct.event.clone())?;
let sync = identity.create_app_data_sync(
created_at + 1,
AppDataContext::ReadState,
r#"{"lastRead":42}"#,
)?;
let event = SignedNostrEvent::try_from(sync.event.clone())?;
let received_sync = identity.decrypt_app_data_sync(&event)?;
val direct = identity.createApplicationData(
createdAt,
"com.example.settings",
"""{"enabled":true}""",
)
val receivedDirect = SoftchatEvents.applicationData(direct.event)
val sync = identity.createAppDataSync(
createdAt + 1,
AppDataContext.READ_STATE,
"""{"lastRead":42}""",
)
val receivedSync = identity.decryptAppDataSync(sync.event)
Kind 30078 is direct signed application data. Kind 30079 is self-encrypted
cross-device state. Enqueue or ingest the verified signed event through
SoftchatAccount; Rust commits and derives/decrypts the projection
atomically.
let direct = try identity.createApplicationData(
createdAt: createdAt,
identifier: "com.example.settings",
content: #"{"enabled":true}"#
)
let receivedDirect = try SoftchatEvents.applicationData(direct.event)
let sync = try identity.createAppDataSync(
createdAt: createdAt + 1,
context: .readState,
json: #"{"lastRead":42}"#
)
let receivedSync = try identity.decryptAppDataSync(sync.event)
const direct = identity.createApplicationData(
createdAt,
"com.example.settings",
"{\"enabled\":true}",
);
const receivedDirect = SoftchatEvents.applicationData(direct.event);
const sync = identity.createAppDataSync(
createdAt + 1,
"readState",
"{\"lastRead\":42}",
);
const receivedSync = identity.decryptAppDataSync(sync.event);
Raw values are permanent evidence
Typed results are views. Preserve the raw rumor and unknown tags/JSON so a future SDK can re-project the same authenticated input without losing information. Never construct a replacement raw event from a typed view.