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 in BaseClient.authToken (defined in packages/linejs/base/core/mod.ts). The Login.login, Login.withQrCode, and Login.withPassword methods in packages/linejs/base/login/mod.ts set this token after successful authentication. It automatically attaches to every HTTP request via the RequestClient interceptor.

  • sessionId / sessionKey: A persistent cryptographic key generated during QR login via Login.requestSQR or Login.requestSQR2. This identifier binds a specific device instance to the LINE account and is required for E2EE operations and service calls like E2EE.createSqrSecret.

  • syncToken: An incremental cursor stored in BaseClient.poll.sync.<service> that tracks the client's position in the event stream. The ConnManager._OnSignOnResponse method in packages/linejs/base/push/connManager.ts updates these tokens after every fetch operation, while packages/linejs/base/polling/mod.ts maintains 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), and syncToken (event cursor).
  • Multi-device support is achieved by instantiating separate Client objects with unique device parameters, each maintaining independent push connections via ConnManager.
  • Real-time synchronization relies on persistent HTTP/2 connections where InitAndRead sends sign-on requests containing the current syncToken, and _OnSignOnResponse updates the token for the next request.
  • Session restoration is possible by storing the authToken and calling loginProcess.login({ authToken: savedToken }), bypassing interactive authentication.
  • Automatic recovery handles token refreshes by closing and reopening push connections when authToken changes, 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →