Cross-platform Immich-compatible photo backup server.
This project implements a lightweight, Immich API-compatible server that runs on multiple platforms:
- Android (API 16+): Native APK with two UI variants
- Modern (API 21+): Jetpack Compose UI
- Legacy (API 16+): Traditional XML UI
- iOS: Framework for integration with SwiftUI
- JVM/Desktop: Command-line minimal version
immich-server/
├── shared/ ← Core shared code (Kotlin Multiplatform)
│ ├── api/ ← Ktor REST API routes (CIO engine, no native deps)
│ ├── model/ ← Data models (kotlinx.serialization)
│ ├── service/ ← Business logic
│ ├── db/ ← SQLDelight database (cross-platform SQLite)
│ ├── discovery/ ← UDP discovery protocol (v1.0 - v3.0)
│ └── platform/ ← Platform abstractions (expect/actual)
│
├── androidApp/ ← Android application
│ ├── modern/ ← Compose UI (API 21+, Android 5.0+)
│ └── legacy/ ← XML UI (API 16+, Android 4.1+)
│
├── iosApp/ ← iOS application (SwiftUI)
│
├── minimal/ ← Command-line JVM version
│
└── docs/ ← Documentation
│ ├── discovery-protocol.md ← UDP discovery protocol spec
│ └── api-spec.md ← API specification (TODO)
- API Compatible: Implements core Immich REST API
- Cross-Platform: Share 90%+ code across platforms
- Ultra-Low Android: Supports Android 4.1 (API 16) via Ktor CIO engine
- SQLite Database: SQLDelight for type-safe SQL
- File Storage: Platform-specific file management
- Background Service: Keep server running
- Secure Discovery: Token-based signing for secure server identification
| Variant | Min SDK | UI | Engine | Output |
|---|---|---|---|---|
| modernDebug | 21 (5.0) | Compose | Ktor CIO | app-modern-debug.apk |
| legacyDebug | 16 (4.1) | XML | Ktor CIO | app-legacy-debug.apk |
GET /api/server-infoGET /api/server-info/pingGET /api/server-info/version
POST /api/auth/login- User login (first user becomes admin)POST /api/auth/logout- User logoutGET /api/auth/admin-sign-up- Check if admin existsPOST /api/auth/admin-sign-up- Create admin userPOST /api/auth/token-exchange- Exchange server token (v3.0 discovery)
POST /api/assets- UploadGET /api/assets- ListGET /api/assets/:id- Download
GET /api/albumsPOST /api/albums
The server supports three versions of the UDP discovery protocol:
| Version | Features | Security Level |
|---|---|---|
| v1.0 | Basic discovery | None |
| v2.0 | Server ID matching | Basic |
| v3.0 | Token-based signing | Advanced |
| Concept | Description |
|---|---|
| Server ID | Unique UUID generated on first startup, stored in database |
| Server Token | 256-bit secret token for signing discovery responses |
| Server Name | User-configurable display name |
1. Client sends: DISCOVER_IMMICH_SERVER:<clientId>:<challengeNonce>
2. Server responds with signed JSON:
{
"serverId": "...",
"serverName": "...",
"serverUrl": "...",
"timestamp": ...,
"challengeNonce": "...",
"signature": "HMAC-SHA256(serverToken, data)"
}
3. Client verifies signature using saved serverToken
See docs/discovery-protocol.md for full specification.
-- User accounts
CREATE TABLE user (
id TEXT PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
password_hash TEXT NOT NULL,
is_admin INTEGER NOT NULL DEFAULT 0,
created_at INTEGER NOT NULL DEFAULT 0,
updated_at INTEGER NOT NULL DEFAULT 0
);
-- Server configuration (v2.0+ discovery)
CREATE TABLE server_config (
id INTEGER PRIMARY KEY,
server_id TEXT NOT NULL UNIQUE,
server_name TEXT NOT NULL DEFAULT 'Immich Android Server',
server_token TEXT NOT NULL, -- Encrypted, for v3.0 signing
created_at INTEGER NOT NULL DEFAULT 0,
updated_at INTEGER NOT NULL DEFAULT 0
);
-- API keys for authentication
CREATE TABLE api_key (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
name TEXT NOT NULL,
key TEXT NOT NULL UNIQUE,
created_at INTEGER NOT NULL DEFAULT 0
);- Server generates unique
serverId(UUID v4) - Server generates
serverToken(256-bit random) - Both stored in
server_configtable - First user to login is automatically created as admin
./gradlew :androidApp:assembleModernDebug./gradlew :androidApp:assembleLegacyDebug./gradlew :minimal:run./gradlew :shared:linkDebugFrameworkIosArm64- Install APK (modern or legacy variant)
- Open app, tap "Start Server"
- Server displays URL and unique server ID
- Connect with Immich client using displayed URL
- Client saves server ID for future auto-discovery
./gradlew :minimal:run
# Server starts on http://localhost:2283When server IP changes (e.g., home network DHCP):
- Client tries saved URL, connection fails
- Client sends discovery broadcast
- Server responds with new IP and signed response
- Client verifies signature, updates saved URL
- Automatic reconnect without user intervention
| Threat | Mitigation |
|---|---|
| Server spoofing | HMAC signature verification |
| Replay attacks | Challenge nonce matching |
| MITM on UDP | Token never sent over UDP |
| Unauthorized discovery | Token only after user auth |
- Server: Encrypted in SQLite database
- Client: Platform secure storage (Keychain/Keystore)
- Create new module (e.g.,
desktop/) - Implement
PlatformFileStorage,PlatformNotification,PlatformDatabaseDriverFactory - Add platform-specific UI
- Add route in
shared/src/commonMain/api/ - Add service method in
shared/src/commonMain/service/ - Update database schema if needed
- Update
DiscoveryProtocol.ktfor new message format - Update
DiscoveryServer.ktfor new response generation - Update
docs/discovery-protocol.mdwith new version spec
- Discovery Protocol Specification
- Storage Structure
- API Compliance Check
- Client-Server Communication
- API Specification (TODO: docs/api-spec.md)
AGPL-3.0