本文にスキップする

Building an app server with Firebase

Juheon Jeon

Hello, I'm Juheon Jeon, responsible for iOS/macOS and Flutter PlanetKit at LINE Planet.

LINE Planet is a real-time communications platform as a service (CPaaS) that provides the cloud platform and client SDKs needed to integrate real-time voice and video calling into services.

To use LINE Planet, each service must implement its own app server on the application layer to connect LINE Planet with the service's users and devices. This post introduces a high-level guide to implementing the app server using Firebase, with minimal code examples showing the core API design and implementation patterns.

What is an app server?

In LINE Planet, "app server" refers to an application-layer server implemented by each LINE Planet user for their own service.

The main roles of the app server are as follows:

  • Issues PlanetKit access tokens so clients can use LINE Planet securely.
  • Receives call-related callbacks from LINE Planet.
  • Sends incoming call notifications to the appropriate devices using the service's chosen push infrastructure.
  • Receives device and call information in the service's backend so that behavior can be monitored, debugged, and analyzed.
DIAGRAM: Incoming call notification handling example to show the app server role

Introduction to Firebase and the features used

Firebase is a cloud-based backend service platform (backend as a service, BaaS) provided by Google, a service that pre-implements and provides server-side functions commonly used when developing web or mobile apps. Using Firebase, you can reduce infrastructure work and implement an app server that can be easily scaled.

The app server implementation introduced in this post uses the following three Firebase products:

Let me briefly introduce each product one by one.

Cloud Functions and two types of calling methods

Cloud Functions is a service that allows you to run backend code without a dedicated server. If you write backend code, deploy it to cloud infrastructure, and set up events such as other Firebase features, database changes, or HTTPS calls as triggers, the code automatically runs in a managed environment provided by Firebase when the event occurs.

The app server introduced in this post implements all endpoints as Cloud Functions, and there are two ways to call these endpoints.

The first is direct HTTPS request. Each function is exposed as a URL such as:

[CODE SNIPPET: Example HTTPS endpoint URL]
https://<region>-<project-id>.cloudfunctions.net/registerDevice

Any HTTP client can call this endpoint. For security, the app server issues a short-lived app server access token, and other HTTP APIs (except token issuance) must be called with Authorization: Bearer <token>. This mode is convenient for server-to-server integration and software tool development.

The second is the Firebase callable function. From Android, iOS, Web, or Flutter, the client can obtain a Functions instance and call:

let functions = Functions.functions(region: "asia-northeast1")
let registerDevice = functions.httpsCallable("registerDevice")
registerDevice.call(requestData) { result, error in}

In this mode, Firebase handles authentication and transport, so there is no need to issue app server access token to the client. This is the recommended method for platforms that support Firebase SDK.

Cloud Firestore

Cloud Firestore is a scalable NoSQL cloud database based on Google Cloud, provided by Firebase. Here, it is used as the app server's database, storing device registrations, incoming call entries (Firestore-based "virtual push"), and call event logs. The exact collection and document structure can be customized in the source code. For this post, it is sufficient to know that Firestore stores the app server's state and logs.

FCM

FCM is a free messaging service provided by the Firebase platform. The app server built in this post uses it to send push notifications to platforms that support FCM, such as Android or certain desktop or web environments. When the app server receives an incoming call callback, it uses FCM to deliver a data message to the target device so that the client app can display an incoming call UI and proceed with PlanetKit call verification.

LINE Planet Console

LINE Planet Console (LPC) is a service for managing services developed using the LINE Planet SDK. It provides features such as usage and call history checking, service traffic monitoring, and API key and callback URL configuration and management. To deploy the app server after implementation, you need to sign up for LPC and prepare various items necessary for deploying the app server. This will be explained in detail later in the App server deployment preparation section.

App server feature introduction

This section presents a minimal app server design that demonstrates the core features necessary to use LINE Planet with PlanetKit. This is a reference implementation showing the essential client APIs and server callbacks.

Caution

