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
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.
- Rust
- Kotlin
- 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)
The Android API is the same. Keep waveform samples in the local Room/file model; only bounded interoperable attachment metadata belongs in the rumor.
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);
Other chat writers and readers
The same author/classify pattern applies to:
| Family | Writer | Reader | Important rule |
|---|---|---|---|
| Subject | createSubject | subject classifier | canonical subject; icon temporarily emits matching imeta and image |
| Reaction | createReaction | reaction classifier | parent e, p, and k; optional custom emoji |
| Deletion | createDeletion | deletion classifier | author validation; deletion wins over edits |
| Edit | createEdit | edit classifier | replaces text/emoji only; deterministic latest ordering |
| Typing | createTyping | typing classifier | kind 21234 rumor in an ephemeral wrapper |
All writers require caller-supplied timestamps. Participants and references are validated canonical identifiers, not arbitrary strings.
- Rust
- Kotlin
- Android
- Swift
- TypeScript
Native functions are create_subject, create_reaction, create_deletion,
create_edit, create_typing, and the matching classify_* functions.
Writers are SoftchatIdentity extensions. Readers are grouped under
SoftchatEvents, keeping discovery separate from the identity capability.
Apply edits and deletions in the database projection transaction. Do not overwrite the original raw rumor or infer delivery from the projection.
Writers are SoftchatIdentity methods. Readers are static operations on
SoftchatEvents.
Writers are SoftchatIdentity methods; frozen projection readers are grouped
under SoftchatEvents.
Metadata and contacts
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.
Foreign hosts apply their documented (createdAt, eventID) ordering inside
the platform-owned database projection. An older event must never replace a
newer projection merely because it arrived later.
- Rust
- Kotlin
- 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)
Use the Kotlin operations above inside the account repository. Store the raw
rumor and update the latest projection in one Room transaction ordered by
(createdAt, eventID).
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
- 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
- Kotlin
- 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. Persist the verified signed event; derive/decrypt the projection after the Room transaction accepts it.
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.