Register / refresh this device's push token with the relay and the homeserver. Idempotent; safe (and recommended) to call on every launch.
Rotates the pseudonym after ~7 days or when (deviceToken, platform)
changes.
"apns" (iOS) or "fcm" (Android).
"sandbox" for debug builds, "production" for
App Store / TestFlight builds.
Re-upload the encrypted recovery blob (for instance after joining a new homeserver).
32-byte symmetric key.
Updated list of homeserver URLs.
Whether this account has a recovery blob configured on the server.
Leave this server (docs/53): leave every group hosted here, then delete the account on the server. Your identity, contacts, and other servers are unaffected. Intended for non-discovery memberships.
Delete this identity from the network as completely as the protocol allows (docs/53): leave-cascade every server, tombstone the DID in the PLC directory, then wipe all local state. Irreversible. Throws (leaving local state intact) if the PLC tombstone can't be submitted.
Snapshot of the current connection state. Non-blocking.
Opportunistically retry/validate connectivity now: wakes the reconnect loop if it's backing off, and probes a live socket for silent death. Cheap and infallible. Call on any signal that connectivity may have changed (foreground, network change, user action).
Tell the core whether the app is foreground-active. Gates the WebSocket keepalive (foreground-only, for battery); a transition to active also probes the connection. Bots can leave this untouched — the keepalive defaults on.
Block (off the event loop) until the connection state differs from
last, then return the new value. Typically used in a long-running
while loop to drive a UI indicator.
Async iterator over decrypted messages and delivery-status updates from the homeserver. The recommended receive path:
for await (const e of core.events()) {
if (e.kind === "message") console.log(e.message.body);
}
Internally drains the native batch queue and yields one event at a
time. Single-consumer: run from exactly one async loop. The iterator
runs forever; break (or return) to stop, or it ends if the channel
is closed (i.e. the AppCore is torn down).
Lower-level variant of events: block until at least one event is available, then drain and return the whole batch.
Prefer events for normal use; this is here for callers that want explicit batch-processing semantics. Same single-consumer rule applies. Throws when the event channel is closed.
Async iterator over admin-only events from the homeserver. Mirrors events but for the parallel admin queue. Only adminbot sessions ever receive admin events; for any other session this iterator hangs forever waiting on an empty queue.
for await (const e of core.adminEvents()) {
if (e.kind === "accountJoined") await invite(e.accountJoined.did);
}
Single-consumer: run from exactly one async loop. Drive it concurrently
with events via Promise.all.
Lower-level variant of adminEvents: block until at least one admin event is available, then drain and return the whole batch.
StaticcreateCreate a brand-new account on the homeserver.
Generates an identity keypair + rotation key, computes a did:plc:...,
submits the PLC genesis op, registers with the homeserver, and (if
recoveryKey is non-empty) uploads an encrypted recovery blob.
Homeserver URL.
Where to create the SQLCipher database file.
Passphrase used to derive the SQLCipher key.
32-byte symmetric key (from passkey PRF or recovery
phrase). Pass an empty Uint8Array to skip recovery
setup.
Profile display name. Encrypted under a fresh profile key and uploaded with registration.
OptionalinviteToken: stringStaticcreateRegister a brand-new bot account on the homeserver.
Bot accounts skip the PLC directory and receive a did:local:... DID
assigned by the server. displayName is stored plaintext on the server
(so bots can be looked up by name); humans use createAccount
which encrypts the display name into a profile blob instead.
No recovery blob is uploaded — bots are operator-managed and don't use the passkey recovery flow.
OptionaldidSuffix: stringOptionalinviteToken: stringStaticfinalizeFinalize registration using a previously prepared identity.
Use this when a passkey ceremony needs the DID up front: call PreparedAccount.create to compute the DID locally, register the passkey with that DID, then call this to encrypt the recovery blob, submit the PLC genesis op, and complete server registration.
The prepared handle is consumed.
OptionalinviteToken: stringStaticrecoverRecover an account from a passkey-protected recovery blob.
Downloads the encrypted blob, decrypts it with recoveryKey, restores
the identity + rotation keys into a freshly opened SQLCipher store,
registers a replacement device with the homeserver, and returns an
AppCore ready to use.
Contacts see no safety-number change because the original identity key is preserved.
StaticloginOpen an existing account from a local SQLCipher store and authenticate with the homeserver.
StaticloginOpen a bot account, registering it on first run and re-logging-in thereafter — the idempotent bootstrap an operator-run bot needs.
If the store at dbPath already holds an account, this logs in and
serverUrl / displayName / didSuffix are ignored (they were fixed by
the original registration). Otherwise — a fresh deploy, or an empty DB
left by a registration that failed after opening the store — it registers
a new bot account.
The "is the store empty?" decision is made inside the core, so callers no longer pattern-match on error strings to tell a first run from a real failure. Layer identity policy on top by checking AppCore.did against the DID you expect.
OptionaldidSuffix: stringOptionalinviteToken: stringStaticbootstrapBuild a bootstrap registration token from a shared secret (docs/24).
Pass the result as inviteToken to createBotAccount /
loginOrCreateBot to register against a closed-registration server.
Naming project links the new account into that Project — e.g. the
superuser Project (slug "adminbot") to bootstrap admin authority.
Optionalproject: stringEvery known contact, newest-interaction-first. Joins the curation flag
from contacts with the cached display name from contact_profiles.
Touch a contact row, creating it if missing.
true marks this as a deliberate gesture (sticky). Pass
false to record an interaction without curating.
Set or clear the local pending-message-request flag on a contact. app-core
no longer sets this automatically on receive; a bot that surfaces a
message-request inbox calls this when it receives a DecryptedMessage
with isRequest === true. Most bots don't track requests and can ignore
this entirely.
Show a pairing code to link a new device to this identity. Creates an
ephemeral mailbox session and returns the pairing string to render as a QR
and/or copyable code. mailboxServer defaults to this device's own server.
Follow with AppCore.linkSendBundle.
OptionalmailboxServer: stringIngest the new device's pairing code (scanned or pasted). Follow with AppCore.linkSendBundle.
Seal the identity's credentials to the new device and post them to the mailbox. Requires a prior AppCore.linkCreatePairing or AppCore.linkAcceptPairing.
Send an encrypted DM. The body is wrapped in a content envelope (with
sentAt) before encryption and fanned out to every active device of
the recipient.
Text body. Sent verbatim — UTF-8 on the wire.
OptionalsentAt: InstantSend-time stamped into the envelope. Defaults to
Temporal.Now.instant().
Send a text message to a SendTarget — a DM or a group — without
branching on the transport yourself. A "dm" target behaves exactly like
sendDm (envelope + per-device fan-out + marks the contact
curated); a "group" target behaves like sendGroupMessage (Sender
Key encryption + per-member fan-out).
Handy for replying back through whichever channel a message arrived on:
const target: SendTarget = msg.groupId
? { kind: "group", groupId: msg.groupId }
: { kind: "dm", recipientDid: msg.senderDid };
await core.send(target, "got it");
Text body. Sent verbatim — UTF-8 on the wire.
OptionalsentAt: InstantSend-time stamped into the envelope. Defaults to
Temporal.Now.instant().
Encrypt and upload an attachment blob (docs/35-attachments.md), returning the Attachment pointer to pass to sendWithAttachments. The blob is padded, CBC+HMAC-encrypted under a fresh one-off key, and uploaded; the server only ever sees ciphertext.
Send a text message with attachments and/or link previews to a
SendTarget. Attachments (and each preview's image) must already be
uploaded via uploadAttachment. body may be empty for an
attachment-only message.
OptionalsentAt: InstantDownload, verify, and decrypt an attachment blob (docs/35). Aborts before decrypting if the downloaded bytes don't match the pointer's digest.
Record that an attachment's blob was downloaded to localPath (docs/35),
so later loads render the local file instead of re-fetching.
React to a message in a DM or group (docs/33-reactions.md). One reaction
per account per message: a fresh emoji replaces any prior one; remove
clears it. Applies locally and sends a reaction control message to the
conversation.
Where the reacted-to message lives (DM peer or group).
DID of the reacted-to message's author.
sentAt of the reacted-to message — its wire identity.
The reaction emoji (ignored when remove is true).
true to clear this account's reaction on the target.
OptionalsentAt: InstantThis reaction op's timestamp. Defaults to now.
Fetch and decrypt all pending messages from the homeserver.
Most callers should use AppCore.nextEvents (the push-driven stream) instead. This is the explicit-pull variant, mainly useful for tests and one-shot tools.
Send a read receipt for a batch of messages to recipientDid. Fans
out across all of the recipient's active devices.
sentAt instants of the messages being acknowledged.
Invite recipientDid into the group with the given role. Also sends
the substrate GroupContext DM + Sender Key distribution message so
the invitee can decrypt the group on accept.
Admit a requester from pendingApprovals into the group.
Reject a requester from pendingApprovals.
Remove a member from the group.
Change a member's role (member ↔ admin).
Set the group's disappearing-messages timer in seconds (0 = off).
Admin-gated by the group's modify_expiry policy role (default admin),
so the caller must be an admin of the group.
Create a new action-bound group on this homeserver.
Disappearing-messages timer. 0 disables it.
The new groupId (URL-safe-no-pad base64) and the 32-byte
master key. Stash the master key — it's the secret an invite
link carries, and there is no way to recover it from the server.
Pull the latest decrypted group state from the homeserver.
Last-known group info from the local cache, with no server round-trip
(docs/53). Returns null if nothing is cached. Use this to show group info
for a group you've left, where AppCore.fetchGroupState would fail
the membership-gated server fetch.
Group ids of every group held locally — every group we have the master key for, including ones we were invited to (invites are auto-accepted). Reads the local group store directly, so it surfaces groups with no messages yet (unlike AppCore.loadConversations). Pair with AppCore.fetchGroupState to inspect membership and roles.
Accept a pending invite. Generates our own Sender Key and broadcasts the distribution message to every other member.
Decline a pending invite. Removes the local pending row.
Join via an invite link.
32-byte master key from the link.
Homeserver hosting the group.
Link password, or an empty Uint8Array if none.
"member" (open link, admitted) or "pending" (RequestToJoin
link, awaiting admin approval).
Cancel a pending join request we issued via AppCore.joinViaLink.
Leave a group yourself. Unlike AppCore.removeMember (an admin action), any member can leave. Removes your server-side membership and drops the group locally.
Whether the current identity is still a member of the group, per the
locally-cached state (docs/53). Returns false after leaving — the UI
uses this to hide the composer while keeping the conversation readable.
Apply any pending changes from /changes since the last applied
revision.
The new local revision (equal to the previous one if nothing was pending).
Encrypt and send a message to the group. Uses our Sender Key for symmetric encryption, then fans out per-recipient via the existing DM transport.
Text body. Sent verbatim — UTF-8 on the wire.
Generate a fresh push-routing pseudonym for this group on the server. Caller should re-register with the relay using the returned bytes.
The new pseudonym bytes.
This account's DID (did:plc:... or did:local:...).
This account's per-device id. Stable for the lifetime of the local store.
Insert or update a message in local history (SQLCipher).
Load all persisted messages for a conversation, oldest first.
Enumerate every conversation with at least one persisted message, along with its most recent message. Sorted newest-first.
Most recent message for a conversation, or null if it has none.
Mark every message in the conversation with sentAt ≤ upTo as read.
Number of rows newly marked.
Count unread messages in a conversation.
This user's own display name from the local profile cache. Empty string until a profile has been set.
Update the user's display name. Re-encrypts and uploads the profile blob, then updates the local cache.
Cached display name for a contact. Empty string if no profile has been fetched yet for this DID (caller should fall back to the DID).
Re-fetch a contact's encrypted profile and update the local cache.
true if the cached display name changed.
Prime the contact-profile cache with metadata extracted from an invite token (so the auto-DM to the inviter shows their name from the first frame). Call right after AppCore.createAccount when an invite was accepted.
Fetch the sender's encrypted profile blob using the profileKey from an
inbound DecryptedMessage, decrypt it, and cache the display name
locally. app-core does NOT do this automatically on receive (it would
block on the network and persist a name most bots never need). Best-effort
and idempotent. Blocks on the network.
List the Projects installed on this homeserver.
Request a short-lived token for opening a Project's webview / API.
Project login ("Sign in with Avalanche"), same-device front-end (docs/25).
After the user consents in-app, mint an OAuth authorization code bound to
this account and the PKCE challenge. Redirect the browser to
redirectUri?code=<returned>&state=.... codeChallengeMethod is normally
"S256".
Optionalscope: stringProject login, cross-device front-end (docs/25). After the user consents
in-app to a login started on another device, approve the pending
userCode. Returns the Project URL just signed in to.
The main client handle. Wraps a SQLCipher-backed local store, a HTTP/WS client for the homeserver, and a background reconnect task.
Construct via one of the four static factories (AppCore.createAccount, AppCore.finalizeAccount, AppCore.recoverFromBlob, AppCore.login). Each instance spawns a background task that owns the WebSocket lifecycle and pushes decrypted messages + delivery-status updates into the AppCore.nextEvents queue.
All methods are safe to call concurrently. Async methods run on the napi libuv threadpool so they do not block the JS event loop.