This design represents one possible approach to building an app server. Depending on your service's requirements, you may need to:

  • Add additional authentication and authorization layers
  • Implement more sophisticated device management (e.g., handling multiple devices per user, device preferences)
  • Add rate limiting and abuse prevention
  • Implement custom business logic for call routing, user permissions, or billing
  • Extend the data model to fit your service's specific needs
  • Add monitoring, alerting, and analytics beyond the basic event logging shown here

Additionally, the code snippets provided in this post are simplified examples focusing on core functionality. Production implementations should include comprehensive error handling, input validation, logging, and security measures appropriate for your use case.

Now let's look at each app server function one by one.

Client APIs

The app server exposes four client APIs: RegisterDevice, RemoveDevice, IssuePlanetKitAccessToken, and IssueAppServerAccessToken. These APIs are all implemented as Cloud Functions and accessible both via HTTPS and Firebase callable functions.

RegisterDevice

Registers or updates a device for a given user and service so that incoming call notifications can be delivered to the registered device.

  • Request parameters

    • userId: User identifier
    • serviceId: Service identifier from LPC
    • deviceType: Device platform (iOS, Android, Windows (desktop), Linux, etc.)
    • deviceToken: Push notification token
    • applicationUserData: Optional custom data
  • Response parameters

    • result: Result code (SUCCESS or error code)
    • message: Human-readable message
    • path: Firestore document path where device was registered
Code example
const {onCall} = require("firebase-functions/v2/https");
const admin = require("firebase-admin");
const db = admin.firestore();

const registerDevice = onCall(async (request) => {
const data = request.data;

// 1. Validate required fields (userId, serviceId, deviceType, deviceToken)
const required = ["userId", "serviceId", "deviceType", "deviceToken"];
for (const field of required) {
if (!data[field] || data[field].trim() === "") {
return {
result: "MISSING_REQUIRED_FIELD",
message: `Missing required field: ${field}`,
};
}
}

// 2. Validate service ID exists in configuration
// 3. Validate device type format
// 4. Validate device token format

// 5. Store device registration in Firestore
// Path: services/{serviceId}/users/{userId}/devices/{deviceType}
const deviceDoc = {
deviceToken: data.deviceToken,
applicationUserData: data.applicationUserData || null,
registeredAt: admin.firestore.FieldValue.serverTimestamp(),
};

await db
.collection("services")
.doc(data.serviceId)
.collection("users")
.doc(data.userId)
.collection("devices")
.doc(data.deviceType)
.set(deviceDoc);

return {result: "SUCCESS", message: "Device registered successfully"};
});

RemoveDevice

Removes a device registration so that the device no longer receives incoming call notifications.

  • Request parameters

    • userId: User identifier
    • serviceId: Service identifier
    • deviceType: Device platform to remove
  • Response fields

    • result: Result code (SUCCESS, DEVICE_NOT_FOUND, or error code)
    • message: Human-readable message
Code example
const {onCall} = require("firebase-functions/v2/https");
const admin = require("firebase-admin");
const db = admin.firestore();

const removeDevice = onCall(async (request) => {
const {userId, serviceId, deviceType} = request.data;

// 1. Validate required fields
const required = ["userId", "serviceId", "deviceType"];
for (const field of required) {
if (!request.data[field] || request.data[field].trim() === "") {
return {
result: "MISSING_REQUIRED_FIELD",
message: `Missing required field: ${field}`,
};
}
}

// 2. Check if device exists
const devicePath = db
.collection("services")
.doc(serviceId)
.collection("users")
.doc(userId)
.collection("devices")
.doc(deviceType);

const deviceDoc = await devicePath.get();
if (!deviceDoc.exists) {
return {result: "DEVICE_NOT_FOUND", message: "Device not found"};
}

// 3. Delete the device document
await devicePath.delete();

// 4. Clean up user document if no devices remain
const remainingDevices = await db
.collection("services")
.doc(serviceId)
.collection("users")
.doc(userId)
.collection("devices")
.get();

if (remainingDevices.empty) {
await db.collection("services").doc(serviceId)
.collection("users").doc(userId).delete();
}

return {result: "SUCCESS", message: "Device removed successfully"};
});

