Skip to main content

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

Operation contractchat.message

Create 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
LocalIdentity::create_chat_messageclassify_chat_message

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

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)?;

Subjects

Operation contractchat.subject

Create 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
LocalIdentity::create_subjectclassify_subject

Subjects use the canonical subject tag. During icon migration the writer emits matching imeta and image tags.

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)?;

Reactions

Operation contractchat.reaction

Create 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
LocalIdentity::create_reactionclassify_reaction

Reactions bind the parent event ID, author, and kind. A custom shortcode also requires its bounded HTTPS emoji URL.

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)?;

Deletions

Operation contractchat.deletion

Create 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
LocalIdentity::create_deletionclassify_deletion

Only the original author can delete its events. Projection code applies valid deletion before every edit and never rewrites the original rumor.

let deletion = identity.create_deletion(
vec![recipient], vec![original_id], "retracted".into(), created_at,
)?;
let received = classify_deletion(deletion.rumor)?;

Edits

Operation contractchat.edit

Create 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
LocalIdentity::create_editclassify_edit

An edit replaces text and emoji only. Author validation, (createdAt, eventID) ordering, and deletion precedence are deterministic.

let edit = identity.create_edit(
vec![recipient], original_id, "updated".into(), vec![], created_at,
)?;
let received = classify_edit(edit.rumor)?;

Typing indicators

Operation contractchat.typing

Create 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
LocalIdentity::create_typingclassify_typing

Typing is kind 21234 and must be wrapped as ephemeral kind 21059. Do not store it as durable conversation history.

let typing = identity.create_typing(vec![recipient], created_at)?;
let received = classify_typing(typing.rumor)?;

Generic repost compatibility

Operation contractchat.generic-repost

Classify 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
classify_generic_repost

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

let repost = classify_generic_repost(signed_repost)?;
let embedded = repost.embedded_event;

Metadata and contacts

Operation contractaccount.metadata

Create 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
LocalIdentity::create_user_metadataLocalIdentity::create_follow_list

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

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.

Application data

Operation contractaccount.application-data

Create 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
LocalIdentity::create_application_dataLocalIdentity::create_app_data_sync
  • Kind 30078 represents namespaced application data.
  • Kind 30079 is signed, self-encrypted cross-device app-data synchronization.
  • decryptAppDataSync authenticates 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.

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)?;

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.