48 KiB
Iota Decentralization Implementation Guide
Scope
The implementation should proceed in three stages:
- Separate Iota responsibilities so decentralized routing, foreign users, and Communities can use common identity, authentication, session, storage, and routing services.
- Implement decentralized operation without requiring an Omega or Omikron.
- Compose centralized and decentralized providers into hybrid mode.
This guide does not include:
- UI changes.
- Add Conversation UI changes.
- Browser bootstrap changes.
- Static webclient hosting on the Iota.
- Reimplementation of Communities themselves.
The networking endpoint required for a client to connect directly to an Iota is in scope because decentralized mode requires it. Serving the webclient assets from that endpoint is not.
The old iota/communities crate should be treated as a prototype, not as the implementation base. It is excluded from the workspace and its authentication and storage model do not fit the new architecture.
Look at MTP when deciding protocol / connection details.
Stage 1: Separate the Iota architecture
Goal
After Stage 1, the Iota should still operate in centralized mode, but core Iota logic should no longer depend directly on OmikronConnection.
The architectural target is:
+--------------------+
| Daemon / IPC |
+----------+---------+
|
+-----------------+-----------------+
| |
+-------v--------+ +-------v--------+
| AccountService | | SessionManager |
+-------+--------+ +-------+--------+
| |
+-------v--------+ +-------v--------+
| IdentityService|<---------------->| AuthService |
+-------+--------+ +----------------+
|
+---------+----------+
| |
+-------v-------+ +-------v-------+
| Local users | | Foreign users |
| and accounts | | and principals|
+---------------+ +---------------+
+-------------------+
| RelayService |
+---------+---------+
|
+---------v---------+
| PeerRouter |
+---------+---------+
|
+------------------+------------------+
| |
+-------v---------+ +------v------+
| Omikron adapter | | future peer |
| centralized | | transports |
+-----------------+ +-------------+
OmikronConnection becomes one provider attached to these abstractions. It stops implementing identity resolution, account lifecycle, relay processing, routing, and client delivery itself.
1. Introduce canonical identity types
The current system treats numeric user_id values as globally meaningful because Omega allocates them.
That assumption must stop at the domain layer.
Create a small identity module or crate, preferably iota-identity, containing types with no network or database dependencies.
Suggested concepts:
pub struct IotaNodeId(/* hash/fingerprint of Iota public identity */);
pub struct AuthorityId(/* stable cryptographic authority identity */);
pub enum AuthorityKind {
Iota,
Omega,
}
pub struct PrincipalId {
pub authority: AuthorityId,
pub user_id: u64,
}
pub enum UserSelector {
UserId(u64),
Username(String),
}
pub struct UserAddress {
pub selector: UserSelector,
pub public_key_pin: Option<PublicKeyBundle>,
pub authority: Option<AuthorityLocator>,
}
A username is an alias used for lookup. It must not become the network identity.
A public key supplied through an address is a verification pin. It must not replace the authority and user ID pair as the canonical identity because keys need to be rotatable.
For example:
alice@example.org
might resolve to:
AuthorityId = Iota abc123...
UserId = 51
Username = alice
Key = K1
The canonical identity is:
abc123... / 51
If alice later changes display name or rotates keys, existing chats and community memberships still reference the same principal.
Numeric IDs must become local identifiers
Keep the existing local UserProfile.user_id.
It remains useful as the local account identifier.
Do not use it as a network-wide user identity after this boundary.
Code should make the distinction visible:
pub struct LocalUserId(pub i64);
pub struct PrincipalHandle(pub i64);
Using separate Rust types is preferable to aliases because accidentally passing a local user ID into a federated lookup should become a compile-time error where practical.
2. Add a principal directory separate from hosted users
Do not place users from another Iota or Omega into the existing users table.
Current code uses:
user_manager::get_user(id).is_some()
as a locality test in relay handling.
If foreign users are inserted into that table, the Iota will incorrectly conclude that they are hosted locally.
Create a separate principal directory.
A useful model is:
principals
----------
principal_pk
authority_kind
authority_id
remote_user_id
username
display_name
current_public_key
descriptor_revision
descriptor_valid_until
last_resolved_at
UNIQUE(authority_id, remote_user_id)
Key history should be stored separately if rotation is supported:
principal_keys
--------------
principal_pk
public_key
valid_from
valid_until
source_revision
Aliases can also be separate:
principal_aliases
-----------------
principal_pk
authority_locator
username
principal_pk is an Iota-local database handle. It exists so existing tables do not have to carry large cryptographic authority identifiers everywhere.
Then expose distinct storage services:
trait LocalUserStore {
fn get_local_user(...);
fn get_local_user_by_username(...);
fn is_hosted_here(...);
}
trait PrincipalStore {
fn get_principal(...);
fn get_by_canonical_id(...);
fn upsert_remote_descriptor(...);
fn signing_keys(...);
}
A locally hosted account should also have a corresponding principal record.
That produces one identity namespace for chats, relays, Communities, ACLs, and foreign connections without pretending every principal is hosted locally.
3. Migrate references to remote users toward PrincipalHandle
Several current tables use numeric remote user IDs directly.
The main examples include:
contacts.user_idmessages.external_user- relay replay state
- relay inbox state
- queued relay identity
- blocked users
- receipt policy
- chat secrets and other per-peer state
Do not attempt a complete database rewrite in one commit.
Introduce repository APIs that accept PrincipalHandle, then migrate consumers behind those APIs.
For centralized users already stored by numeric Omega user ID, Stage 1 migration can create principal records representing:
(current Omega authority, existing numeric user ID)
This preserves existing behavior while removing the assumption that the number alone identifies the user.
storage_owner can remain a local user ID because only a locally hosted account owns local account storage.
The important separation is:
storage_owner -> LocalUserId
external_user -> PrincipalHandle
4. Build IdentityService
Move all user identity lookup behind one service.
Suggested interface:
#[async_trait]
pub trait IdentityResolver: Send + Sync {
async fn resolve_address(
&self,
address: &UserAddress,
context: &ResolutionContext,
) -> Result<ResolvedPrincipal, IdentityError>;
async fn resolve_principal(
&self,
principal: &PrincipalId,
) -> Result<ResolvedPrincipal, IdentityError>;
async fn signing_keys(
&self,
principal: &PrincipalId,
) -> Result<Vec<PublicKeyBundle>, IdentityError>;
}
ResolvedPrincipal should contain at least:
pub struct ResolvedPrincipal {
pub principal: PrincipalId,
pub handle: PrincipalHandle,
pub username: Option<String>,
pub public_keys: Vec<PublicKeyBundle>,
pub home: PrincipalHome,
}
During Stage 1, implement:
LocalIdentityResolver
OmegaIdentityResolver
OmegaIdentityResolver wraps the behavior currently embedded in:
OmikronConnection::resolve_relay_signing_keys
OmikronConnection::hosting_iota_for_user
GetUserData
The rest of the Iota must stop issuing GetUserData itself.
5. Separate account lifecycle from Omega
The daemon command router currently calls:
omikron_connector::user_ops::create_user
omikron_connector::user_ops::attach_user_from_tu
omikron_connector::user_ops::inspect_tu_credential
omikron_connector::user_ops::get_remote_user_assignment
directly.
Replace that dependency with an account service.
For example:
#[async_trait]
pub trait AccountAuthority: Send + Sync {
async fn create_user(&self, request: CreateUserRequest)
-> Result<LocalAccount, AccountError>;
async fn inspect_credential(&self, credential: &[u8])
-> Result<CredentialPreview, AccountError>;
async fn attach_user(&self, credential: &[u8])
-> Result<LocalAccount, AccountError>;
async fn reconcile_user(&self, user: LocalUserId)
-> Result<ReconcileResult, AccountError>;
async fn release_user(&self, user: LocalUserId)
-> Result<(), AccountError>;
}
Stage 1 provides:
OmegaAccountAuthority
which internally calls the current omikron_connector::user_ops.
Stage 2 will add:
LocalIotaAccountAuthority
The IPC command router should depend only on AccountAuthority.
This removes a large amount of mode-specific logic from iota-daemon-lib/src/command_router.rs.
6. Extract relay processing from OmikronConnection
This is the most important Stage 1 refactor.
OmikronConnection::handle_relay() currently performs several unrelated responsibilities:
- relay metadata verification
- signer lookup
- local/remote user classification
- replay reservation
- policy validation
- content opening
- message application
- outgoing history updates
- relay queue insertion
- forwarding
- local client event generation
- success/error responses
Move these into a transport-independent RelayService.
Conceptually:
pub struct RelayService {
identities: Arc<dyn IdentityResolver>,
local_users: Arc<dyn LocalUserStore>,
principals: Arc<dyn PrincipalStore>,
router: Arc<dyn PeerRouter>,
sessions: Arc<SessionManager>,
storage: Arc<RelayStorage>,
iota_identity: Arc<IotaIdentity>,
}
Ingress becomes:
relay_service
.accept_relay(IngressSource::Omikron(...), frame)
.await;
Later:
relay_service
.accept_relay(IngressSource::Peer(...), frame)
.await;
and:
relay_service
.accept_relay(IngressSource::HostedClient(...), frame)
.await;
The handler should not care how the relay reached the Iota.
Preserve the existing relay machinery
Do not rewrite the following unless the identity migration requires it:
- durable pending relay queue
- relay inbox
- replay lifecycle
- delivery acknowledgements
- sealed relay verification
- message application
- outgoing message history policy
The current implementation already has the difficult storage and retry behavior.
The purpose of Stage 1 is to move it behind a reusable service.
7. Separate relay transport from relay semantics
ConnectionHandler already describes itself as supporting Omikron, direct, and future modes, but its current return-position impl Future API is inconvenient for heterogeneous dynamically selected connections.
Change it to an object-safe asynchronous interface.
For example:
#[async_trait]
pub trait ConnectionHandler: Send + Sync {
async fn send_message(
&self,
value: &CommunicationValue,
) -> Result<(), ConnectionError>;
async fn await_response(
&self,
value: &CommunicationValue,
timeout: Option<Duration>,
) -> Result<CommunicationValue, ConnectionError>;
async fn is_connected(&self) -> bool;
async fn is_identified(&self) -> bool;
async fn stop(&self);
}
Then introduce the higher-level routing interface separately:
#[async_trait]
pub trait PeerRouter: Send + Sync {
async fn send_to_iota(
&self,
destination: &IotaNodeId,
frame: CommunicationValue,
) -> Result<DeliveryReceipt, RouteError>;
}
ConnectionHandler represents one connection.
PeerRouter answers the question:
How do I reach this Iota?
Do not combine those two responsibilities.
8. Replace mandatory Omikron in DaemonServices
Current:
pub struct DaemonServices {
pub omikron: Arc<dyn OmikronClient>,
...
}
Target:
pub struct DaemonServices {
pub accounts: Arc<dyn AccountAuthority>,
pub identities: Arc<dyn IdentityResolver>,
pub principals: Arc<dyn PrincipalStore>,
pub local_users: Arc<dyn LocalUserStore>,
pub relay: Arc<RelayService>,
pub router: Arc<dyn PeerRouter>,
pub sessions: Arc<SessionManager>,
pub auth: Arc<AuthService>,
pub centralized: Option<Arc<CentralizedServices>>,
}
OmikronClient can remain available inside the centralized adapter for Omega-specific administration.
Core handlers should not receive it.
Daemon startup must also stop treating construction of an Omikron connection as a prerequisite for normal Iota construction.
During Stage 1, centralized mode still creates it, but service composition should look like:
construct storage
construct identity services
construct auth
construct session manager
construct Omikron adapter
construct centralized peer router
construct relay service
construct account service
start daemon
instead of:
connect Omikron
build everything around Omikron
9. Build one session model for hosted and foreign clients
Communities require users from other authorities to connect directly to an Iota without becoming hosted accounts on that Iota.
Create a general session manager rather than another community-specific connection implementation.
Suggested distinction:
pub enum SessionIdentity {
Hosted {
local_user: LocalUserId,
principal: PrincipalHandle,
},
Foreign {
principal: PrincipalHandle,
},
}
A connection then has:
pub struct AuthenticatedSession {
pub connection_id: Uuid,
pub identity: SessionIdentity,
pub capabilities: SessionCapabilities,
}
Capabilities should determine what the connection can do.
A hosted client could receive capabilities such as:
AccountData
Messaging
Settings
LocalStorage
Communities
A foreign client connected to a Community Iota might receive only:
Community(<community-id>)
The foreign session must not gain access to:
- local account state
- local user settings
- hosted user message history
- local user management
- other Communities
- arbitrary relay origination unless explicitly allowed
This makes Communities an authorization problem rather than a parallel networking stack.
10. Repurpose iota-auth
iota-auth currently appears detached from the working architecture.
Use it for reusable principal authentication.
It should support:
HostedAccountAuthenticator
ForeignPrincipalAuthenticator
IotaPeerAuthenticator
Foreign principal authentication
A foreign user should be able to connect to the Iota hosting a Community without becoming a user hosted by that Iota.
The flow should be:
client -> identify as PrincipalId or UserAddress
Iota -> load cached principal or resolve authority
Iota -> obtain trusted public key history
Iota -> issue challenge
client -> prove possession of matching private key
Iota -> create Foreign session
The existing MTP authentication facilities can be used where appropriate.
Do not make Communities implement another hand-written WebSocket challenge protocol like the old excluded prototype.
Authentication without contacting the home authority every time
The foreign user's home Iota or Omega should not have to be online for every Community connection.
On initial resolution, store an authority-authenticated user descriptor and key material locally.
Subsequent authentication can use the cached trusted key while that descriptor remains valid under its authority rules.
If there is no trusted cached descriptor and the authority cannot be reached, authentication must fail closed.
This gives Communities the required property:
Foreign user connects directly to Community Iota
|
+-> Community Iota verifies their existing identity
|
+-> user is not hosted on Community Iota
11. Prepare Communities around principals
Do not implement Community behavior yet.
Only establish interfaces Communities will later consume.
Community membership should eventually reference:
PrincipalHandle
not:
UserProfile
LocalUserId
raw username
Community ACL logic should be able to ask:
session.principal()
community_membership.has_permission(principal, permission)
A Community handler should not need to know:
- which Omega the user belongs to
- which Iota hosts the user
- whether the current connection came through centralized or decentralized networking
- how the user's public key was resolved
That information belongs below the Community layer.
12. Stage 1 acceptance criteria
Stage 1 is complete when all of the following hold:
- Existing centralized users continue to work.
- Existing Omikron relay traffic enters
RelayService. RelayServicedoes not depend onOmikronConnection.- User creation from the daemon goes through
AccountAuthority. - Core identity lookup goes through
IdentityResolver. - Remote users are represented independently from hosted users.
- A foreign principal can be authenticated without being inserted into the hosted
userstable. - Session authorization distinguishes hosted and foreign clients.
- Two users with the same numeric user ID under different authorities are different principals.
- Daemon service construction no longer exposes Omikron as the primary general-purpose service.
- The future Communities implementation can identify an authenticated remote participant using only a
PrincipalHandle.
At the end of Stage 1, behavior should still be centralized. The architecture should no longer be centralized.
Stage 2: Implement decentralized mode
Goal
A deployment consisting of:
Client A
Iota A
Client B
Iota B
must be able to create local accounts, authenticate clients, resolve remote users, and exchange messages without either Iota belonging to an Omega or connecting to an Omikron.
If direct Iota-to-Iota connectivity is impossible, an independent relay/router may be used.
That relay is infrastructure, not an Omega or Omikron.
1. Give every Iota a standalone cryptographic identity
The existing Omega-assigned iota_id cannot be the decentralized node identity.
Create:
pub struct IotaNodeId(...);
derived from the persistent Iota public identity.
The exact representation should be stable across restarts and independent from IP address, hostname, Omega membership, or Omikron assignment.
An Iota can then simultaneously have:
IotaNodeId
legacy Omega Iota ID
network endpoints
relay endpoints
Only IotaNodeId is fundamental.
The Omega-assigned ID becomes centralized compatibility metadata.
Rotating the node keyring changes IotaNodeId. The decentralized protocol must
treat this as creating a new Iota identity, not as rotating operational keys
under an existing node identity.
Implementation status:
- Persist the Iota keyring through
LocalNodeIdentity, outside the Omikron connector. - Derive a versioned
IotaNodeIdfrom the canonical public key bundle. - Derive the decentralized
AuthorityIdfromIotaNodeId. - Reject arbitrary unversioned
IotaNodeIdstrings. - Load
LocalNodeIdentityduring standalone daemon startup.
2. Define decentralized user identity
A locally created decentralized user becomes:
PrincipalId {
authority: this Iota's AuthorityId,
user_id: locally allocated ID
}
Two Iotas can both contain local user 42 without collision:
Iota A / 42
Iota B / 42
The user's public key is attached to the principal descriptor.
It is not necessary for local numeric IDs themselves to be globally unique.
Finish application storage migration
Before decentralized relays enter normal chat storage, contacts and messages must use canonical principals as their authoritative external identity.
Migrate contact uniqueness from:
(storage_owner, user_id)
to:
(storage_owner, principal_handle)
Migrate message, receipt, reaction, block, chat-secret, notification, and sync
lookups toward PrincipalHandle or PrincipalId. Keep remote numeric user IDs
only as legacy protocol metadata.
The existing client may continue to use an Iota-local stable contact or chat ID.
The backend maps that local ID to PrincipalHandle; it must not expose a remote
authority's numeric user ID as the cross-authority identity.
Implementation status:
- Store contacts uniquely by
PrincipalHandle. - Treat
messages.external_principalas authoritative. - Migrate the remaining persisted peer-identity lookups.
- Verify that two authorities can each provide numeric user ID
7to one local user.
3. Implement the address parser
Support a generic form:
<selector>[::<public-key>][@<authority>]
Implementation status:
- Parse username and numeric selectors with optional key pins and authorities.
- Normalize domain names, IP literals, ports, and bracketed IPv6 authorities.
- Reject empty fields, malformed key pins, and extra authority delimiters.
- Discover authority identity and service type through
/.well-known/tensamin.
Examples:
alice
51
alice@192.0.2.20
51@iota.example.org
alice::KEY@iota.example.org
51::KEY@omega.example.org
alice@omega.example.org
The selector is:
username
or
numeric user ID
The optional public key is a pin.
The optional authority selects the resolver.
No @
If no @ is supplied, use the account's default authority.
For a decentralized account:
own Iota
For a centralized account:
own Omega
Hybrid behavior is handled in Stage 3.
Literal IP
Treat a literal IP authority as an Iota endpoint.
The connection must still authenticate the remote Iota key and obtain its IotaNodeId.
Domain
A domain needs service discovery because:
alice@example.org
does not itself reveal whether example.org is an Iota or Omega.
Define a small discovery document, for example:
/.well-known/tensamin
An Iota response should expose:
service type
protocol versions
IotaNodeId
public key
direct endpoints
relay hints
An Omega response should identify itself as an Omega and expose its public authority identity and supported lookup protocols.
The authority type is discovered, not guessed from the username syntax.
Public-key pin
For:
51::KEY@omega.example.org
resolution should:
query authority
resolve user 51
obtain trusted key
compare against KEY
reject on mismatch
The supplied key must not silently override authority data.
4. Define signed user descriptors
Remote user resolution needs a transferable authenticated record.
For example:
pub struct UserDescriptor {
pub principal: PrincipalId,
pub username: String,
pub display_name: Option<String>,
pub public_keys: Vec<PublicKeyBundle>,
pub home_iota: IotaNodeId,
pub revision: u64,
pub valid_until: Option<Timestamp>,
pub authority_signature: Signature,
}
For an Iota-native user, the hosting Iota signs the descriptor.
For an Omega-native user, the Omega signs the descriptor and identifies the user's hosting Iota.
The local Iota verifies the descriptor before inserting it into PrincipalStore.
Keep the untrusted signed wire type separate from the verified descriptor stored
by PrincipalStore. Network code must not construct the trusted storage type
without signature, authority, revision, and validity checks.
This descriptor becomes the basis for:
- remote key lookup
- foreign Community authentication
- route lookup
- contact information
- key rotation
- cached offline authentication
Key rotation will use a separate authority-signed key-history record. Principal
descriptors contain only keys valid for new signatures at descriptor issue
time. PrincipalStore closes prior key validity when it accepts a newer
descriptor, so old keys are not trusted indefinitely. Add the signed history
record before accepting signatures created before the current descriptor.
5. Implement direct client-to-Iota sessions
The current client/src/client_connection.rs already contains much of the client-side server logic, but it explicitly rejects:
Relay
AccountStateRequest
AccountStateApplied
MessageSend
where centralized infrastructure currently owns those paths.
Stage 2 should make these call the common services introduced in Stage 1.
A hosted client connection should become:
MTP transport
|
v
hosted-user authentication
|
v
SessionManager
|
+-> account state
+-> RelayService
+-> message handlers
+-> settings/storage
Implement a dedicated network gateway for this purpose.
Do not make static webclient serving part of this work.
The gateway can share MTP infrastructure with existing components, but its responsibility is only:
accept network connection
authenticate
create session
forward messages into Iota services
6. Implement local account creation
Add:
LocalIotaAccountAuthority
User creation becomes:
validate username
allocate local user ID
generate user keyring
persist UserProfile
create local Principal record
persist credential
return account
No Omega request occurs.
The account's authority is the local Iota.
Version .tu credentials
Current .tu credentials contain Omega-specific assumptions.
Introduce a versioned credential format capable of representing:
credential version
PrincipalId
authority descriptor
user keyring
Legacy Omega credentials must remain parseable.
Do not reinterpret existing credentials as decentralized credentials.
A credential needs to state which authority owns the account.
7. Implement other-iota
iota/other-iota currently provides the peer connection and router interfaces,
an in-memory DirectPeerRouter, and a PeerRelayAdapter. It does not yet create
network connections.
Its responsibility should be Iota peer connectivity, not general message semantics.
Suggested components:
PeerManager
PeerConnection
PeerAuthenticator
PeerDiscovery
PeerConnectionPool
The handshake should authenticate the remote Iota using its node key.
A successful peer connection becomes keyed by:
IotaNodeId
not hostname or legacy numeric Iota ID.
Before implementing the handshake, define relay provenance rules. A direct peer that originates a user relay should authenticate as the signer's home Iota. Infrastructure that forwards opaque frames needs a separate ingress role and authorization policy.
For example:
pub enum PeerIngressSource {
DirectPeer { node_id: IotaNodeId },
RelayRouter { router_id: RelayRouterId },
}
A valid user signature proves authorship. It does not prove that the connected Iota is authorized to originate that network hop.
Direct path
The basic path becomes:
Iota A
|
| authenticated peer connection
v
Iota B
The receiving peer passes relay frames into:
RelayService
It does not contain another copy of relay handling logic.
8. Replace numeric decentralized relay identities
The current relay format uses bare numeric:
signer_id
final_recipient_id
Iota route ID
That is insufficient once multiple independent authorities exist.
Introduce a federated relay version containing canonical principals.
Conceptually:
pub struct FederatedRelayIdentity {
pub signer: PrincipalId,
pub recipient: PrincipalId,
}
The route must identify an Iota using:
IotaNodeId
not the existing Omega-assigned numeric Iota ID.
Do not truncate a key fingerprint to fit the existing numeric route field.
Version the protocol instead.
Compatibility
Keep legacy relay parsing for centralized traffic.
Normalize both forms into an internal representation:
pub struct VerifiedRelayEnvelope {
pub signer: ResolvedPrincipal,
pub recipient: ResolvedPrincipal,
pub message_id: RelayMessageId,
pub created_at: Timestamp,
pub content: VerifiedRelayContent,
pub compatibility: RelayCompatibilityData,
}
The common representation must not require legacy_iota_id or read canonical
identity from V1 fields such as verified.context.signer_id and
verified.context.final_recipient_id. LegacyRelayDecoder owns those fields and
places any required numeric data in optional compatibility metadata.
The common service returns domain results rather than encoded V1 frames:
pub struct RelayOutcome {
pub ingress_response: RelayResponse,
pub local_deliveries: Vec<ClientDelivery>,
}
The ingress adapter encodes ingress_response. A local client-event sink or
adapter sends local_deliveries. A peer adapter must not call into_frames() and
send local client events back to the remote Iota.
For a legacy relay:
numeric user ID + configured Omega authority
becomes a canonical PrincipalId.
For a decentralized relay, the canonical identity is already encoded.
From that point onward, storage and application logic should use the normalized representation.
9. Migrate replay protection
Current replay protection effectively keys on:
signer_id
message_id
This becomes unsafe with independent user namespaces.
Change it to:
PrincipalId
message_id
or its stable local PrincipalHandle.
The same applies to:
relay_inboxRelayIdentity- pending relay ownership
- outgoing relay bookkeeping
This migration is required before two authorities can safely contain the same numeric user ID.
Implementation status:
- Key replay identity by signer principal and message ID.
- Store principal-aware relay identity and decentralized queue destinations.
- Complete the application-storage migration described under decentralized user identity.
10. Implement decentralized routing
PeerRouter should resolve a destination principal to its home Iota:
PrincipalId
|
v
IdentityService
|
v
UserDescriptor.home_iota
|
v
PeerRouter
Then the router selects a transport:
direct connection
or
external relay/router
The application layer should not know which was selected.
Move durable retry execution out of OmikronConnection into a common
PendingRelayDispatcher. It consumes RelayTarget and uses PeerRouter for
both legacy Omega Iotas and decentralized IotaNodeId destinations.
The RelayTarget::Iota(_) branch must deliver queued relays and preserve restart
recovery semantics before direct peer messaging is complete.
Implementation status:
- Resolve
PrincipalHomeinto centralized or decentralized route destinations. - Persist
IotaNodeIdand legacy Omega Iota destinations separately. - Route through the
PeerRouterabstraction. - Dispatch pending decentralized relays through
PeerRouter.
11. Add an independent Iota relay/router
Two Iotas behind networks without inbound connectivity cannot guarantee direct communication using only those two machines.
Provide a separate relay service.
Its responsibility should be intentionally narrow:
authenticate Iota nodes
associate IotaNodeId with active connection
accept opaque frame for destination IotaNodeId
forward frame
It should not:
- allocate users
- resolve usernames
- host user data
- issue user identities
- decrypt sealed user relays
- become an Omega
- become an Omikron
The topology becomes:
Iota A ---- outbound ----+
|
v
Relay/router
|
+---- active connection ---- Iota B
Both Iotas can establish outbound connections to it.
The existing origin Iota relay queue should remain responsible for durable retries.
The router can remain primarily a live forwarding service.
Multiple routers
An Iota descriptor can advertise multiple relay hints.
A PeerRouter can attempt:
existing direct connection
known direct endpoint
configured/advised relay
without changing the message handler.
Implementation status:
- Authenticate router and Iota connections with pinned public keys.
- Register live Iota connections by
IotaNodeId. - Forward opaque Relay V2 frames and return destination acknowledgements.
- Mark destination ingress as
IngressSource::RelayRouter. - Provide direct-first routing with relay fallback.
- Add deployment configuration for explicit router endpoint, public-key, and TLS certificate pins, and publish configured endpoints as signed relay hints.
12. Support Omega addresses without requiring a local Omega
A decentralized Iota should be able to resolve:
alice@omega.example.org
without itself being registered with that Omega.
Add an OmegaAuthorityResolver.
Its responsibility is only to query the explicitly addressed Omega.
For example:
local decentralized Iota
|
| public identity query
v
omega.example.org
|
v
signed UserDescriptor
The remote Omega must expose enough information to obtain:
remote user's canonical Omega principal
remote user's public keys
hosting Iota
hosting Iota decentralized identity/endpoints
This does not make Omega mandatory for decentralized mode.
It means an address explicitly naming an Omega depends on that remote authority for initial resolution.
If the descriptor is cached, future identity verification can use the cached material according to its validity rules.
13. Fetch and cache remote public user data
Remote user fetching should use IdentityResolver, not message handlers.
A lookup can populate:
PrincipalId
username
display name
public key history
home Iota
descriptor revision
descriptor validity
Do not cache arbitrary private account data from another Iota.
The distinction should remain:
PrincipalStore = public identity information
LocalUserStore = accounts hosted by this Iota
14. Foreign client connections
Stage 2 should also complete the foreign-session path prepared for Communities.
Example:
User Alice hosted by Iota A
Alice's client
|
| connects directly
v
Iota B hosting a Community
Iota B performs:
parse Alice's principal
load or resolve Alice's trusted descriptor
issue authentication challenge
verify proof using Alice's public key
create Foreign session
Alice does not become a hosted user on Iota B.
Iota A does not need to proxy the connection.
If Iota B already has a valid cached descriptor, Iota A does not need to participate in that login.
This provides the identity/session foundation Communities need before Communities themselves are implemented.
15. Decentralized startup
The daemon must start normally with:
no omikron_host
no omikron_port
no omikron_id
no Omega-issued iota_id
The mandatory identity becomes the local Iota keypair and derived IotaNodeId.
Startup should construct:
LocalIotaAccountAuthority
LocalIdentityResolver
RemoteIotaResolver
OmegaAuthorityResolver
DirectPeerTransport
RelayPeerTransport
RelayService
SessionManager
The Omikron connector should not be constructed.
16. Stage 2 acceptance tests
The following integration tests should exist before calling decentralized mode complete.
Independent namespaces
Create:
Iota A / user 1
Iota B / user 1
Verify that the users remain separate throughout:
contacts
relay verification
messages
blocking
replay protection
foreign sessions
Direct messaging
Run two Iotas without Omega or Omikron.
Create one local user on each.
Resolve the remote address.
Exchange messages over direct Iota-to-Iota transport.
Restart both Iotas and verify stored identity and relay state remain valid.
Relay messaging
Place the Iotas behind conditions where the test only exposes outbound connectivity through the independent relay service.
Verify the same relay reaches the destination.
No application handler should change between direct and relayed delivery.
Address resolution
Test:
username@iota-ip
userid@iota-domain
username@iota-domain
userid::public-key@iota-domain
username@omega-domain
userid::public-key@omega-domain
Test local inference without @.
Key mismatch
Resolve a valid user while supplying the wrong public-key pin.
The operation must fail.
Foreign authentication
Cache a foreign user's signed descriptor.
Disconnect the foreign user's authority.
Authenticate the foreign client directly to another Iota.
Verify the session is foreign and cannot access hosted-account functionality.
Queue recovery
Disconnect the destination Iota.
Send a relay.
Restart the origin Iota.
Reconnect the destination.
Verify the durable relay queue delivers the original relay without generating a second application event.
No centralized dependencies
Run the Iota with no reachable Omega or Omikron.
Local account creation, local login, remote Iota resolution, peer routing, and messaging must remain functional.
Stage 3: Optional hybrid mode
Goal
Hybrid mode should not become another set of handlers.
A hybrid Iota should simply construct more providers.
Application code should not need:
if decentralized {
...
} else if centralized {
...
} else if hybrid {
...
}
The mode should affect service composition and route selection, not business logic.
1. Composite identity resolution
Build:
CompositeIdentityResolver
It dispatches based on the address authority.
Examples:
alice@iota.example
-> IotaAuthorityResolver
alice@omega.example
-> OmegaAuthorityResolver
alice
-> account's configured default authority
Do not blindly try Omega and then Iota for the same unresolved address.
The namespace must remain deterministic.
2. Store account origin
Every hosted account should know who owns its identity.
For example:
pub enum AccountOrigin {
LocalIota {
authority: AuthorityId,
},
Omega {
authority: AuthorityId,
},
}
This controls account lifecycle operations.
Creating or deleting a locally authoritative user calls:
LocalIotaAccountAuthority
An Omega-managed user calls:
OmegaAccountAuthority
Messaging code does not need to care.
3. Composite routing
Implement route providers:
DirectPeerRoute
RelayPeerRoute
OmikronRoute
The router should return a route plan for the destination.
For a decentralized-capable remote Iota:
direct
then independent relay
For a legacy centralized destination:
Omikron
For an Omega user whose Iota advertises decentralized capability:
direct/relay peer transport
with Omikron available as compatibility fallback
The exact preference should live in route policy.
No relay handler should contain transport preference logic.
4. Normalize ingress before processing
All ingress paths should produce the same internal object:
Omikron
|
+------+
|
Peer -----+--> normalized verified relay --> RelayService
|
Client ---+
The source transport is metadata used for policy and acknowledgements.
It must not select a separate message implementation.
5. Use one replay namespace
A message received through a direct Iota connection and later through Omikron must be recognized as the same logical relay.
Replay identity therefore has to use:
canonical PrincipalId
canonical message ID
not transport-specific IDs.
This prevents hybrid mode from applying one message twice after route failover.
6. Keep centralized administration isolated
Functions that only make sense for Omega-managed accounts should remain inside the centralized adapter:
Omega invitations
Omikron registration
Iota registration with Omega
Omega assignment reconciliation
Omega-specific credential operations
They should not leak back into:
RelayService
IdentityService
SessionManager
Communities
message handlers
7. Hybrid acceptance criteria
Hybrid mode is complete when:
- One Iota can host both local-Iota-authoritative and Omega-authoritative accounts.
- Both account types use the same local message/session handlers.
- A remote principal resolves to the same canonical identity regardless of transport.
- Direct and Omikron delivery cannot apply the same relay twice.
- Omikron disconnect does not affect decentralized users.
- Direct peer failure does not affect centralized users.
- Communities only see authenticated principals and are independent of the route used to authenticate or contact the user's home network.
Recommended repository boundaries
The current workspace already contains many crates, so avoid splitting every trait into a new crate.
A practical distribution is:
iota-identity
PrincipalId
AuthorityId
UserAddress
UserDescriptor
resolver traits
principal normalization
iota-auth
hosted authentication
foreign principal authentication
peer authentication
challenge/proof logic
iota-storage
LocalUserStore
PrincipalStore
relay persistence
account data
iota-connection
wire-level relay parsing
relay verification primitives
normalized connection errors
transport connection traits
iota-routing
RelayService
PeerRouter
route planning
relay ingress normalization
client
authenticated client connection
session binding
client message dispatch
other-iota
PeerManager
PeerConnection
peer discovery
direct peer transport
external relay transport
omikron-connector
Omikron transport
Omega resolver adapter
Omega account authority
centralized administration
iota-daemon-lib
service composition
IPC
lifecycle
communities
future implementation consuming:
PrincipalId
SessionManager
AuthService
PrincipalStore
PeerRouter
The key rule is that omikron-connector should depend inward on common abstractions where necessary. Core identity, relay, session, and Communities code should not depend outward on omikron-connector.
Suggested implementation sequence
Stage 1A: Identity foundation
Implement:
PrincipalId
AuthorityId
UserAddress
PrincipalHandle
PrincipalStore
LocalUserStore wrapper
Add tests for identical numeric IDs under different authorities.
Do not change network behavior.
Stage 1B: Service extraction
Implement:
IdentityResolver
AccountAuthority
PeerRouter
SessionManager
Wrap existing centralized behavior.
Change daemon and command-router consumers to use the new interfaces.
Stage 1C: Relay extraction
Move handle_relay logic from OmikronConnection into RelayService.
Keep Omikron as the only transport initially.
Make the centralized integration tests pass through the new service.
Stage 1D: Foreign sessions
Implement foreign principal caching and authentication.
Add session capabilities.
Prove that a foreign principal can authenticate without entering users.
This completes the architectural work required by future Communities.
The Stage 2 sections describe components. Implement the remaining work in this dependency order.
Stage 2A: Principal-native application storage
Make contact uniqueness and message-side identity use PrincipalHandle.
Migrate receipts, reactions, blocks, chat secrets, notifications, and sync keys
that still identify a remote user by a bare numeric ID.
Stage 2B: Protocol-neutral relay domain
Replace VerifiedNormalizedRelay with a representation that contains canonical
resolved principals and optional V1 compatibility data. Split ingress responses
from local client deliveries.
Stage 2C: Federated relay protocol
Add canonical signer and recipient principals plus IotaNodeId routing to MTP.
Keep V1 decoding and encoding in transport adapters.
Stage 2D: Signed descriptors
Define signed node and user descriptor wire types, verification, revision rules, and cache validity.
Stage 2E: Authority discovery
Implement raw-IP Iota bootstrap and /.well-known/tensamin authority discovery.
Raw-IP bootstrap pins both the authenticated IotaNodeId and the observed TLS
certificate. Later direct MTP connections load that certificate pin instead of
using system roots or disabling certificate verification.
Stage 2F: Local accounts and credentials
Implement LocalIotaAccountAuthority and versioned .tu credentials. Preserve
the V1 Omega credential parser.
Iota-native .tu V2 credentials authenticate accounts already hosted by the
same Iota authority. They do not reconstruct a deleted account. attach_user()
must find the matching local account and descriptor state, and it must reject a
credential bound to another IotaNodeId. Account backup and deleted-account
restoration require a separate authority-side backup format.
Stage 2G: Common pending-relay dispatch
Move retry execution out of OmikronConnection. Dispatch legacy and
decentralized queue targets through PeerRouter, including restart recovery for
RelayTarget::Iota(_).
Stage 2H: Direct Iota peers
Define peer ingress provenance, then implement mutual node authentication,
listener, dialer, connection pool, duplicate-connection policy, liveness,
reconnection, and DirectPeerRouter registration.
Stage 2I: Federated relay path
Connect two Iotas and exchange one federated relay through RelayService. Verify
that the peer receives only the ingress response and the hosted client receives
the local delivery.
Stage 2J: Direct client gateway
Implement the MTP client gateway and hosted-client relay origination.
Stage 2K: Standalone daemon composition
Load LocalNodeIdentity during daemon startup and compose decentralized
providers without constructing OmikronConnection.
Stage 2L: Independent relay/router
Implement the independent opaque relay/router after direct peer delivery works.
Stage 2M: Remote Omega resolution
Add direct remote-Omega resolution without requiring a local Omikron connection.
Stage 2N: Integration suite
Run the Stage 2 acceptance tests. Include two remote authorities that both use the same numeric user ID, durable queue recovery, key-pin mismatch, cached foreign authentication, direct delivery, and relay-only delivery.
Stage 3: Hybrid composition
Add:
CompositeIdentityResolver
CompositeAccountAuthority
HybridPeerRouter
route policy
Do not introduce hybrid-specific message handlers.
Architecture required before Communities
Communities should not be started until the following interfaces are stable:
PrincipalId
PrincipalStore
IdentityResolver
AuthService
AuthenticatedSession
SessionCapabilities
IotaNodeId
PeerRouter
Once those are available, the Community architecture becomes straightforward:
foreign/local client
|
v
AuthService
|
v
AuthenticatedSession
|
v
Community authorization
|
v
Community handlers
A Community only needs to answer:
Who is this principal?
Are they a member?
What are they allowed to do?
It should not need to answer:
Which Omega owns them?
Which Iota hosts them?
How do I fetch their key?
Did they arrive through Omikron?
Are they a local user?
How do I establish a peer connection?
Those questions belong to the infrastructure built in Stages 1 and 2.
The resulting separation also gives decentralized and hybrid mode the same property: identity and application semantics remain stable while the transport and authority providers can change underneath them.