Implementing Access Control System (ACS) Management via Mobile Applications
ACS controllers — Suprema BioStar, HID OSDP, Ironlogic Z-5R, RusGuard — are managed via proprietary APIs or OSDP v2 standard. Mobile applications in such systems handle several unrelated tasks simultaneously: monitoring access events, managing permissions (add/remove card), remote door unlock, viewing entrance camera.
Integration with Controllers: REST API vs OSDP
Most modern ACS provide REST API over their database. Suprema BioStar 2 is typical: /api/v1/doors, /api/v1/events, /api/v1/users. Authentication via JWT with short TTL (usually 30 minutes), refresh via refresh_token.
Direct OSDP over TCP is rare in mobile apps — more for industrial integrators. Mobile clients always have an API layer.
Typical access event structure from BioStar 2 API:
data class AccessEvent(
val id: Long,
val timestamp: Instant,
val deviceId: Long,
val doorId: Long,
val userId: Long?, // null if card not recognized
val cardNumber: String?,
val eventCode: Int, // 0=access allowed, 1=denied, 2=exit
val temperature: Double?, // if terminal has temp sensor
val imageData: String?, // base64 photo from terminal camera
)
Live event stream via WebSocket (BioStar 2 supports /ws/events):
class AcsEventStream(private val url: String, private val token: String) {
fun observe(): Flow<AccessEvent> = callbackFlow {
val client = OkHttpClient.Builder()
.readTimeout(0, TimeUnit.MILLISECONDS)
.build()
val ws = client.newWebSocket(
Request.Builder().url(url)
.header("Authorization", "Bearer $token").build(),
object : WebSocketListener() {
override fun onMessage(webSocket: WebSocket, text: String) {
val event = Json.decodeFromString<AccessEvent>(text)
trySend(event)
}
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
close(t)
}
}
)
awaitClose { ws.close(1000, null) }
}
}
Remote Door Unlock
Most sensitive feature — door unlock via app button. Three requirements: request authorization, action audit, replay attack protection.
suspend fun unlockDoor(doorId: Long) {
// Sign request: timestamp + doorId + userId to prevent replay
val timestamp = System.currentTimeMillis()
val payload = "$timestamp:$doorId:${authService.userId}"
val signature = hmacSha256(payload, authService.apiSecret)
api.unlockDoor(DoorUnlockRequest(
doorId = doorId,
timestamp = timestamp,
signature = signature,
reason = "manual_unlock_mobile",
))
// Event logged on server automatically via API
}
On server: validate timestamp not older than 30 seconds, signature valid, user has permission for this door. Without these checks, intercepted request can be replayed.
Permission and Card Management
Assign card to user — typical HR/security task. Form in app: select user, input or read card number (NFC via android.nfc.NfcAdapter or CoreNFC on iOS), select doors and access schedule:
Future<void> assignCard(int userId, String cardNumber, List<int> doorIds,
AccessSchedule schedule) async {
await _api.post('/users/$userId/credentials', body: {
'type': 'card',
'card_number': cardNumber,
'doors': doorIds,
'schedule_id': schedule.id,
'valid_from': schedule.from.toIso8601String(),
'valid_until': schedule.until?.toIso8601String(),
});
}
NFC card number reading (Mifare Classic) — convenient: security guard applies card to phone instead of typing 10-digit number.
Photos and Video at Entrance
Terminals with cameras (Suprema FaceStation, ZKTeco SpeedFace) send photos in access event (base64 JPEG ~15-30 KB). Display in event feed via CachedNetworkImage or decode base64 directly. IP cameras (Hikvision, Dahua) via RTSP stream — Flutter via flutter_vlc_player or media_kit.
Developing mobile ACS app with event stream, remote door control and card management: 4–7 weeks. Cost calculated individually after analyzing specific system API and security requirements.







