How to Implement Multi-Device Sync and Session Management in LINEJS
LINEJS implements a single-session, multi-device architecture using authToken for HTTP credentials, sessionId for cryptographic device binding, and syncToken as an incremental cursor for real-time state synchronization across Talk and Square services.
LINEJS is an open-source TypeScript library that replicates the official LINE client protocol, enabling developers to build applications that sync messages and events across multiple devices simultaneously. Understanding how to implement multi-device sync and session management in LINEJS requires familiarity with its three-tier identifier system and persistent push-based connection model.
Understanding LINEJS Session Architecture
LINEJS maintains session state through three distinct identifiers that work together to enable secure, synchronized communication across devices.
Core Session Identifiers
The library manages three primary identifiers, each serving a specific purpose in the authentication and synchronization flow:
-
authToken: A short-lived JWT-style credential stored inBaseClient.authToken(defined inpackages/linejs/base/core/mod.ts). TheLogin.login,Login.withQrCode, andLogin.withPasswordmethods inpackages/linejs/base/login/mod.tsset this token after successful authentication. It automatically attaches to every HTTP request via theRequestClientinterceptor. -
sessionId/sessionKey: A persistent cryptographic key generated during QR login viaLogin.requestSQRorLogin.requestSQR2. This identifier binds a specific device instance to the LINE account and is required for E2EE operations and service calls likeE2EE.createSqrSecret. -
syncToken: An incremental cursor stored inBaseClient.poll.sync.<service>that tracks the client's position in the event stream. TheConnManager._OnSignOnResponsemethod inpackages/linejs/base/push/connManager.tsupdates these tokens after every fetch operation, whilepackages/linejs/base/polling/mod.tsmaintains the persistent state.
Initializing Sessions and Multi-Device Login
Creating a multi-device setup involves instantiating separate Client instances, each representing a distinct device within the same LINE account.
QR Code and Password Authentication
The primary entry points for session creation are loginWithQR and loginWithPassword, both of which orchestrate the handshake with LINE's servers:
import { loginWithQR } from "@evex/linejs/client/login.ts";
const client = await loginWithQR(
{
onReceiveQRUrl: async (url) => console.log("Scan QR:", url),
onPincodeRequest: (pin) => console.log("Enter PIN:", pin),
},
{
device: "iOS",
version: "14.0",
endpoint: "legy.line-apps.com",
},
);
Under the hood, loginWithQR calls Login.requestSQR (QR v1) or Login.requestSQR2 (QR v2), which return a sessionId and a certificate. After the user scans the QR code and verifies the PIN, the server returns the authToken that gets stored in BaseClient.authToken. The Login.registerCert method then persists the device certificate for future logins.
Device-Specific Session Binding
Each client instance represents a physical device through the Device type defined in packages/linejs/base/utils/devices.ts. The library validates device strings (e.g., "iOS", "Android") against supported versions using getDeviceDetails.
const clientA = await loginWithQR(..., { device: "iOS", version: "13.5" });
const clientB = await loginWithQR(..., { device: "Android", version: "11" });
Both clients maintain independent authToken values but share the underlying LINE account. Because the sessionId is cryptographically bound to the device parameters, each client receives only its own device-specific push events through separate ConnManager instances.
Implementing Real-Time Multi-Device Sync
LINEJS opens a persistent HTTP/2 push connection for each client to receive real-time updates, ensuring all devices stay synchronized without polling.
Push Connection Architecture
The ConnManager class in packages/linejs/base/push/connManager.ts manages the H2 connection lifecycle. To start synchronization, invoke InitAndRead with the service identifiers you want to sync:
await client.push.InitAndRead([3, 5]); // 3 = Square, 5 = Talk
The InitAndRead method sends a sign-on request containing the current syncToken for each service. The server responds with a payload that includes new events and an updated token. The _OnSignOnResponse handler extracts these values and updates the client's state:
// Inside ConnManager._OnSignOnResponse
this.client.poll.sync.square = newSquareToken;
this.client.poll.sync.talk.revision = response.fullSyncResponse.nextRevision;
Sync Token Lifecycle
The sync token mechanism ensures exactly-once delivery and ordering guarantees. When the client receives a response, it immediately uses the new token for the next sign-on request, creating a continuous chain. The Polling module in packages/linejs/base/polling/mod.ts exposes these tokens via client.poll.sync, allowing applications to track synchronization state across Talk and Square services.
Session Persistence and Restoration
LINEJS supports session restoration to eliminate repeated QR scans or password entry when restarting applications or switching between devices.
Token-Based Re-authentication
Store the authToken securely after the initial login, then restore the session by injecting it into a new Client instance:
import { Client } from "@evex/linejs/base/core/mod.ts";
const savedToken = await secureStorage.get("authToken");
const client = new Client({
device: "Android",
version: "12",
storage: new MemoryStorage(),
});
// Restore session without QR/password
await client.loginProcess.login({ authToken: savedToken });
await client.push.InitAndRead([3, 5]);
The loginProcess.login method in packages/linejs/base/login/mod.ts accepts the authToken parameter and bypasses the interactive authentication flow, immediately setting BaseClient.authToken and initializing the service layer.
Handling Auth Token Refresh
When the authToken expires or the user changes their password, ConnManager._OnPingCallback detects the token mismatch and automatically re-establishes the push connection:
if (oldToken !== newToken && newToken) {
this.authToken = newToken;
this.conns[0].close(); // Triggers reconnection with new credentials
}
This mechanism ensures that all active devices maintain valid connections even when the primary authentication credentials rotate.
Advanced Sync Token Management
While LINEJS automatically manages sync tokens during push operations, applications requiring custom pagination or historical event retrieval can manipulate these tokens directly.
Manual Pagination Control
To fetch older Square events manually, retrieve the current token from client.poll.sync.square and pass it to fetchMyEvents:
const current = client.poll.sync.square;
const { events, syncToken } = await client.square.fetchMyEvents({
subscriptionId: 12345,
syncToken: current,
limit: 200,
});
client.poll.sync.square = syncToken; // Update state for continuity
For Talk synchronization, use the talk.sync method with the revision numbers stored in client.poll.sync.talk:
const sync = client.poll.sync.talk;
const res = await client.talk.sync({
request: {
lastRevision: sync.revision - 500, // Historical pagination
count: 200,
lastGlobalRevision: sync.globalRev,
lastIndividualRevision: sync.individualRev,
},
});
The client.square.fetchMyEvents implementation in packages/linejs/client/features/square/mod.ts and the talk.sync implementation in packages/linejs/base/service/talk/mod.ts both rely on the Polling state managed in packages/linejs/base/polling/mod.ts to maintain consistency with the automatic push synchronization.
Summary
- LINEJS uses three identifiers for session management:
authToken(HTTP credentials),sessionId(device binding), andsyncToken(event cursor). - Multi-device support is achieved by instantiating separate
Clientobjects with unique device parameters, each maintaining independent push connections viaConnManager. - Real-time synchronization relies on persistent HTTP/2 connections where
InitAndReadsends sign-on requests containing the currentsyncToken, and_OnSignOnResponseupdates the token for the next request. - Session restoration is possible by storing the
authTokenand callingloginProcess.login({ authToken: savedToken }), bypassing interactive authentication. - Automatic recovery handles token refreshes by closing and reopening push connections when
authTokenchanges, ensuring continuous synchronization across all devices.
Frequently Asked Questions
How does LINEJS handle authentication across multiple devices simultaneously?
LINEJS creates a unique sessionId for each device during the QR or password login flow in packages/linejs/base/login/mod.ts. While all devices share the same LINE account, each maintains an independent authToken and push connection through ConnManager. The server routes events to each device based on its specific sessionId, allowing multiple clients to operate concurrently without interfering with each other's state.
What is the difference between authToken and syncToken in LINEJS?
The authToken is a short-lived authentication credential stored in BaseClient.authToken that validates HTTP requests to LINE's servers. In contrast, the syncToken (stored in client.poll.sync.talk or client.poll.sync.square) is an incremental cursor that tracks the client's position in the event stream. While the authToken identifies who is making the request, the syncToken tells the server where the client left off in the message history.
Can I restore a LINEJS session without scanning a QR code again?
Yes. Store the authToken after the initial successful login, then create a new Client instance and call client.loginProcess.login({ authToken: savedToken }) as implemented in packages/linejs/base/login/mod.ts. This method bypasses the QR or password flow and immediately restores the session, though you must ensure the authToken has not expired if you intend to use it for an extended period.
How does LINEJS ensure messages stay synchronized when the connection drops?
The ConnManager in packages/linejs/base/push/connManager.ts monitors connection health through _OnPingCallback. If the connection drops or the authToken changes, the manager automatically closes the stale connection and reinitializes InitAndRead using the last known syncToken from client.poll.sync. This ensures that when the client reconnects, it requests only the events missed during the downtime, maintaining exactly-once delivery semantics.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →