Bridge
May 12, 2026 · View on GitHub
This document is normative for dApps, wallets, bridge operators, SDKs. The keywords MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are to be interpreted as described in RFC 2119.
The bridge is a transport for messages between a dApp and a wallet. Two variants are defined:
- HTTP bridge — for external dApps and services.
- JS bridge — for dApps opened inside a wallet's webview, or when the wallet is a browser extension.
Properties:
- The bridge is operated by the wallet provider. dApp developers do not choose or run a bridge — each wallet's bridge URL is listed in the wallets list.
- Messages MUST be end-to-end encrypted on the HTTP bridge. The bridge sees neither plaintext nor stable user identifiers (for example wallet addresses, account IDs or other cross-session identity markers).
- Communication is symmetrical. The bridge does not distinguish dApps from wallets — both are clients.
- The bridge keeps a separate queue of messages per recipient
client_id.
HTTP bridge
A client with client_id A connects to the bridge to receive incoming messages.
The paths /events and /message are relative to the bridge URL published in the wallets list — for example https://connect.ton.org/bridge becomes https://connect.ton.org/bridge/events.
client_id is semi-private. dApps and wallets SHOULD NOT share their client_id with other entities to avoid having their messages removed unexpectedly.
A client MAY subscribe to multiple client_ids by enumerating them comma-separated: ?client_id=<A1>,<A2>,<A3>.
GET /events — subscribe
GET /events?client_id=<to_hex_str(A)>[&trace_id=<uuid>][&heartbeat=<legacy|message>]
Accept: text/event-stream
On reconnect, pass last_event_id to receive every event published after that ID:
GET /events?client_id=<to_hex_str(A)>&last_event_id=<lastEventId>[&trace_id=<uuid>][&heartbeat=<legacy|message>]
Accept: text/event-stream
POST /message — send
POST /message?client_id=<to_hex_str(A)>&to=<to_hex_str(B)>&ttl=300[&topic=<sendTransaction|signData|signMessage|disconnect>][&trace_id=<uuid>]
body: <base64_encoded_message>
The topic parameter is optional. If present, it SHOULD equal the RPC method inside the encrypted message so the bridge can route a push notification. Bridges do not validate topic against an enum — values not listed here are forwarded unchanged.
The bridge buffers each message up to ttl seconds (the sender specifies it on the request). It removes the message as soon as the recipient receives it.
If ttl exceeds the bridge's hard limit, the bridge MUST respond with HTTP 400. Bridges SHOULD support at least ttl=300.
trace_id — analytics correlation
trace_id is an optional query parameter on GET /events, POST /message and the connect URL (see deeplinks.md § Parameters). It carries a UUID used to correlate logs and analytics across the dApp, bridge and wallet for a single user-visible operation. It is not security-sensitive and the bridge does not act on its value.
- Senders SHOULD use UUIDv7 so identifiers sort by time.
- A bridge that supports tracing MUST treat a malformed
trace_idas absent. It SHOULD then generate a fresh UUIDv7 and use that for the request. - A bridge MAY also generate a
trace_idwhen the sender omitted it. - A bridge that propagates tracing MUST include the resulting
trace_idin theBridgeMessageenvelope it writes to the recipient. Older or tracing-unaware bridges MAY omit the field; readers MUST tolerate its absence. - Recipients (wallets and dApps) SHOULD reuse the inbound
BridgeMessage.trace_idwhen posting their reply, so that a request and its response share an ID.
BridgeMessage envelope
When the bridge forwards a message from A to B, it wraps it in a BridgeMessage and writes it to B's SSE channel:
{
"from": "<to_hex_str(A)>",
"message": "<base64_encoded_message>",
"trace_id": "<uuid>"
}
| Field | Required | Description |
|---|---|---|
from | yes | Sender client_id as 64-character lowercase hex. |
message | yes | Base64-encoded ciphertext: 24-byte nonce concatenated with the nacl.box output. See session.md. |
trace_id | no | UUID for analytics correlation. Set by bridges that support tracing; absent on older or tracing-unaware bridges. Readers MUST tolerate its absence. See trace_id. |
See schemas/bridge-message.schema.json for the machine-readable schema.
Heartbeat
The bridge MUST periodically send a heartbeat over each SSE connection. The format is controlled by the optional heartbeat query parameter on /events:
heartbeat=legacy(default when omitted): the heartbeat isevent: heartbeat. The browser SSE client does not expose this to JavaScript because it is not a standardmessageevent.heartbeat=message: the heartbeat isevent: message,data: heartbeat. Use this when the client wants to implement custom keep-alive logic.
Operational rules
Protocol does not fix numeric global limits. Bridge operators SHOULD define and publish their limits for each deployment.
Rate limits
- The bridge MUST enforce request-rate limits on
GET /eventsandPOST /message. - Limits SHOULD be applied per source IP and per
client_id. - Exceeding a limit MUST return HTTP
429 Too Many Requests. - The bridge SHOULD include
Retry-Afterin429responses.
Message size limits
- The bridge MUST enforce a maximum request-body size for
POST /message(base64-encoded ciphertext). - Exceeding the size limit MUST return HTTP
413 Payload Too Largeor HTTP400 Bad Request. - The configured limit MUST be documented by the bridge operator.
TTL limits
- The bridge MUST reject non-positive
ttlvalues with HTTP400. - If
ttlexceeds the bridge maximum, the bridge MUST return HTTP400. - Every bridge deployment SHOULD support
ttl=300seconds or higher. - The configured maximum
ttlMUST be documented by the bridge operator.
CORS and origin policy
- Browser-facing bridge endpoints (
GET /events,POST /message) MUST be callable from web dApps. - A bridge deployment MUST either:
- allow cross-origin requests from any origin; or
- maintain an explicit allowlist policy that includes every supported dApp origin.
- If an origin-based deny policy is used, denied requests MUST return HTTP
403. - Wallet-side requests (mobile or server-side) MUST NOT rely on browser CORS behaviour.
Deep-link transport
Universal links, unified tc:// links, custom wallet schemes (deepLink), ret behaviour, URL-length constraints and embedded-request (e) wire format are specified in deeplinks.md.
Bridge implementations and SDKs MUST follow that document for deep-link parameter handling and embedded-request transport.
JS bridge
The JS bridge is used by dApps embedded in a wallet's webview, via the injected binding window.<walletJsBridgeKey>.tonconnect. The walletJsBridgeKey value is published in the wallets list as the key field of the wallet's JS bridge entry.
The JS bridge runs on the same device as the wallet and the dApp — communication is not encrypted. The dApp works directly with plaintext requests and responses, with no session keys.
interface TonConnectBridge {
deviceInfo: DeviceInfo; // see Requests/Responses spec
walletInfo?: WalletInfo;
protocolVersion: number; // max supported TON Connect version (e.g. 2)
isWalletBrowser: boolean; // true when the page is opened inside the wallet's browser
connect(protocolVersion: number, message: ConnectRequest): Promise<ConnectEvent>;
restoreConnection(): Promise<ConnectEvent>;
send(message: AppRequest): Promise<WalletResponse>;
listen(callback: (event: WalletEvent) => void): () => void;
}
Like the HTTP bridge, the wallet side ignores dApp requests until the user confirms the session. Messages arrive from the webview into the bridge controller and are silently dropped before that point.
walletInfo (optional)
Wallet metadata. Use it to make an injectable wallet work with TON Connect even when the wallet is not yet listed in the wallets list.
interface WalletInfo {
name: string;
image: string; // PNG image URL
tondns?: string;
about_url: string; // about page URL
}
If TonConnectBridge.walletInfo is set and the wallet is also listed in the wallets list, the values in walletInfo override those in the registry.
connect()
Initiates a connect request. Equivalent to the QR/link flow on the HTTP bridge.
If the dApp was previously approved for the current account, the bridge connects silently and emits a ConnectEvent. Otherwise it shows the confirmation dialog.
The dApp SHOULD NOT call connect() without an explicit user action (e.g. a click on a Connect button). To restore a prior connection automatically, use restoreConnection().
restoreConnection()
Attempts to restore a prior connection.
If the dApp was previously approved for the current account, the bridge connects silently and emits a ConnectEvent with only a ton_addr data item.
Otherwise it returns ConnectEventError with code 100 (UNKNOWN_APP).
send()
Sends an AppRequest to the bridge — every method except ConnectRequest (which goes into the QR code on the HTTP bridge and into connect() on the JS bridge). Returns a promise that resolves with the WalletResponse directly. The caller does not need listen() to await the reply.
listen()
Registers a listener for wallet-initiated events. Returns an unsubscribe function. Currently disconnect is the only event. Future versions may add more.
See also
session.md— end-to-end encryption on top of the HTTP bridgeconnect.md—ConnectRequest/ConnectEventshapesrpc.md— RPC methods andWalletResponsedeeplinks.md— universal link details