IssuePlanetKitAccessToken

Issues a short-lived PlanetKit access token for a given user and service. This token is used when starting, accepting, or joining calls with PlanetKit.

  • Request parameters

    • userId: User identifier
    • serviceId: Service identifier
  • Response parameters

    • result: Result code (SUCCESS or error code)
    • message: Human-readable message
    • accessToken: JWT token for PlanetKit authentication
Code example
const {onCall} = require("firebase-functions/v2/https");
const jwt = require("jsonwebtoken");

// Service configuration should be loaded from a secure config file
// Example structure: {"serviceId": {"apiKey": "...", "apiSecret": "..."}}
const serviceConfigs = require("./config/services.json");

const issuePlanetKitAccessToken = onCall(async (request) => {
const {userId, serviceId} = request.data;

// 1. Validate required fields
if (!userId || !serviceId) {
return {
result: "MISSING_REQUIRED_FIELD",
message: "userId and serviceId are required",
};
}

// 2. Get API credentials from service configuration
const credentials = serviceConfigs[serviceId];
if (!credentials || !credentials.apiKey || !credentials.apiSecret) {
return {
result: "INVALID_SERVICE_ID",
message: "Service configuration not found",
};
}

// 3. Create JWT payload
const payload = {
sub: serviceId, // subject: serviceId
iss: credentials.apiKey, // issuer: apiKey from LPC
iat: Math.floor(Date.now() / 1000), // issued at
uid: userId, // user identifier
};

// 4. Sign with HMAC256 using API secret from LPC
const token = jwt.sign(payload, credentials.apiSecret, {
algorithm: "HS256",
header: {typ: "JWT"},
});

return {
result: "SUCCESS",
message: "Access token generated successfully",
accessToken: token,
};
});

IssueAppServerAccessToken (optional)

Issues a short-lived app server access token for authenticating direct HTTPS calls to other app server APIs. This is only required when not using Firebase callable functions.

  • Request parameters

    • serviceId: Service identifier
    • APIKey: API key from LPC (for authentication)
  • Response parameters

    • result: Result code (SUCCESS, INVALID_API_KEY, or error code)
    • message: Human-readable message
    • accessToken: JWT token for app server API authentication
Code example
const {onRequest} = require("firebase-functions/v2/https");
const jwt = require("jsonwebtoken");

// Service configuration should be loaded from a secure config file
const serviceConfigs = require("./config/services.json");

const issueAppServerAccessToken = onRequest(async (req, res) => {
const {serviceId, APIKey} = req.body;

// 1. Validate API key matches service configuration
const credentials = serviceConfigs[serviceId];
if (!credentials || APIKey !== credentials.apiKey) {
return res.status(401).json({
result: "INVALID_API_KEY",
message: "Invalid API key",
});
}

// 2. Create JWT with expiration (e.g., 1 hour)
const now = Math.floor(Date.now() / 1000);
const payload = {
sub: serviceId,
iss: "app-server",
iat: now,
exp: now + 3600, // 1 hour expiration
type: "app-server-token",
};

const token = jwt.sign(payload, credentials.apiSecret, {
algorithm: "HS256",
});

res.json({
result: "SUCCESS",
message: "App server access token generated successfully",
accessToken: token,
});
});

Server callbacks

LINE Planet cloud calls the app server when call-related events occur. These are called server callbacks and are implemented as Cloud Functions with HTTPS triggers that accept GET requests with query parameters.

For complete payload structure and field specifications, refer to the LINE Planet Callbacks documentation.

notifyCallback

Called when there is an incoming call for a user. When this callback is called, the app server looks up the devices registered for that user and sends notifications via APNs or FCM so that the client can display an incoming call screen and verify the call with PlanetKit.

  • Request parameters (GET query string)

    • sid: Session ID (36 characters)
    • from_service_id: Caller's service ID
    • from_user_id: Caller's user ID
    • to_service_id: Callee's service ID
    • to_user_id: Callee's user ID
    • type: Call type ('A' for audio, 'V' for video)
    • param: Call verification parameter (to be passed to PlanetKit as cc_param)
    • app_svr_data: Optional application data
  • Authentication

    • Custom header with key-value pair configured in LPC
