Build it into an app

The CLI proved the round trip. The SDK is the same thing from your own code: connect to your tenant, sign a user in, find another, send. One surface, four languages. Below is what exists today, including the parts that are not ready.

What this takes today: every head builds from the public repository (there is no published package yet), it is one-to-one only, and it is not yet independently audited. The full list is what is not ready, below.

Pick a language

TypeScript

Browsers and Node. The same Rust client the native heads run, compiled to WebAssembly, reaching the service over WebSockets so a page needs nothing but HTTPS. Built from the repository; not on npm yet.

Swift

A Swift Package wrapping an xcframework, for macOS and iOS. Build it from the repository with build-xcframework.sh and add the package.

Kotlin

An .aar carrying the four Android ABIs. Build it with build-aar.sh and declare it as a dependency.

Rust

The client crate the CLI itself uses, and the surface every other head is generated from. The most direct path if your service is already Rust.

The shape of it

Two objects. A tenant handle is built from your API key and the server's name; it fetches the server's service document once to learn where the services are, so no app carries a host or a port, and a self-hosted deployment is just a different name. It signs users up and in. A client is one signed-in user on one device: it finds other users, sends, receives (as a batch with receive, or one message at a time as they arrive with inbound, the language's own stream), and exports its state so the same identity comes back next run. Sessions establish themselves on first contact, and the client reconnects and drains anything queued while it was away.

const tenant = await Tacenta.connect("tct_your_api_key");
const alice = await tenant.signIn("alice", "correct horse");

const bob = await alice.find("bob");
if (bob) await alice.send(bob, "hello");

for await (const m of alice.inbound()) console.log(m.from, m.text());

TypeScript shown. Every head has the same two objects; in Swift and Kotlin the handle is called Tenant, since the Swift module is itself Tacenta. Each language page shows the sample in its own spelling.

Which calls exist where

One row per call, one column per language, from the manifest every head's test suite checks itself against. A dash is a call that head does not have yet, with the reason beneath the table. This is the surface as of v1.12.1, the last release; it is generated, not maintained.

Tacenta

One tenant's handle: built from the API key and the server's name, it fetches the service document once and hands out signed-in clients.

Call RustTypeScriptSwiftKotlin
connect Tacenta::connectTacenta.connectTenant.connectTenant.connect
signUp Tacenta::sign_upTacenta.signUpTenant.signUpTenant.signUp
signIn Tacenta::sign_inTacenta.signInTenant.signInTenant.signIn
signInWithState Tacenta::sign_in_with_stateTacenta.signInWithStateTenant.signInWithStateTenant.signInWithState
signInWithStateSealed Tacenta::sign_in_with_state_sealedTenant.signInWithStateSealedTenant.signInWithStateSealed
websocket Tacenta::websocketTenant.websocketTenant.websocket
  • signInWithStateSealed: The TypeScript head has no sealed state yet.
  • websocket: TypeScript always takes the carriage, so there is no switch.

Client

One signed-in user on one device.

Call RustTypeScriptSwiftKotlin
address Client::addressClient.addressClient.addressClient.address
find Client::findClient.findClient.findClient.find
send Client::sendClient.sendClient.sendClient.send
receive Client::receiveClient.receiveClient.receiveClient.receive
inbound Client::inboundClient.inboundClient.inboundClient.inbound
restoreOutcome Client::restore_outcomeClient.restoreOutcomeClient.restoreOutcomeClient.restoreOutcome
exportState Client::export_stateClient.exportStateClient.exportStateClient.exportState
exportStateSealed Client::export_state_sealedClient.exportStateSealedClient.exportStateSealed
attachSecureStore Client::attach_secure_storeClient.attachSecureStoreClient.attachSecureStore
reconnect Client::reconnect
  • receive: Awaits the next non-empty batch on every head; inbound is the same one message at a time. A pending receive waits for mail outside the client's turn, so a send on the same client goes through meanwhile.
  • inbound: Rust: a Stream that borrows the client; TypeScript: an async iterator for `for await`; Swift: an Inbound that is an AsyncSequence; Kotlin: an Inbound with `asFlow()`. A message goes to whichever loop is running, so run one per client.
  • restoreOutcome: Only the sealed restore and a checkpoint can discard sessions, so on TypeScript, which has neither, it is fresh or resumed.
  • exportState: The bytes carry private keys: app-private, encrypted at rest, only the latest copy kept, and exported again after every send and receive (a restore of an older copy rewinds sessions and, on the sealed path, is refused).
  • exportStateSealed: No sealed state on the TypeScript head yet.
  • attachSecureStore: No secure-store hook on the TypeScript head yet.
  • reconnect: Every head reconnects on its own with backoff; only Rust exposes the call.

Inbound

A client's inbound messages one at a time, on the FFI heads; Rust's Stream and TypeScript's async iterator carry their own next.

Call RustTypeScriptSwiftKotlin
next Inbound.nextInbound.next
  • next: Rust and TypeScript iterate the stream itself; the Swift and Kotlin sugar is built on this call.

Errors

Every call raises one error type with a kind to branch on, the same kinds on every head. The message is the detail.

Kind RustTypeScriptSwiftKotlin
Network ErrorKind::Network"network"ClientError.NetworkClientException.Network
Discovery ErrorKind::Discovery"discovery"ClientError.DiscoveryClientException.Discovery
UnknownTenant ErrorKind::UnknownTenant"unknownTenant"ClientError.UnknownTenantClientException.UnknownTenant
UsernameTaken ErrorKind::UsernameTaken"usernameTaken"ClientError.UsernameTakenClientException.UsernameTaken
InvalidUsername ErrorKind::InvalidUsername"invalidUsername"ClientError.InvalidUsernameClientException.InvalidUsername
WeakPassword ErrorKind::WeakPassword"weakPassword"ClientError.WeakPasswordClientException.WeakPassword
SignUpRefused ErrorKind::SignUpRefused"signUpRefused"ClientError.SignUpRefusedClientException.SignUpRefused
SignInRefused ErrorKind::SignInRefused"signInRefused"ClientError.SignInRefusedClientException.SignInRefused
IdentityMismatch ErrorKind::IdentityMismatch"identityMismatch"ClientError.IdentityMismatchClientException.IdentityMismatch
NotFound ErrorKind::NotFound"notFound"ClientError.NotFoundClientException.NotFound
RateLimited ErrorKind::RateLimited"rateLimited"ClientError.RateLimitedClientException.RateLimited
ServerFailure ErrorKind::ServerFailure"serverFailure"ClientError.ServerFailureClientException.ServerFailure
State ErrorKind::State"state"ClientError.StateClientException.State
StoreUnavailable ErrorKind::StoreUnavailable"storeUnavailable"ClientError.StoreUnavailableClientException.StoreUnavailable
InvalidArgument ErrorKind::InvalidArgument"invalidArgument"ClientError.InvalidArgumentClientException.InvalidArgument
Internal ErrorKind::Internal"internal"ClientError.InternalClientException.Internal
  • Network: The network or the transport failed; retry later.
  • Discovery: The service document could not be fetched or read.
  • UnknownTenant: The API key selects no tenant.
  • UsernameTaken: The username is already taken in this tenant.
  • InvalidUsername: The username is not one the server accepts.
  • WeakPassword: The password is too weak.
  • SignUpRefused: A sign-up was refused for another reason: registration is closed, or the handle is reserved.
  • SignInRefused: The credentials were refused, or the session expired; coarse by design.
  • IdentityMismatch: The address is bound to a different device identity (trust on first use); also a sealed state offered to a user or device it was not sealed for.
  • NotFound: The address is not registered.
  • RateLimited: The server asked for a slower pace: too many failed sign-ins, or the recipient's queue is full. Back off and retry.
  • ServerFailure: The server could not process the request; nothing was applied. Retry.
  • State: The persisted state was refused: altered, older than the last send, or its secure-storage key is wrong; whatever else a SecureStore raises surfaces here. Do not delete the blob.
  • StoreUnavailable: The platform's secure store could not be reached (a Keychain before first unlock, a Keystore that needs the user). Nothing was refused: retry after unlock and keep the blob.
  • InvalidArgument: The caller's own input was wrong: a malformed address or config, a message over the size limit.
  • Internal: A protocol or cryptographic failure, or a bug: worth reporting.

The same grid, as the repository renders it, is at tacenta.com/dl/sdk/SURFACE.md, and the manifest itself at surface.json.

Manage your keys

The key from signup is the first one. Mint more, list what exists, or revoke one that leaked, with the CLI:

tacenta keys create --label ci
tacenta keys list
tacenta keys revoke tct_a1b2c3

Each command prompts for the email and password you set at signup, rather than reading a saved API key: it manages the tenant's actual keys, a different, tenant-admin credential from the API key context and try use, and there is no tenant session yet for it to hold instead. Nothing is written to disk. Keep that password: it is the only credential that manages keys, and there is no reset flow yet.

What is not ready

Because finding out later wastes your time:

No published package yet
Every head builds from the repository. There is no npm package, no package registry entry and no hosted binary framework, so adding Tacenta means building the artifact yourself for now.
Not yet independently audited
No independent external audit has issued a report. What is proven, what is tested and what is assumed is on the assurance page, and nothing here claims more than it does.
No password reset
Key management authenticates with the email and password from signup, and there is no reset flow yet. Lose the tenant password and recovery needs manual support, so store it where you keep other infrastructure secrets. There is also no web dashboard: rotation and revocation are CLI only.
Group messaging is not implemented
One to one only. Group messaging needs sender keys, which are specified but not built.
Delivery is to connected devices
There is no mobile push integration yet, so a device that is not connected collects its messages when it next connects rather than being woken.

Tell us what broke

The most useful thing you can send is the point where you stopped. Not a bug report, just the step: the command that failed, the doc that said the wrong thing, or the moment you could not tell whether something had worked. Mail info@natuvea.com with the step you got to. Things that worked first time are worth hearing about too, since they tell us what not to change.