Installation
Add InsForge dependencies to your project
Maven Central
GitHub Packages
build.gradle.kts: repositories {
mavenLocal () // For local development
mavenCentral ()
}
dependencies {
implementation ( "dev.insforge:insforge-kotlin:0.1.5" )
}
First, create a GitHub Personal Access Token:
Go to GitHub → Settings → Developer settings → Personal access tokens → Tokens (classic)
Select permission: read:packages
Then configure your project using one of the following methods:
Option A: Environment Variables
settings.gradle.kts (or build.gradle.kts): repositories {
mavenCentral ()
maven {
url = uri ( "https://maven.pkg.github.com/InsForge/insforge-kotlin" )
credentials {
username = System. getenv ( "GITHUB_USER" ) ?: ""
password = System. getenv ( "GITHUB_TOKEN" ) ?: ""
}
}
}
build.gradle.kts: dependencies {
implementation ( "dev.insforge:insforge-kotlin:0.1.3" )
}
Set environment variables before building: export GITHUB_USER = "your-github-username"
export GITHUB_TOKEN = "your-personal-access-token"
Option B: gradle.properties (Local Development)
Add credentials to your global Gradle properties file: ~/.gradle/gradle.properties: gpr.user =your-github-username
gpr.token =ghp_xxxxxxxxxxxx
settings.gradle.kts: repositories {
mavenCentral ()
maven {
url = uri ( "https://maven.pkg.github.com/InsForge/insforge-kotlin" )
credentials {
username = providers. gradleProperty ( "gpr.user" ).orNull ?: ""
password = providers. gradleProperty ( "gpr.token" ).orNull ?: ""
}
}
}
build.gradle.kts: dependencies {
implementation ( "dev.insforge:insforge-kotlin:0.1.1" )
}
The ~/.gradle/gradle.properties file is stored outside your project, so credentials won’t be accidentally committed to version control.
Initialize InsForge SDK
import dev.insforge.createInsforgeClient
import dev.insforge.auth.Auth
import dev.insforge.database.Database
import dev.insforge.storage.Storage
import dev.insforge.functions.Functions
import dev.insforge.realtime.Realtime
import dev.insforge.ai.AI
val client = createInsforgeClient (
baseUrl = "https://your-app.insforge.app" ,
anonKey = "your-api-key"
) {
install (Auth)
install (Database)
install (Storage)
install (Functions)
install (Realtime) {
autoReconnect = true
reconnectDelay = 5000
}
install (AI)
}
Enable Logging (Optional)
For debugging, you can configure the SDK log level:
import dev.insforge.InsforgeLogLevel
val client = createInsforgeClient (
baseUrl = "https://your-app.insforge.app" ,
anonKey = "your-api-key"
) {
// DEBUG: logs request method/URL and response status
// VERBOSE: logs full headers and request/response bodies
logLevel = InsforgeLogLevel.DEBUG
install (Auth)
install (Database)
// ... other modules
}
Log Level Description NONENo logging (default, recommended for production) ERROROnly errors WARNWarnings and errors INFOInformational messages DEBUGDebug info (request method, URL, response status) VERBOSEFull details (headers, request/response bodies)
Use NONE or ERROR in production to avoid exposing sensitive data in logs.
Android Initialization
Initialize InsForge SDK (With Local Storage and Browser Launcher for OAuth)
import android.content.Context
import android.content.Intent
import android.net.Uri
import dev.insforge.createInsforgeClient
import dev.insforge.ai.AI
import dev.insforge.auth.Auth
import dev.insforge.database.Database
import dev.insforge.functions.Functions
import dev.insforge.realtime.Realtime
import dev.insforge.storage.Storage
import dev.insforge.auth.BrowserLauncher
import dev.insforge.auth.SessionStorage
class InsforgeManager ( private val context: Context ) {
val client = createInsforgeClient (
baseUrl = "https://your-app.insforge.app" ,
anonKey = "your-anon-key"
) {
install (Auth) {
// 1. config BrowserLauncher (for OAuth)
browserLauncher = BrowserLauncher { url ->
val intent = Intent (Intent.ACTION_VIEW, Uri. parse (url))
intent. addFlags (Intent.FLAG_ACTIVITY_NEW_TASK)
context. startActivity (intent)
}
// 2. enable session persistence
persistSession = true
// 3. config SessionStorage (use SharedPreferences)
sessionStorage = object : SessionStorage {
private val prefs = context. getSharedPreferences (
"insforge_auth" ,
Context.MODE_PRIVATE
)
override suspend fun save (key: String , value : String ) {
prefs. edit (). putString (key, value ). apply ()
}
override suspend fun get (key: String ): String ? {
return prefs. getString (key, null )
}
override suspend fun remove (key: String ) {
prefs. edit (). remove (key). apply ()
}
}
}
// Install Database module
install (Database)
// Install Realtime module for real-time subscriptions
install (Realtime) {
debug = true
}
// Install other modules
install (Storage)
install (Functions)
install (AI)
}
}
Use Jetpack DataStore for Session Storage (Optional)
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
val Context.authDataStore: DataStore < Preferences > by preferencesDataStore (name = "insforge_auth" )
class DataStoreSessionStorage ( private val context: Context ) : SessionStorage {
override suspend fun save (key: String , value : String ) {
context.authDataStore. edit { prefs ->
prefs[ stringPreferencesKey (key)] = value
}
}
override suspend fun get (key: String ): String ? {
return context.authDataStore. data . map { prefs ->
prefs[ stringPreferencesKey (key)]
}. first ()
}
override suspend fun remove (key: String ) {
context.authDataStore. edit { prefs ->
prefs. remove ( stringPreferencesKey (key))
}
}
}
// Then use it in your InsForge client
install (Auth) {
browserLauncher = .. .
persistSession = true
sessionStorage = DataStoreSessionStorage (context)
}
connect()
Establish a WebSocket connection to the realtime server.
Example
// Connect to Realtime server
client.realtime. connect ()
// Check connection status
println ( "Connected: ${ client.realtime.isConnected } " )
println ( "Socket ID: ${ client.realtime.socketId } " )
Monitor Connection State
// Monitor connection state using Flow
client.realtime.connectionState. collect { state ->
when (state) {
is Realtime.ConnectionState.Connected -> println ( "Connected" )
is Realtime.ConnectionState.Disconnected -> println ( "Disconnected" )
is Realtime.ConnectionState.Connecting -> println ( "Connecting..." )
is Realtime.ConnectionState.Error -> println ( "Error: ${ state.message } " )
}
}
disconnect()
Disconnect from the realtime server.
Example
client.realtime. disconnect ()
// Verify disconnection
println ( "Connected: ${ client.realtime.isConnected } " ) // false
println ( "Socket ID: ${ client.realtime.socketId } " ) // null
subscribe()
Subscribe to a channel to receive events.
Parameters
channel (String) - The channel name to subscribe to
Returns
SubscribeResponse (
ok: Boolean ,
channel: String ,
error: SubscribeError ? // null if ok is true
)
Example
client.realtime. connect ()
val response = client.realtime. subscribe ( "todos" )
if (response.ok) {
println ( "Subscribed to channel: ${ response.channel } " )
} else {
println ( "Subscribe failed: ${ response.error?.message } " )
}
Auto-Connect
If not connected, subscribe() will automatically establish a connection first:
// No need to call connect() first
val response = client.realtime. subscribe ( "todos" )
// Client is now connected and subscribed
println ( "Connected: ${ client.realtime.isConnected } " ) // true
Subscribe to Multiple Channels
client.realtime. connect ()
val todosResponse = client.realtime. subscribe ( "todos" )
val chatResponse = client.realtime. subscribe ( "chat" )
val notificationsResponse = client.realtime. subscribe ( "notifications" )
// Get list of subscribed channels
val channels = client.realtime. getSubscribedChannels ()
println ( "Subscribed channels: $channels " )
unsubscribe()
Unsubscribe from a channel.
Example
client.realtime. unsubscribe ( "todos" )
// Verify unsubscription
val channels = client.realtime. getSubscribedChannels ()
println ( "Still subscribed: ${ channels. contains ( "todos" ) } " ) // false
on()
Register an event listener for a specific event type.
Parameters
event (String) - The event name to listen for (e.g., “INSERT”, “UPDATE”, “DELETE”)
callback (EventCallback<T>) - Callback function that receives the event data
Example (Listen for INSERT Events)
import dev.insforge.realtime.models.SocketMessage
client.realtime. connect ()
client.realtime. subscribe ( "todos" )
// Register INSERT event listener
client.realtime. on < SocketMessage >( "INSERT" ) { message ->
message?. let {
println ( "INSERT event received:" )
println ( " Channel: ${ it.channel } " )
println ( " MessageId: ${ it.messageId } " )
println ( " Payload: ${ it.payload } " )
}
}
Example (Listen for UPDATE Events)
client.realtime. on < SocketMessage >( "UPDATE" ) { message ->
message?. let {
println ( "UPDATE event received:" )
println ( " Channel: ${ it.channel } " )
println ( " Payload: ${ it.payload } " )
}
}
Example (Listen for DELETE Events)
client.realtime. on < SocketMessage >( "DELETE" ) { message ->
message?. let {
println ( "DELETE event received:" )
println ( " Channel: ${ it.channel } " )
println ( " Payload: ${ it.payload } " )
}
}
Example (Listen for Multiple Event Types)
client.realtime. connect ()
client.realtime. subscribe ( "todos" )
// Register listeners for all CRUD events
listOf ( "INSERT" , "UPDATE" , "DELETE" ). forEach { eventType ->
client.realtime. on < SocketMessage >(eventType) { message ->
message?. let {
println ( " $eventType event: ${ it.payload } " )
}
}
}
off()
Remove an event listener.
Example
val callback = Realtime. EventCallback < SocketMessage > { message ->
println ( "Event received: $message " )
}
// Register listener
client.realtime. on ( "INSERT" , callback)
// Later, remove the listener
client.realtime. off ( "INSERT" , callback)
once()
Register a one-time event listener that automatically removes itself after the first event.
Example
client.realtime. once < SocketMessage >( "INSERT" ) { message ->
println ( "Received first INSERT event: ${ message?.payload } " )
// Listener is automatically removed after this
}
publish()
Publish a message to a channel.
You must be subscribed to a channel before publishing messages to it.
Parameters
channel (String) - The channel name to publish to
event (String) - The event name
payload (Any) - The message payload
Example
client.realtime. connect ()
client.realtime. subscribe ( "chat" )
// Publish a message
client.realtime. publish (
channel = "chat" ,
event = "message" ,
payload = mapOf (
"text" to "Hello from Kotlin SDK" ,
"timestamp" to System. currentTimeMillis ()
)
)
Error Handling
// Publishing without connection throws an error
try {
client.realtime. publish ( "channel" , "event" , mapOf ( "test" to "data" ))
} catch (e: IllegalStateException ) {
println ( "Error: ${ e.message } " ) // Not connected
}
getSubscribedChannels()
Get the list of currently subscribed channels.
Example
client.realtime. connect ()
client.realtime. subscribe ( "todos" )
client.realtime. subscribe ( "chat" )
val channels = client.realtime. getSubscribedChannels ()
println ( "Subscribed to: $channels " ) // [todos, chat]
Database Change Monitoring
Listen to real-time database changes triggered by INSERT, UPDATE, and DELETE operations.
Complete CRUD Example
import dev.insforge.realtime.models.SocketMessage
import kotlinx.serialization.json.buildJsonArray
import kotlinx.serialization.json.addJsonObject
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
// Connect and subscribe
client.realtime. connect ()
client.realtime. subscribe ( "todos" )
// Track all events
val allEvents = mutableListOf < Pair < String , SocketMessage >>()
// Register listeners for all event types
listOf ( "INSERT" , "UPDATE" , "DELETE" ). forEach { eventType ->
client.realtime. on < SocketMessage >(eventType) { message ->
message?. let {
println ( " $eventType event received:" )
println ( " Payload: ${ it.payload } " )
allEvents. add (eventType to it)
}
}
}
// Step 1: INSERT
val insertData = buildJsonArray {
addJsonObject {
put ( "title" , "New Todo" )
put ( "is_completed" , false )
put ( "user_id" , userId)
}
}
val inserted = client.database. from ( "todos" )
. insert (insertData)
. returning ()
. execute < JsonObject >()
val todoId = inserted. first ()[ "id" ]. toString (). removeSurrounding ( " \" " )
// Step 2: UPDATE
val updateData = buildJsonObject {
put ( "title" , "Updated Todo" )
put ( "is_completed" , true )
}
client.database. from ( "todos" )
. eq ( "id" , todoId)
. update (updateData)
. execute < JsonObject >()
// Step 3: DELETE
client.database. from ( "todos" )
. eq ( "id" , todoId)
. delete ()
. execute < JsonObject >()
// Summary
println ( "Total events: ${ allEvents.size } " )
println ( "INSERT: ${ allEvents. count { it.first == "INSERT" } }" )
println ( "UPDATE: ${ allEvents. count { it.first == "UPDATE" } }" )
println ( "DELETE: ${ allEvents. count { it.first == "DELETE" } }" )
// Cleanup
client.realtime. disconnect ()
Monitor INSERT Events
client.realtime. connect ()
client.realtime. subscribe ( "todos" )
client.realtime. on < SocketMessage >( "INSERT" ) { message ->
message?. let {
println ( "New todo created:" )
println ( " ${ it.payload } " )
}
}
// Insert a new todo
val insertData = buildJsonArray {
addJsonObject {
put ( "title" , "New Todo ${ System. currentTimeMillis () } " )
put ( "is_completed" , false )
put ( "user_id" , userId)
}
}
client.database. from ( "todos" )
. insert (insertData)
. returning ()
. execute < JsonObject >()
Monitor UPDATE Events
client.realtime. connect ()
client.realtime. subscribe ( "todos" )
client.realtime. on < SocketMessage >( "UPDATE" ) { message ->
message?. let {
println ( "Todo updated:" )
println ( " ${ it.payload } " )
}
}
// Update an existing todo
val updateData = buildJsonObject {
put ( "title" , "Updated Title" )
put ( "is_completed" , true )
}
client.database. from ( "todos" )
. eq ( "id" , todoId)
. update (updateData)
. execute < JsonObject >()
Monitor DELETE Events
client.realtime. connect ()
client.realtime. subscribe ( "todos" )
client.realtime. on < SocketMessage >( "DELETE" ) { message ->
message?. let {
println ( "Todo deleted:" )
println ( " ${ it.payload } " )
}
}
// Delete a todo
client.database. from ( "todos" )
. eq ( "id" , todoId)
. delete ()
. execute < JsonObject >()
Jetpack Compose Integration
Real-time Todo List
@Composable
fun RealtimeTodoScreen () {
val viewModel: TodoViewModel = viewModel ()
val todos by viewModel.todos. collectAsState ()
val isConnected by viewModel.isConnected. collectAsState ()
LaunchedEffect (Unit) {
viewModel. connect ()
}
DisposableEffect (Unit) {
onDispose {
viewModel. disconnect ()
}
}
Column (modifier = Modifier. fillMaxSize ()) {
// Connection status
if ( ! isConnected) {
Text (
"Connecting..." ,
modifier = Modifier
. fillMaxWidth ()
. background (Color.Yellow)
. padding ( 8 .dp)
)
}
LazyColumn {
items (todos) { todo ->
TodoItem (todo = todo)
}
}
}
}
class TodoViewModel : ViewModel () {
private val _todos = MutableStateFlow < List < Todo >>( emptyList ())
val todos: StateFlow < List < Todo >> = _todos. asStateFlow ()
private val _isConnected = MutableStateFlow ( false )
val isConnected: StateFlow < Boolean > = _isConnected. asStateFlow ()
@Serializable
data class Todo (
val id: String ,
val title: String ,
@SerialName ( "is_completed" ) val isCompleted: Boolean
)
fun connect () {
viewModelScope. launch {
client.realtime. connect ()
client.realtime. subscribe ( "todos" )
_isConnected. value = true
// Listen for INSERT events
client.realtime. on < SocketMessage >( "INSERT" ) { message ->
message?. let {
// Parse and add new todo
val payload = it.payload
// Update _todos
}
}
// Listen for UPDATE events
client.realtime. on < SocketMessage >( "UPDATE" ) { message ->
message?. let {
// Parse and update todo in list
}
}
// Listen for DELETE events
client.realtime. on < SocketMessage >( "DELETE" ) { message ->
message?. let {
// Remove todo from list
}
}
}
}
fun disconnect () {
viewModelScope. launch {
client.realtime. disconnect ()
_isConnected. value = false
}
}
}
Chat Room with Publish/Subscribe
@Composable
fun ChatRoomScreen (roomId: String ) {
val viewModel: ChatViewModel = viewModel ()
val messages by viewModel.messages. collectAsState ()
val isConnected by viewModel.isConnected. collectAsState ()
var inputText by remember { mutableStateOf ( "" ) }
DisposableEffect (roomId) {
viewModel. connect (roomId)
onDispose {
viewModel. disconnect ()
}
}
Column (modifier = Modifier. fillMaxSize ()) {
// Connection status
if ( ! isConnected) {
Text (
"Connecting..." ,
modifier = Modifier
. fillMaxWidth ()
. background (Color.Yellow)
. padding ( 8 .dp)
)
}
// Messages list
LazyColumn (
modifier = Modifier
. weight ( 1f )
. padding ( 16 .dp),
reverseLayout = true
) {
items (messages. reversed ()) { message ->
ChatMessageItem (message = message)
}
}
// Input field
Row (
modifier = Modifier
. fillMaxWidth ()
. padding ( 16 .dp)
) {
OutlinedTextField (
value = inputText,
onValueChange = { inputText = it },
modifier = Modifier. weight ( 1f ),
placeholder = { Text ( "Message" ) }
)
Spacer (modifier = Modifier. width ( 8 .dp))
IconButton (
onClick = {
viewModel. sendMessage (inputText)
inputText = ""
},
enabled = inputText. isNotBlank () && isConnected
) {
Icon (Icons.Default.Send, "Send" )
}
}
}
}
class ChatViewModel : ViewModel () {
private val _messages = MutableStateFlow < List < ChatMessage >>( emptyList ())
val messages: StateFlow < List < ChatMessage >> = _messages. asStateFlow ()
private val _isConnected = MutableStateFlow ( false )
val isConnected: StateFlow < Boolean > = _isConnected. asStateFlow ()
private var channelName: String ? = null
data class ChatMessage (
val sender: String ,
val text: String ,
val timestamp: Long
)
fun connect (roomId: String ) {
channelName = "chat: $roomId "
viewModelScope. launch {
try {
val response = client.realtime. subscribe (channelName !! )
if (response.ok) {
_isConnected. value = true
// Listen for new messages
client.realtime. on < SocketMessage >( "message" ) { message ->
message?. let {
// Parse message from payload and add to list
val payload = it.payload
// _messages.value = _messages.value + parsedMessage
}
}
}
} catch (e: Exception ) {
Log. e ( "Chat" , "Connection error: ${ e.message } " )
}
}
}
fun sendMessage (text: String ) {
val channel = channelName ?: return
viewModelScope. launch {
try {
client.realtime. publish (
channel = channel,
event = "message" ,
payload = mapOf (
"sender" to currentUser.name,
"text" to text,
"timestamp" to System. currentTimeMillis ()
)
)
} catch (e: Exception ) {
Log. e ( "Chat" , "Send failed: ${ e.message } " )
}
}
}
fun disconnect () {
viewModelScope. launch {
channelName?. let { client.realtime. unsubscribe (it) }
client.realtime. disconnect ()
_isConnected. value = false
}
}
}
API Reference
Realtime Class
Property / Method Type Description connect()suspend funConnect to WebSocket server disconnect()suspend funDisconnect from server subscribe(channel)suspend funSubscribe to a channel unsubscribe(channel)funUnsubscribe from a channel publish(channel, event, payload)suspend funPublish message to channel on(event, callback)funRegister event listener off(event, callback)funRemove event listener once(event, callback)funRegister one-time listener getSubscribedChannels()funGet subscribed channel list isConnectedBooleanCurrent connection status socketIdString?Current socket ID connectionStateStateFlow<ConnectionState>Connection state Flow
ConnectionState
State Description DisconnectedNot connected to server ConnectingConnection in progress ConnectedConnected to server Error(message)Connection error occurred
SubscribeResponse
data class SubscribeResponse (
val ok: Boolean ,
val channel: String ,
val error: SubscribeError ?
)
data class SubscribeError (
val code: String ,
val message: String
)
SocketMessage
data class SocketMessage (
val channel: String ,
val messageId: String ,
val senderType: String ,
val senderId: String ?,
val payload: JsonObject
)
EventCallback
fun interface EventCallback < T > {
fun onEvent ( data : T ?)
}
Best Practices
Always disconnect when done : Call disconnect() in onDispose or onDestroy to clean up resources.
Check subscribe response : Always verify response.ok before assuming subscription succeeded.
Handle connection state : Monitor connectionState Flow to handle reconnection scenarios.
Use typed callbacks : Specify the expected type in on<T>() for type-safe event handling.
Subscribe before publish : You must subscribe to a channel before publishing messages to it.
Remove listeners when not needed : Use off() to remove listeners and prevent memory leaks.
Error Handling
import dev.insforge.exceptions.InsforgeException
try {
client.realtime. connect ()
val response = client.realtime. subscribe ( "todos" )
if ( ! response.ok) {
when (response.error?.code) {
"UNAUTHORIZED" -> println ( "Not authorized to subscribe" )
"CHANNEL_NOT_FOUND" -> println ( "Channel does not exist" )
else -> println ( "Subscribe failed: ${ response.error?.message } " )
}
}
} catch (e: InsforgeException ) {
println ( "Realtime error: ${ e.message } " )
} catch (e: IllegalStateException ) {
println ( "Invalid state: ${ e.message } " )
}