Code example
const {onRequest} = require("firebase-functions/v2/https");
const admin = require("firebase-admin");
const db = admin.firestore();

// Authentication configuration for callbacks
const CALLBACK_AUTH_HEADER = "your-callback-auth-header";
const CALLBACK_AUTH_VALUE = "your-secret-value";

const notifyCallback = onRequest({cors: true}, async (req, res) => {
// 1. Validate authentication header
const authHeader = req.headers[CALLBACK_AUTH_HEADER.toLowerCase()];
if (authHeader !== CALLBACK_AUTH_VALUE) {
return res.status(401).json({
result: "INVALID_CALLBACK_AUTH",
message: "Invalid callback authentication",
});
}

// 2. Parse and validate query parameters
const {sid, from_service_id, from_user_id, to_service_id,
to_user_id, type, param, app_svr_data} = req.query;

// 3. Validate both users exist in registered devices
const fromUserExists = await checkUserExists(from_service_id, from_user_id);
const toUserExists = await checkUserExists(to_service_id, to_user_id);

if (!fromUserExists || !toUserExists) {
return res.status(404).json({
result: "USER_NOT_FOUND",
message: "User not found",
});
}

// 4. Store callback data in Firestore for logging
const callbackDoc = {
sid,
from_service_id,
from_user_id,
to_service_id,
to_user_id,
type,
cc_param: param, // Renamed for clarity
app_svr_data,
receivedAt: admin.firestore.FieldValue.serverTimestamp(),
};

await db.collection("notify_callbacks").add(callbackDoc);

// 5. Send push notifications to callee's devices
// This function looks up all devices for to_user_id and sends:
// - APNs VoIP push for iOS devices
// - FCM data message for Android devices
const pushResult = await sendPushNotificationsToUser(
to_service_id,
to_user_id,
callbackDoc,
sid
);

res.json({
result: "SUCCESS",
message: "Notify callback processed successfully",
pushNotifications: pushResult,
});
});

// Helper function to check if user exists
async function checkUserExists(serviceId, userId) {
const userDoc = await db
.collection("services")
.doc(serviceId)
.collection("users")
.doc(userId)
.get();
return userDoc.exists;
}

callEventCallback

Called when a 1-to-1 call ends. The app server records call statistics such as start time, end time, duration, and disconnect reason in Firestore for later monitoring and analysis. For detailed information about each parameter, refer to the 1-to-1 Call Event Callback documentation.

  • Request parameters (GET query string)
    • sid: Session ID
    • from_service_id, from_user_id: Caller identifiers
    • to_service_id, to_user_id: Callee identifiers
    • type: Media type ('A' or 'V')
    • setup_time, start_time, end_time: Call setup, start, and end time (Unix timestamps)
    • duration: Call duration in seconds
    • srcip, dstip: Caller and callee IP addresses
    • terminate: Q.850 cause code
    • rel_code: Detailed call release code
    • rel_code_str: Detailed call release message
    • billing_sec: Billable call time in seconds
    • disconnect_reason: Call disconnect reason
    • releaser_type: Call termination source type
    • Additional fields: rc_idc (IDC that handled the caller), user_rel_code (application-defined call release code), app_svr_data (application data), etc.
Code example
const {onRequest} = require("firebase-functions/v2/https");
const admin = require("firebase-admin");
const db = admin.firestore();

// Authentication configuration for callbacks
const CALLBACK_AUTH_HEADER = "your-callback-auth-header";
const CALLBACK_AUTH_VALUE = "your-secret-value";

const callEventCallback = onRequest({cors: true}, async (req, res) => {
// 1. Validate authentication header
const authHeader = req.headers[CALLBACK_AUTH_HEADER.toLowerCase()];
if (authHeader !== CALLBACK_AUTH_VALUE) {
return res.status(401).json({
result: "INVALID_CALLBACK_AUTH",
message: "Invalid callback authentication",
});
}

// 2. Parse query parameters
const data = req.query;

// 3. Validate users exist
const fromUserExists = await checkUserExists(
data.from_service_id,
data.from_user_id
);
const toUserExists = await checkUserExists(
data.to_service_id,
data.to_user_id
);

if (!fromUserExists || !toUserExists) {
return res.status(404).json({
result: "USER_NOT_FOUND",
message: "User not found",
});
}

// 4. Store call event in Firestore for analytics
const callEventDoc = {
sid: data.sid,
from_service_id: data.from_service_id,
from_user_id: data.from_user_id,
to_service_id: data.to_service_id,
to_user_id: data.to_user_id,
type: data.type,
setup_time: parseInt(data.setup_time, 10),
start_time: parseInt(data.start_time, 10),
end_time: parseInt(data.end_time, 10),
duration: parseInt(data.duration, 10),
srcip: data.srcip,
dstip: data.dstip,
terminate: parseInt(data.terminate, 10),
rel_code: parseInt(data.rel_code, 10),
rel_code_str: data.rel_code_str,
billing_sec: parseInt(data.billing_sec, 10),
disconnect_reason: parseInt(data.disconnect_reason, 10),
releaser_type: parseInt(data.releaser_type, 10),
receivedAt: admin.firestore.FieldValue.serverTimestamp(),
};

await db.collection("call_events").add(callEventDoc);

res.json({
result: "SUCCESS",
message: "Call event callback processed successfully",
});
});

// Helper function to check if user exists
async function checkUserExists(serviceId, userId) {
const userDoc = await db
.collection("services")
.doc(serviceId)
.collection("users")
.doc(userId)
.get();
return userDoc.exists;
}

groupCallEventCallback

Called for group call events such as participants joining, leaving, or the call ending. The app server maintains per-session summary information and detailed event logs so group call behavior can be analyzed. For detailed information about each parameter, refer to the Group Call Event Callback documentation.

  • Request parameters (GET query string)
    • sid: Session ID (32-36 characters for group calls)
    • svc_id: Group call service ID
    • id: Group call ID
    • user_svc_id, user_id: Participant identifiers
    • host_svc_id, host_id: Host identifiers
    • sc: Group call status code ('S' = Started, 'C' = Changed, 'E' = Ended)
    • msc: Participant status code ('C' = Connected, 'D' = Disconnected, 'T' = Timeout, 'M' = Media changed)
    • setup_time, start_time, end_time: Call setup, start, and end time
    • online: Number of online participants
    • media_type: Media type ('A' or 'V')
    • ts: Event timestamp
    • Additional fields: ue_type (user endpoint type), display_name (user endpoint display name), mtg_data (meeting data specified at meeting creation), etc.
Code example
const {onRequest} = require("firebase-functions/v2/https");
const admin = require("firebase-admin");
const db = admin.firestore();

// Authentication configuration for callbacks
const CALLBACK_AUTH_HEADER = "your-callback-auth-header";
const CALLBACK_AUTH_VALUE = "your-secret-value";

const groupCallEventCallback = onRequest({cors: true}, async (req, res) => {
// 1. Validate authentication header
const authHeader = req.headers[CALLBACK_AUTH_HEADER.toLowerCase()];
if (authHeader !== CALLBACK_AUTH_VALUE) {
return res.status(401).json({
result: "INVALID_CALLBACK_AUTH",
message: "Invalid callback authentication",
});
}

// 2. Parse and validate query parameters
const data = req.query;

// 3. Validate user and host exist
const userExists = await checkUserExists(data.user_svc_id, data.user_id);
const hostExists = await checkUserExists(data.host_svc_id, data.host_id);

if (!userExists || !hostExists) {
return res.status(404).json({
result: "USER_NOT_FOUND",
message: "User not found",
});
}

// 4. Store event in session-based structure for accumulation
// Structure: group_call_events/{sid}/events/{auto-id}
const groupCallEventDoc = {
sid: data.sid,
svc_id: data.svc_id,
id: data.id,
user_svc_id: data.user_svc_id,
user_id: data.user_id,
host_svc_id: data.host_svc_id,
host_id: data.host_id,
sc: data.sc,
msc: data.msc,
setup_time: parseInt(data.setup_time, 10),
start_time: parseInt(data.start_time, 10),
end_time: parseInt(data.end_time, 10),
online: parseInt(data.online, 10),
media_type: data.media_type,
ts: parseInt(data.ts, 10),
receivedAt: admin.firestore.FieldValue.serverTimestamp(),
};

const sessionRef = db.collection("group_call_events").doc(data.sid);

// Add event to subcollection
await sessionRef.collection("events").add(groupCallEventDoc);

// 5. Update session summary document
const sessionSummary = {
sid: data.sid,
svc_id: data.svc_id,
group_call_id: data.id,
host_svc_id: data.host_svc_id,
host_id: data.host_id,
media_type: data.media_type,
latest_sc: data.sc,
latest_online_count: parseInt(data.online, 10),
setup_time: parseInt(data.setup_time, 10),
latest_event_time: parseInt(data.ts, 10),
call_ended: data.sc === "E",
lastUpdatedAt: admin.firestore.FieldValue.serverTimestamp(),
total_events: admin.firestore.FieldValue.increment(1),
};

await sessionRef.set(sessionSummary, {merge: true});

res.json({
result: "SUCCESS",
message: "Group call event callback processed successfully",
});
});

// Helper function to check if user exists
async function checkUserExists(serviceId, userId) {
const userDoc = await db
.collection("services")
.doc(serviceId)
.collection("users")
.doc(userId)
.get();
return userDoc.exists;
}

App server deployment preparation

Before deploying the app server, two things must be prepared. One is to sign up for LINE Planet Console (LPC) and prepare the necessary items for LINE Planet project configuration, and the other is to prepare Firebase for hosting the app server. Once both are prepared, the app server source code can be configured. Let's look at how to prepare each one.

LPC

Sign up and log in to LPC

LPC is where you create and manage LINE Planet projects. Access the following LPC website in a browser, sign up, and log in.

Acquire service ID

When integrating with LINE Planet, you can choose and apply one of two execution environments (Evaluation/Real) provided by LINE Planet.

  • Evaluation environment
    • A separate verification/testing environment provided by LINE Planet (it does not refer to a staging environment operated separately by each service). It can be used for integration development, feature verification, QA, and internal testing before applying actual production traffic.
    • You can acquire a service ID from LPC.
  • Real environment (production)
    • A production environment that handles actual user traffic, used during service launch and operation stages.
    • The Real environment can be accessed after contacting the LINE Planet team (email: dl_planet_help@linecorp.com).

The recommended workflow is to complete integration and verification in the Evaluation environment, then switch to the Real environment when production readiness is achieved.

SCREENSHOT: LPC service ID acquisition menu

Issue API key and secret

In the same LPC project, issue an API key and API secret for the service. The API key and API secret are used when the app server communicates with LINE Planet cloud on behalf of the service, so store them securely as they will be needed later when configuring the app server.

SCREENSHOT: LPC screen showing API key and secret configuration

Note: Be sure to copy and save the API secret before closing this dialog. Once the dialog is closed, you cannot view this API secret again.

Firebase

Prepare a Firebase project

If your service is already using Firebase, you can reuse the existing project for the app server. In this case, skip project creation and use the existing Firebase project that the app is already connected to. If you don't have a Firebase project yet, create a new project dedicated to the LINE Planet app server. This project is for hosting Cloud Functions and Firestore database used by the app server.

For details, refer to the official Firebase guides:

Enable Firebase Cloud Functions

In the Firebase console, open the Functions section. If needed, switch to a billing plan (for example, Blaze) that allows the use of Cloud Functions. In the Functions section, confirm that Cloud Functions is enabled and that the Firebase CLI can deploy to this project.

For details, refer to the official Firebase guides:

Enable Firestore

Open the Firestore Database section and create a database if it is not already enabled. Choose an appropriate region, such as asia-northeast1, and set the initial security mode. This database will be used by the app server to store device registrations and call events.

For details, refer to the official Firebase guide:

Set up a push for the desired platform

For iOS and iPadOS, obtain an APNs Auth Key (.p8) from the Apple Developer portal, then configure FCM for Apple platforms. For Android, register the app in Firebase, download google-services.json, and configure FCM in the client project. For desktop or other platforms, decide whether to use FCM or Firestore-based notification.

For setup steps, refer to the official Firebase guides:

Deploy the app server

After implementing all the app server functions following the patterns introduced in the App server feature introduction section, you can now deploy the app server functions to Firebase and connect them to LINE Planet cloud.

Implement Cloud Functions

Referring to the code snippets introduced in the App server feature introduction section, implement the following Cloud Functions for Firebase in your project:

  • Client APIs
    • registerDevice (Firebase callable function)
    • registerDeviceHttp (HTTP with authentication)
    • removeDevice (Firebase callable function)
    • removeDeviceHttp (HTTP with authentication)
    • issuePlanetKitAccessToken (Firebase callable function)
    • issuePlanetKitAccessTokenHttp (HTTP with authentication)
    • issueAppServerAccessToken (HTTP)
  • Server callbacks
    • notifyCallback (HTTP GET endpoint)
    • callEventCallback (HTTP GET endpoint)
    • groupCallEventCallback (HTTP GET endpoint)
  • Required configuration
    • Configure server callback authentication header (key-value pair)
    • Configure service credentials (API key and API secret from LPC)
    • Configure APNs credentials for iOS push (if supporting iOS)
    • Set up Firestore collections for device registration and event logging

Deploy Cloud Functions

Run the following command in the functions directory using Firebase CLI to deploy all functions:

firebase deploy --only functions

To deploy only a specific function:

firebase deploy --only functions:registerDevice

Logs can be checked with the following command:

firebase functions:log

Register callbacks and the header

After deploying Cloud Functions, once the URLs are visible in the Functions tab of the Firebase console, you need to register the callbacks in LPC.

Firebase Functions tab

Open the callback configuration page for the service in LPC and set the following:

  • Set notifyCallback URL to the corresponding Cloud Function
  • Set callEventCallback URL to the corresponding Cloud Function
  • Set groupCallEventCallback URL if using group calls

After completing the configuration, set the authentication header to match the SERVER_CALLBACK_AUTH value in constants.js (for example, key planet and a secret value). After saving, LINE Planet cloud will be able to call the app server, and the app server will be able to verify that the requests are genuine.

LPC callback configuration

Integrating your client app with the app server

After completing deployment and callback registration, client applications can start using the app server.

Firebase SDK integration

On Android, include Firebase Functions and Messaging in Gradle. On iOS, include FirebaseCore, FirebaseFunctions, and FirebaseMessaging via CocoaPods (or Swift Package Manager). Then initialize Firebase and create a Functions instance in the appropriate region.

[CODE SNIPPET: Android Firebase dependencies]
implementation platform("com.google.firebase:firebase-bom:<VERSION>")
implementation "com.google.firebase:firebase-functions"
implementation "com.google.firebase:firebase-messaging"
[CODE SNIPPET: iOS Firebase dependencies]
pod 'Firebase/Core'
pod 'Firebase/Functions'
pod 'Firebase/Messaging'
[CODE SNIPPET: SwiftFunctions instance]
import FirebaseFunctions

let functions = Functions.functions(region: "asia-northeast1")
[CODE SNIPPET: Kotlin – Functions instance]
import com.google.firebase.functions.ktx.functions
import com.google.firebase.ktx.Firebase

val functions = Firebase.functions("asia-northeast1")

Swift/Kotlin example: Making calls (call creation and verification)

The basic pattern on the client side is simple:

  • Register the device once the user is logged in and a push token is available.
  • Before making or verifying a call, request a PlanetKit access token.
  • Pass the token and callback parameters to PlanetKit.

Swift example

[CODE SNIPPET: Swift – register device and issue token]

// On user login or push token update, call registerDevice.
let registerDevice = functions.httpsCallable("registerDevice")
let registerData: [String: Any] = [
"userId": "user123",
"serviceId": "<SERVICE_ID>",
"deviceType": "IOS",
"pushType": "PUSHKIT",
"deviceToken": "<PUSH_TOKEN>",
"pushkitEnvironment": "DEBUG"
]
registerDevice.call(registerData) { result, error in
// Handle result or error
}

// Before making or verifying a call, call issuePlanetKitAccessToken
let issueToken = functions.httpsCallable("issuePlanetKitAccessToken")
let tokenData: [String: Any] = [
"userId": "user123",
"serviceId": "<SERVICE_ID>"
]
issueToken.call(tokenData) { result, error in
// Extract "accessToken" and pass it to PlanetKit
}

Kotlin example

// On user login or push token update, call registerDevice.
val registerData = hashMapOf(
"userId" to "user123",
"serviceId" to "<SERVICE_ID>",
"deviceType" to "ANDROID",
"pushType" to "FCM",
"deviceToken" to "<FCM_TOKEN>"
)
functions
.getHttpsCallable("registerDevice")
.call(registerData)
.addOnSuccessListener { /* Handle success */ }
.addOnFailureListener { /* Handle error */ }

// Before making or verifying a call, call issuePlanetKitAccessToken
val tokenData = hashMapOf(
"userId" to "user123",
"serviceId" to "<SERVICE_ID>"
)
functions
.getHttpsCallable("issuePlanetKitAccessToken")
.call(tokenData)
.addOnSuccessListener { result ->
// Extract "accessToken" and pass it to PlanetKit
}
.addOnFailureListener { /* Handle error */ }

Incoming calls follow a similar pattern. LINE Planet cloud calls notifyCallback, the app server sends a push, the client receives it and displays an incoming call UI, then fetches a PlanetKit access token and verifies the call via PlanetKit.

For detailed PlanetKit usage, including how to use fields such as cc_param during verification, please refer to the PlanetKit SDK documentation.

Closing

This post introduced a high-level guide to implementing the app server required for LINE Planet usage using Firebase.

Implementing an app server using Firebase offers several key advantages:

  • Efficiency: You can skip many of the cumbersome tasks traditionally required for server setup and maintenance, such as server provisioning, OS management, and authentication.
  • Scalability: Firebase automatically scales your server resources up or down based on traffic (auto-scaling), ensuring stable service performance without manual intervention even during sudden traffic spikes.

These advantages allow developers to focus more on core business logic rather than spending time on infrastructure management. Although this guide focuses on Firebase, the architectural patterns and core logic can be applied to other serverless platforms such as AWS Lambda or Azure Functions, and you can adapt the API designs and callback flows to your preferred environment.

Finally, here's a summary of the key points from this post:

  • The app server's role
    • Provides client APIs
    • Receives callbacks from LINE Planet cloud
    • Delivers incoming call notifications to the callee (client) app via push
  • Using Cloud Functions for Firebase that can be called in two ways (HTTPS, Firebase callable functions), and using Cloud Firestore as the app server's data store
  • Minimal API design for app server features required by LINE Planet
    • Client APIs (RegisterDevice, RemoveDevice, IssuePlanetKitAccessToken, IssueAppServerAccessToken)
      • Includes code snippets for core validation, token generation, and Firestore operations
    • Server callbacks (notifyCallback, callEventCallback, groupCallEventCallback)
      • Includes code snippets for authentication, data validation, event logging, and push notification patterns
  • Concrete steps to prepare LPC and Firebase, implement the functions, deploy them, and register callbacks
  • Basic examples to understand how client apps can call the app server via Firebase SDK and use the results with PlanetKit

If you have any questions or feedback about the Firebase-based LINE Planet app server implementation guide introduced in this post, please contact us at the following email address: