diff --git a/api/build.gradle.kts b/api/build.gradle.kts
new file mode 100644
index 0000000000000000000000000000000000000000..ff29e76dea2e4ed3ca65fec4f9a3968441372d26
--- /dev/null
+++ b/api/build.gradle.kts
@@ -0,0 +1,44 @@
+@file:Suppress("UnstableApiUsage")
+
+/*
+ * Quasseldroid - Quassel client for Android
+ *
+ * Copyright (c) 2019 Janne Mareike Koschinski
+ * Copyright (c) 2019 The Quassel Project
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3 as published
+ * by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+plugins {
+  id("justjanne.kotlin")
+  alias(libs.plugins.kotlin.serialization)
+}
+
+dependencies {
+  implementation(libs.kotlin.stdlib)
+
+  implementation(libs.kotlinx.coroutines.core)
+  testImplementation(libs.kotlinx.coroutines.test)
+
+  testImplementation(libs.kotlin.test)
+  testImplementation(libs.junit.api)
+  testImplementation(libs.junit.params)
+  testRuntimeOnly(libs.junit.engine)
+
+  implementation(libs.kotlinx.datetime)
+
+  implementation(libs.okhttp)
+  implementation(libs.retrofit.core)
+  implementation(libs.retrofit.converter.kotlinx)
+  implementation(libs.kotlinx.serialization.json)
+}
diff --git a/api/proguard-rules.pro b/api/proguard-rules.pro
new file mode 100644
index 0000000000000000000000000000000000000000..690a4be68db285dea8841626be81fa215a589a84
--- /dev/null
+++ b/api/proguard-rules.pro
@@ -0,0 +1,54 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in /usr/lib/android-sdk/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.kts.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
+
+# The project is open source anyway, obfuscation is useless.
+-dontobfuscate
+
+# remove unnecessary warnings
+# Android HTTP Libs
+-dontnote android.net.http.**
+-dontnote org.apache.http.**
+# Kotlin stuff
+-dontnote kotlin.**
+# Gson
+-dontnote com.google.gson.**
+# Dagger
+-dontwarn com.google.errorprone.annotations.*
+# Retrofit
+-dontwarn retrofit2.**
+# Annotation used by Retrofit on Java 8 VMs
+-dontwarn javax.annotation.Nullable
+-dontwarn javax.annotation.ParametersAreNonnullByDefault
+-dontwarn javax.annotation.concurrent.GuardedBy
+# Retain generic type information for use by reflection by converters and adapters.
+-keepattributes Signature
+# Retain declared checked exceptions for use by a Proxy instance.
+-keepattributes Exceptions
+# Okio
+-dontwarn okio.**
+-dontwarn org.conscrypt.**
+# OkHttp3
+-dontwarn okhttp3.**
diff --git a/api/src/main/kotlin/de/chaosdorf/mete/BarcodeId.kt b/api/src/main/kotlin/de/chaosdorf/mete/BarcodeId.kt
new file mode 100644
index 0000000000000000000000000000000000000000..61245654db6684cb8eaf6456a705f464cfe75d37
--- /dev/null
+++ b/api/src/main/kotlin/de/chaosdorf/mete/BarcodeId.kt
@@ -0,0 +1,31 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.mete
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+@JvmInline
+value class BarcodeId(val value: Long)
diff --git a/api/src/main/kotlin/de/chaosdorf/mete/DrinkId.kt b/api/src/main/kotlin/de/chaosdorf/mete/DrinkId.kt
new file mode 100644
index 0000000000000000000000000000000000000000..a7ce209dc7fb65fc629fa0ff8d3879134f861b90
--- /dev/null
+++ b/api/src/main/kotlin/de/chaosdorf/mete/DrinkId.kt
@@ -0,0 +1,31 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.mete
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+@JvmInline
+value class DrinkId(val value: Long)
diff --git a/api/src/main/kotlin/de/chaosdorf/mete/PwaIcon.kt b/api/src/main/kotlin/de/chaosdorf/mete/PwaIcon.kt
new file mode 100644
index 0000000000000000000000000000000000000000..7734a53237cc1ac1025219e912cfc7047ef4cc47
--- /dev/null
+++ b/api/src/main/kotlin/de/chaosdorf/mete/PwaIcon.kt
@@ -0,0 +1,34 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.mete
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class PwaIcon(
+  val src: String?,
+  val sizes: String?,
+  val type: String?
+)
diff --git a/api/src/main/kotlin/de/chaosdorf/mete/PwaManifest.kt b/api/src/main/kotlin/de/chaosdorf/mete/PwaManifest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..2732335e5c80baef808284b78648b854f3f1a789
--- /dev/null
+++ b/api/src/main/kotlin/de/chaosdorf/mete/PwaManifest.kt
@@ -0,0 +1,40 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.mete
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class PwaManifest(
+  val name: String?,
+  @SerialName("short_name")
+  val shortName: String?,
+  val icons: List<PwaIcon>,
+  val display: String?,
+  @SerialName("start_url")
+  val startUrl: String?,
+  val scope: String?
+)
diff --git a/api/src/main/kotlin/de/chaosdorf/mete/UserId.kt b/api/src/main/kotlin/de/chaosdorf/mete/UserId.kt
new file mode 100644
index 0000000000000000000000000000000000000000..1f72b510a21243674af3e290c5aee0be9fef9541
--- /dev/null
+++ b/api/src/main/kotlin/de/chaosdorf/mete/UserId.kt
@@ -0,0 +1,31 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.mete
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+@JvmInline
+value class UserId(val value: Long)
diff --git a/api/src/main/kotlin/de/chaosdorf/mete/v1/AuditEntryModelV1.kt b/api/src/main/kotlin/de/chaosdorf/mete/v1/AuditEntryModelV1.kt
new file mode 100644
index 0000000000000000000000000000000000000000..8ba703392c092f96a4e70d126a34507e513b8905
--- /dev/null
+++ b/api/src/main/kotlin/de/chaosdorf/mete/v1/AuditEntryModelV1.kt
@@ -0,0 +1,36 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.mete.v1
+
+import de.chaosdorf.mete.DrinkId
+import de.chaosdorf.mete.UserId
+import kotlinx.datetime.Instant
+
+data class AuditEntryModelV1(
+  val difference: Double,
+  val drink: DrinkId,
+  val user: UserId,
+  val createdAt: Instant
+)
diff --git a/api/src/main/kotlin/de/chaosdorf/mete/v1/BarcodeModelV1.kt b/api/src/main/kotlin/de/chaosdorf/mete/v1/BarcodeModelV1.kt
new file mode 100644
index 0000000000000000000000000000000000000000..93f8461efe0f3e6e8e93d758ea22ed98cda0e4b3
--- /dev/null
+++ b/api/src/main/kotlin/de/chaosdorf/mete/v1/BarcodeModelV1.kt
@@ -0,0 +1,35 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.mete.v1
+
+import de.chaosdorf.mete.BarcodeId
+import de.chaosdorf.mete.DrinkId
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class BarcodeModelV1(
+  val id: BarcodeId,
+  val drink: DrinkId,
+)
diff --git a/api/src/main/kotlin/de/chaosdorf/mete/v1/DrinkModelV1.kt b/api/src/main/kotlin/de/chaosdorf/mete/v1/DrinkModelV1.kt
new file mode 100644
index 0000000000000000000000000000000000000000..bba984b3d95d0622a87dd7b238c5ab85b028db72
--- /dev/null
+++ b/api/src/main/kotlin/de/chaosdorf/mete/v1/DrinkModelV1.kt
@@ -0,0 +1,55 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.mete.v1
+
+import de.chaosdorf.mete.DrinkId
+import kotlinx.datetime.Instant
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class DrinkModelV1(
+  val id: DrinkId,
+  val name: String,
+  @SerialName("bottle_size")
+  val bottleSize: Double,
+  val caffeine: Int?,
+  val price: Double,
+  @SerialName("logo_file_name")
+  val logoFileName: String,
+  @SerialName("created_at")
+  val createdAt: Instant,
+  @SerialName("updated_at")
+  val updatedAt: Instant,
+  @SerialName("logo_content_type")
+  val logoContentType: String,
+  @SerialName("logo_file_size")
+  val logoFileSize: Long,
+  @SerialName("logo_updated_at")
+  val logoUpdatedAt: Instant,
+  @SerialName("logo_url")
+  val logoUrl: String,
+  val active: Boolean
+)
diff --git a/api/src/main/kotlin/de/chaosdorf/mete/v1/MeteApiFactory.kt b/api/src/main/kotlin/de/chaosdorf/mete/v1/MeteApiFactory.kt
new file mode 100644
index 0000000000000000000000000000000000000000..a120e4eeecd292d1a1e4ee8b843ca4c1b797049a
--- /dev/null
+++ b/api/src/main/kotlin/de/chaosdorf/mete/v1/MeteApiFactory.kt
@@ -0,0 +1,29 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.mete.v1
+
+interface MeteApiFactory<T> {
+  fun newInstance(baseUrl: String): T
+}
diff --git a/api/src/main/kotlin/de/chaosdorf/mete/v1/MeteApiV1.kt b/api/src/main/kotlin/de/chaosdorf/mete/v1/MeteApiV1.kt
new file mode 100644
index 0000000000000000000000000000000000000000..0a1b1ee34a1c7568fcf9c1c2546aac1cce7eeae8
--- /dev/null
+++ b/api/src/main/kotlin/de/chaosdorf/mete/v1/MeteApiV1.kt
@@ -0,0 +1,72 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.mete.v1
+
+import de.chaosdorf.mete.DrinkId
+import de.chaosdorf.mete.PwaManifest
+import de.chaosdorf.mete.UserId
+import retrofit2.http.GET
+import retrofit2.http.Path
+
+interface MeteApiV1 {
+  @GET("manifest.json")
+  suspend fun getManifest(): PwaManifest?
+
+  @GET("api/v1/audits.json")
+  suspend fun getAudits(): List<AuditEntryModelV1>
+
+  @GET("api/v1/barcodes.json")
+  suspend fun listBarcodes(): List<BarcodeModelV1>
+
+  @GET("api/v1/barcodes/{id}.json")
+  suspend fun getBarcode(): BarcodeModelV1?
+
+  @GET("api/v1/drinks.json")
+  suspend fun listDrinks(): List<DrinkModelV1>
+
+  @GET("api/v1/drinks/{id}.json")
+  suspend fun getDrink(@Path("id") id: DrinkId): DrinkModelV1?
+
+  @GET("api/v1/users.json")
+  suspend fun listUsers(): List<UserModelV1>
+
+  @GET("api/v1/users/{id}.json")
+  suspend fun getUser(@Path("id") id: UserId): UserModelV1?
+
+  @GET("api/v1/users/{id}/deposit.json")
+  suspend fun deposit(@Path("id") id: UserId)
+
+  @GET("api/v1/users/{id}/payment.json")
+  suspend fun payment(@Path("id") id: UserId)
+
+  @GET("api/v1/users/{id}/buy.json")
+  suspend fun buy(@Path("id") id: UserId)
+
+  @GET("api/v1/users/{id}/buy_barcode.json")
+  suspend fun buyWithBarcode(@Path("id") id: UserId)
+
+  @GET("api/v1/users/stats.json")
+  suspend fun getStats()
+}
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/Screen.kt b/api/src/main/kotlin/de/chaosdorf/mete/v1/MeteApiV1Factory.kt
similarity index 59%
rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/Screen.kt
rename to api/src/main/kotlin/de/chaosdorf/mete/v1/MeteApiV1Factory.kt
index 1563705d5d29fdc2215887481e9f7fe7dd09fd86..2e51fec5877d10c5a8885ea4b1103471d25d1e55 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/Screen.kt
+++ b/api/src/main/kotlin/de/chaosdorf/mete/v1/MeteApiV1Factory.kt
@@ -22,33 +22,25 @@
  * THE SOFTWARE.
  */
 
-package de.chaosdorf.meteroid.ui
+package de.chaosdorf.mete.v1
 
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.outlined.Person
-import androidx.compose.material.icons.twotone.Person
-import androidx.compose.ui.graphics.vector.ImageVector
-import de.chaosdorf.meteroid.icons.MeteroidIcons
-import de.chaosdorf.meteroid.icons.outlined.WaterFull
-import de.chaosdorf.meteroid.icons.twotone.WaterFull
+import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
+import kotlinx.serialization.json.Json
+import okhttp3.MediaType.Companion.toMediaType
+import retrofit2.Retrofit
+import retrofit2.create
 
-sealed class Screen(
-    val label: String,
-    val route: String,
-    val icon: ImageVector,
-    val selectedIcon: ImageVector = icon
-) {
-  object Drinks : Screen(
-    "Drinks",
-    "drinks",
-    MeteroidIcons.Outlined.WaterFull,
-    MeteroidIcons.TwoTone.WaterFull
-  )
+object MeteApiV1Factory : MeteApiFactory<MeteApiV1> {
+  private val json = Json {
+    ignoreUnknownKeys = true
+  }
 
-  object Users : Screen(
-    "Users",
-    "users",
-    Icons.Outlined.Person,
-    Icons.TwoTone.Person
-  )
+  override fun newInstance(baseUrl: String): MeteApiV1 {
+    val contentType = "application/json".toMediaType()
+    val retrofit = Retrofit.Builder()
+      .baseUrl(baseUrl)
+      .addConverterFactory(json.asConverterFactory(contentType))
+      .build()
+    return retrofit.create<MeteApiV1>()
+  }
 }
diff --git a/api/src/main/kotlin/de/chaosdorf/mete/v1/UserModelV1.kt b/api/src/main/kotlin/de/chaosdorf/mete/v1/UserModelV1.kt
new file mode 100644
index 0000000000000000000000000000000000000000..a7feac18f7201d54eaf062963573813dee5f99bf
--- /dev/null
+++ b/api/src/main/kotlin/de/chaosdorf/mete/v1/UserModelV1.kt
@@ -0,0 +1,42 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.mete.v1
+
+import de.chaosdorf.mete.UserId
+import kotlinx.datetime.Instant
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class UserModelV1(
+  val id: UserId,
+  val name: String,
+  val email: String,
+  val createdAt: Instant,
+  val updatedAt: Instant,
+  val balance: Double,
+  val active: Boolean,
+  val audit: Boolean,
+  val redirect: Boolean
+)
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 18766b3e1bb6a0a59472ababca79ebd2992fb872..b64fb07f37feb6cce7896abcc69f783ccbe04dd6 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -76,8 +76,18 @@ dependencies {
   implementation(libs.androidx.compose.runtime)
   implementation(libs.androidx.compose.ui.tooling)
 
+  implementation(libs.androidx.room.runtime)
+  implementation(libs.androidx.room.ktx)
+  implementation(libs.androidx.room.paging)
+
   implementation(libs.androidx.navigation.compose)
 
+  implementation(libs.okhttp)
+  implementation(libs.kotlinx.serialization.json)
+  implementation(libs.coil.compose)
+  implementation(project(":api"))
+  implementation(project(":persistence"))
+
   debugImplementation(libs.androidx.compose.ui.tooling)
   implementation(libs.androidx.compose.ui.preview)
   testImplementation(libs.androidx.compose.ui.test)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index efe8d657816b3b46d6ad728f9cffb60bde054343..3ae67d5ae61dbb0dfc50a8c2ffce2a3cb7f2a18d 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,6 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-  package="de.chaosdorf.meteroid">
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
 
   <uses-permission android:name="android.permission.INTERNET" />
 
@@ -14,7 +13,6 @@
     <activity
       android:name=".MainActivity"
       android:exported="true"
-      android:label="@string/application_name"
       android:theme="@style/Theme.Meteroid">
       <intent-filter>
         <action android:name="android.intent.action.MAIN" />
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/MainActivity.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/MainActivity.kt
index 3a62e2bdfc769330ccb6228cb4ab802a9e4bec12..875e72b70e622265bed12b57871ffb7f9a8206b5 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/MainActivity.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/MainActivity.kt
@@ -27,32 +27,37 @@ package de.chaosdorf.meteroid
 import android.os.Bundle
 import androidx.activity.ComponentActivity
 import androidx.activity.compose.setContent
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.padding
-import androidx.compose.material3.Scaffold
-import androidx.compose.ui.Modifier
-import androidx.navigation.compose.rememberNavController
-import de.chaosdorf.meteroid.ui.MeteroidBottomBar
-import de.chaosdorf.meteroid.ui.MeteroidRouter
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import de.chaosdorf.meteroid.di.RootViewModel
+import de.chaosdorf.meteroid.di.RootViewModelFactory
+import de.chaosdorf.meteroid.routes.RootRouter
 import de.chaosdorf.meteroid.ui.theme.MeteroidTheme
+import kotlinx.coroutines.MainScope
 
 
 class MainActivity : ComponentActivity() {
+  private val rootViewModelFactory = object : RootViewModelFactory {}
+  private lateinit var rootViewModel: RootViewModel
+
   override fun onCreate(savedInstanceState: Bundle?) {
     super.onCreate(savedInstanceState)
-    setContent {
-      val navController = rememberNavController()
+    val scope = MainScope()
+    rootViewModel =
+      rootViewModelFactory.newInstance(scope, applicationContext)
 
+    setContent {
       MeteroidTheme {
-        Scaffold(
-          bottomBar = { MeteroidBottomBar(navController) }
-        ) { padding: PaddingValues ->
-          MeteroidRouter(
-            navController,
-            modifier = Modifier.padding(padding)
-          )
-        }
+        App(rootViewModel)
       }
     }
   }
 }
+
+@Composable
+fun App(viewModel: RootViewModel) {
+  val route by viewModel.route.collectAsState()
+
+  RootRouter(route)
+}
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/di/AddServerViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/di/AddServerViewModel.kt
new file mode 100644
index 0000000000000000000000000000000000000000..0b07b4ca5c84946e991b55ed97d67d63de8b9ecb
--- /dev/null
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/di/AddServerViewModel.kt
@@ -0,0 +1,116 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.meteroid.di
+
+import android.util.Log
+import de.chaosdorf.mete.PwaManifest
+import de.chaosdorf.meteroid.model.Server
+import de.chaosdorf.meteroid.model.ServerId
+import de.chaosdorf.meteroid.util.await
+import de.chaosdorf.meteroid.util.findBestIcon
+import de.chaosdorf.meteroid.util.resolve
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.debounce
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.mapNotNull
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.json.decodeFromStream
+import okhttp3.HttpUrl.Companion.toHttpUrl
+import okhttp3.OkHttpClient
+import okhttp3.Request
+
+
+interface AddServerViewModelFactory {
+  fun newInstance(
+    scope: CoroutineScope,
+    isFirstServer: Boolean,
+    onSubmit: (url: String, manifest: PwaManifest?) -> Unit,
+    onCancel: () -> Unit
+  ) = AddServerViewModelImpl(scope, isFirstServer, onSubmit, onCancel)
+}
+
+interface AddServerViewModel {
+  val url: MutableStateFlow<String>
+  val server: StateFlow<Server?>
+  val loading: StateFlow<Boolean>
+  val isFirstServer: Boolean
+  fun submit()
+  fun cancel()
+}
+
+class AddServerViewModelImpl(
+  scope: CoroutineScope,
+  override val isFirstServer: Boolean,
+  private val onSubmit: (url: String, manifest: PwaManifest?) -> Unit,
+  private val onCancel: () -> Unit
+) : AddServerViewModel {
+  private val json = Json {
+    ignoreUnknownKeys = true
+  }
+  private val httpClient = OkHttpClient()
+  override val url = MutableStateFlow("")
+  private val _loading = MutableStateFlow(false)
+  override val loading = _loading
+
+  @OptIn(ExperimentalSerializationApi::class)
+  private val manifest = url.debounce(300).mapNotNull { address ->
+    _loading.value = true
+    try {
+      val url = address.toHttpUrl().resolve("manifest.json")
+      val call = httpClient.newCall(Request.Builder().url(url!!).build())
+      val body = call.await()
+      val manifest = json.decodeFromStream<PwaManifest>(body.byteStream())
+      Pair(address, manifest)
+    } catch (_: Exception) {
+      null
+    } finally {
+      _loading.value = false
+    }
+  }.stateIn(scope, SharingStarted.WhileSubscribed(), null)
+
+  override val server = manifest.mapLatest { pair ->
+    pair?.let { (url, manifest) ->
+      Server(
+        id = ServerId(-1),
+        url = url,
+        name = manifest.name,
+        logoUrl = manifest.findBestIcon()?.resolve(url)
+      )
+    }
+  }.stateIn(scope, SharingStarted.WhileSubscribed(), null)
+
+  override fun submit() {
+    onSubmit(url.value, manifest.value?.second)
+  }
+
+  override fun cancel() {
+    onCancel()
+  }
+}
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/di/DrinkListViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/di/DrinkListViewModel.kt
new file mode 100644
index 0000000000000000000000000000000000000000..f8a46446dc672b9eab61615d4813fd591c476d6e
--- /dev/null
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/di/DrinkListViewModel.kt
@@ -0,0 +1,52 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.meteroid.di
+
+import de.chaosdorf.mete.DrinkId
+import de.chaosdorf.meteroid.model.Drink
+import de.chaosdorf.meteroid.Repository
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.stateIn
+
+interface DrinkViewModelFactory {
+  fun newInstance(
+    scope: CoroutineScope,
+    repository: Repository<DrinkId, Drink>
+  ) = DrinkListViewModelImpl(scope, repository)
+}
+
+interface DrinkListViewModel {
+  val drinks: StateFlow<List<Drink>>
+}
+
+class DrinkListViewModelImpl(
+  scope: CoroutineScope,
+  repository: Repository<DrinkId, Drink>
+) : DrinkListViewModel {
+  override val drinks: StateFlow<List<Drink>> = repository.getAllFlow()
+    .stateIn(scope, SharingStarted.WhileSubscribed(), emptyList())
+}
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/di/MainLayoutViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/di/MainLayoutViewModel.kt
new file mode 100644
index 0000000000000000000000000000000000000000..0e732b34426176d28a2defd007306990cb982ccb
--- /dev/null
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/di/MainLayoutViewModel.kt
@@ -0,0 +1,75 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.meteroid.di
+
+import androidx.room.withTransaction
+import de.chaosdorf.mete.v1.MeteApiV1Factory
+import de.chaosdorf.meteroid.MeteroidDatabase
+import de.chaosdorf.meteroid.RepositorySyncHandler
+import de.chaosdorf.meteroid.SyncHandler
+import de.chaosdorf.meteroid.model.Drink
+import de.chaosdorf.meteroid.model.Server
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.launch
+
+
+interface MainLayoutViewModelFactory {
+  fun newInstance(
+    scope: CoroutineScope, db: MeteroidDatabase, server: Server, onOpenServerSelection: () -> Unit
+  ): MainLayoutViewModel = MainLayoutViewModelImpl(scope, db, server, onOpenServerSelection)
+}
+
+interface MainLayoutViewModel {
+  val server: Server
+  val syncState: StateFlow<SyncHandler.State>
+  val drinkListViewModel: DrinkListViewModel
+
+  fun openServerSelection()
+}
+
+class MainLayoutViewModelImpl(
+  scope: CoroutineScope,
+  db: MeteroidDatabase,
+  override val server: Server,
+  private val onOpenServerSelection: () -> Unit
+) : MainLayoutViewModel {
+  private val api = MeteApiV1Factory.newInstance(server.url)
+  override val drinkListViewModel = DrinkListViewModelImpl(scope, db.drinks())
+  private val syncHandler = RepositorySyncHandler(db::withTransaction, db.drinks()) {
+    api.listDrinks().map { Drink.fromModelV1(it) }
+  }
+  override val syncState: StateFlow<SyncHandler.State> = syncHandler.state
+
+  init {
+    scope.launch {
+      syncHandler.doSync()
+    }
+  }
+
+  override fun openServerSelection() {
+    onOpenServerSelection()
+  }
+}
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/di/RootViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/di/RootViewModel.kt
new file mode 100644
index 0000000000000000000000000000000000000000..4e9ced5b09c9b4b36aa26961f910122adc080a00
--- /dev/null
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/di/RootViewModel.kt
@@ -0,0 +1,142 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.meteroid.di
+
+import android.content.Context
+import androidx.room.Room
+import de.chaosdorf.mete.v1.MeteApiV1Factory
+import de.chaosdorf.meteroid.MeteroidDatabase
+import de.chaosdorf.meteroid.Repository
+import de.chaosdorf.meteroid.SyncHandler
+import de.chaosdorf.meteroid.model.Server
+import de.chaosdorf.meteroid.model.ServerId
+import de.chaosdorf.meteroid.util.findBestIcon
+import de.chaosdorf.meteroid.util.resolve
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
+
+interface RootViewModelFactory {
+  fun newInstance(
+    scope: CoroutineScope, context: Context
+  ): RootViewModel = RootViewModelImpl(scope, context)
+}
+
+interface RootViewModel {
+  val route: StateFlow<RootRoute>
+}
+
+class ServerListSyncHandler(
+  private val repository: Repository<ServerId, Server>,
+) : SyncHandler {
+  private val _state = MutableStateFlow<SyncHandler.State>(SyncHandler.State.Idle)
+  override val state = _state
+
+  override suspend fun doSync() {
+    _state.value = SyncHandler.State.Syncing
+    for (server in repository.getAll()) {
+      val api = MeteApiV1Factory.newInstance(server.url)
+      val manifest = api.getManifest()
+      val updated = server.copy(
+        name = manifest?.name,
+        logoUrl = manifest?.findBestIcon()?.resolve(server.url)
+      )
+      repository.save(updated)
+    }
+    _state.value = SyncHandler.State.Idle
+  }
+}
+
+class RootViewModelImpl(
+  scope: CoroutineScope, context: Context
+) : RootViewModel {
+  private val db = Room.databaseBuilder(context, MeteroidDatabase::class.java, "mete").build()
+
+  private val syncHandler = ServerListSyncHandler(db.server())
+
+  private val setupViewModelFactory: SetupViewModelFactory = object : SetupViewModelFactory {}
+
+  private val mainLayoutViewModelFactory: MainLayoutViewModelFactory =
+    object : MainLayoutViewModelFactory {}
+
+  private val _serverSelectionOpen = MutableStateFlow(false)
+  private val _serverId: MutableStateFlow<ServerId?> = MutableStateFlow(null)
+  private val _server: StateFlow<Server?> = _serverId.flatMapLatest {
+    it?.let { db.server().getFlow(it) } ?: flowOf(null)
+  }.stateIn(scope, SharingStarted.WhileSubscribed(), null)
+  override val route: StateFlow<RootRoute> =
+    combine(_server, _serverSelectionOpen) { server, serverSelectionOpen ->
+      if (server == null || serverSelectionOpen) {
+        RootRoute.Setup(
+          setupViewModelFactory.newInstance(
+            scope,
+            db,
+            db.server(),
+            server != null,
+            ::onSelect,
+            ::onCloseServerSelection
+          )
+        )
+      } else {
+        RootRoute.MainLayout(
+          mainLayoutViewModelFactory.newInstance(
+            scope,
+            db,
+            server,
+            ::onOpenServerSelection
+          )
+        )
+      }
+    }.stateIn(scope, SharingStarted.WhileSubscribed(), RootRoute.Init)
+
+  init {
+    scope.launch { syncHandler.doSync() }
+  }
+
+  private fun onSelect(server: ServerId) {
+    _serverId.value = server
+    _serverSelectionOpen.value = false
+  }
+
+  private fun onOpenServerSelection() {
+    _serverSelectionOpen.value = true
+  }
+
+  private fun onCloseServerSelection() {
+    _serverSelectionOpen.value = true
+  }
+}
+
+sealed class RootRoute {
+  data object Init : RootRoute()
+  data class Setup(val viewModel: SetupViewModel) : RootRoute()
+  data class MainLayout(val viewModel: MainLayoutViewModel) : RootRoute()
+}
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/di/ServerSelectionViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/di/ServerSelectionViewModel.kt
new file mode 100644
index 0000000000000000000000000000000000000000..639df3ddad8375ee41dbf2344ade0235cda39eac
--- /dev/null
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/di/ServerSelectionViewModel.kt
@@ -0,0 +1,92 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.meteroid.di
+
+import de.chaosdorf.meteroid.Repository
+import de.chaosdorf.meteroid.model.Server
+import de.chaosdorf.meteroid.model.ServerId
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
+
+
+interface ServerSelectionViewModelFactory {
+  fun newInstance(
+    scope: CoroutineScope,
+    repository: Repository<ServerId, Server>,
+    hasSelectedServer: Boolean,
+    onAddServer: () -> Unit,
+    onSelect: (server: ServerId) -> Unit,
+    onClose: () -> Unit
+  ) = ServerSelectionViewModelImpl(
+    scope,
+    repository,
+    hasSelectedServer,
+    onAddServer,
+    onSelect,
+    onClose
+  )
+}
+
+interface ServerSelectionViewModel {
+  val servers: StateFlow<List<Server>>
+  val hasSelectedServer: Boolean
+  fun addServer()
+  fun select(server: ServerId)
+  fun remove(server: ServerId)
+  fun close()
+}
+
+class ServerSelectionViewModelImpl(
+  private val scope: CoroutineScope,
+  private val repository: Repository<ServerId, Server>,
+  override val hasSelectedServer: Boolean,
+  private val onAddServer: () -> Unit,
+  private val onSelect: (server: ServerId) -> Unit,
+  private val onClose: () -> Unit
+) : ServerSelectionViewModel {
+  override val servers: StateFlow<List<Server>> =
+    repository.getAllFlow().stateIn(scope, SharingStarted.WhileSubscribed(), emptyList())
+
+  override fun addServer() {
+    onAddServer()
+  }
+
+  override fun select(server: ServerId) {
+    onSelect(server)
+  }
+
+  override fun remove(server: ServerId) {
+    scope.launch {
+      repository.delete(server)
+    }
+  }
+
+  override fun close() {
+    onClose()
+  }
+}
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/di/SetupViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/di/SetupViewModel.kt
new file mode 100644
index 0000000000000000000000000000000000000000..16bbde1943a8f2f1259ddeb19f1dbd9c30b6fa35
--- /dev/null
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/di/SetupViewModel.kt
@@ -0,0 +1,126 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.meteroid.di
+
+import androidx.room.withTransaction
+import de.chaosdorf.mete.PwaManifest
+import de.chaosdorf.meteroid.MeteroidDatabase
+import de.chaosdorf.meteroid.Repository
+import de.chaosdorf.meteroid.model.Server
+import de.chaosdorf.meteroid.model.ServerId
+import de.chaosdorf.meteroid.util.findBestIcon
+import de.chaosdorf.meteroid.util.resolve
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
+
+
+interface SetupViewModelFactory {
+  fun newInstance(
+    scope: CoroutineScope,
+    db: MeteroidDatabase,
+    repository: Repository<ServerId, Server>,
+    hasSelectedServer: Boolean,
+    onSelect: (server: ServerId) -> Unit,
+    onClose: () -> Unit
+  ): SetupViewModel = SetupViewModelImpl(
+    scope, db, repository, hasSelectedServer, onSelect, onClose
+  )
+}
+
+interface SetupViewModel {
+  val route: StateFlow<SetupRoute>
+}
+
+class SetupViewModelImpl(
+  private val scope: CoroutineScope,
+  private val db: MeteroidDatabase,
+  private val repository: Repository<ServerId, Server>,
+  private val hasSelectedServer: Boolean,
+  private val onSelect: (server: ServerId) -> Unit,
+  private val onClose: () -> Unit
+) : SetupViewModel {
+  private val addServerViewModelFactory = object : AddServerViewModelFactory {}
+  private val serverSelectionViewModelFactory = object : ServerSelectionViewModelFactory {}
+  private val isFirstServer = repository.getAllFlow().map(List<Server>::isEmpty)
+  private val isAddingServer = MutableStateFlow(false)
+  override val route: StateFlow<SetupRoute> =
+    combine(isFirstServer, isAddingServer) { isFirstServer, addingServer ->
+      if (addingServer || isFirstServer) {
+        SetupRoute.AddServer(
+          addServerViewModelFactory.newInstance(
+            scope, isFirstServer, ::onSubmit, ::onCancel
+          )
+        )
+      } else {
+        SetupRoute.ServerSelection(
+          serverSelectionViewModelFactory.newInstance(
+            scope, repository, hasSelectedServer, ::onAddServer, onSelect, onClose
+          )
+        )
+      }
+    }.stateIn(
+      scope, SharingStarted.WhileSubscribed(), SetupRoute.ServerSelection(
+        serverSelectionViewModelFactory.newInstance(
+          scope, repository, hasSelectedServer, ::onAddServer, onSelect, onClose
+        )
+      )
+    )
+
+  private fun onAddServer() {
+    isAddingServer.value = true
+  }
+
+  private fun onSubmit(url: String, manifest: PwaManifest?) {
+    isAddingServer.value = false
+    scope.launch {
+      db.withTransaction {
+        val lastId = repository.getAll().map(Server::id).maxByOrNull(ServerId::value)?.value ?: 0
+        repository.save(
+          Server(
+            id = ServerId(lastId + 1),
+            url = url,
+            name = manifest?.name,
+            logoUrl = manifest?.findBestIcon()?.resolve(url)
+          )
+        )
+      }
+    }
+  }
+
+  private fun onCancel() {
+    isAddingServer.value = false
+  }
+}
+
+sealed class SetupRoute {
+  data class ServerSelection(val viewModel: ServerSelectionViewModel) : SetupRoute()
+  data class AddServer(val viewModel: AddServerViewModel) : SetupRoute()
+}
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/routes/InitRoute.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/routes/InitRoute.kt
new file mode 100644
index 0000000000000000000000000000000000000000..a31fbd8a5c163524c6a2b9a3b42ffb0386628449
--- /dev/null
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/routes/InitRoute.kt
@@ -0,0 +1,33 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.meteroid.routes
+
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+
+@Composable
+fun InitRoute() {
+    Text("Loading…")
+}
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/routes/RootRouter.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/routes/RootRouter.kt
new file mode 100644
index 0000000000000000000000000000000000000000..c58ef4258d426e887d87cc3a29edd8fffe870897
--- /dev/null
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/routes/RootRouter.kt
@@ -0,0 +1,39 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.meteroid.routes
+
+import androidx.compose.runtime.Composable
+import de.chaosdorf.meteroid.di.RootRoute
+import de.chaosdorf.meteroid.routes.main.MainLayoutRoute
+import de.chaosdorf.meteroid.routes.setup.SetupView
+
+@Composable
+fun RootRouter(route: RootRoute) {
+  when (route) {
+    RootRoute.Init -> InitRoute()
+    is RootRoute.MainLayout -> MainLayoutRoute(viewModel = route.viewModel)
+    is RootRoute.Setup -> SetupView(viewModel = route.viewModel)
+  }
+}
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/routes/SetupRouter.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/routes/SetupRouter.kt
new file mode 100644
index 0000000000000000000000000000000000000000..0f7b05e93612881a62df5e0ae802fb75d5500327
--- /dev/null
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/routes/SetupRouter.kt
@@ -0,0 +1,38 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.meteroid.routes
+
+import androidx.compose.runtime.Composable
+import de.chaosdorf.meteroid.di.SetupRoute
+import de.chaosdorf.meteroid.routes.setup.AddServerRoute
+import de.chaosdorf.meteroid.routes.setup.ServerSelectionRoute
+
+@Composable
+fun SetupRouter(route: SetupRoute) {
+  when (route) {
+    is SetupRoute.AddServer -> AddServerRoute(viewModel = route.viewModel)
+    is SetupRoute.ServerSelection -> ServerSelectionRoute(viewModel = route.viewModel)
+  }
+}
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/MeteroidRouter.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/routes/main/DrinkList.kt
similarity index 70%
rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/MeteroidRouter.kt
rename to app/src/main/kotlin/de/chaosdorf/meteroid/routes/main/DrinkList.kt
index b69b7ec8608fbbecfb9685ce3508b54d3a66588c..67907a294fb2a28b0365e25ccebd42eeb5c940f0 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/MeteroidRouter.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/routes/main/DrinkList.kt
@@ -22,26 +22,22 @@
  * THE SOFTWARE.
  */
 
-package de.chaosdorf.meteroid.ui
+package de.chaosdorf.meteroid.routes.main
 
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.navigation.NavHostController
-import androidx.navigation.compose.NavHost
-import androidx.navigation.compose.composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import de.chaosdorf.meteroid.di.DrinkListViewModel
 
 @Composable
-fun MeteroidRouter(
-  navController: NavHostController,
-  modifier: Modifier = Modifier
-) {
-  NavHost(
-    navController,
-    modifier = modifier,
-    startDestination = Screen.Drinks.route,
-  ) {
-    composable(Screen.Users.route) { Text("Userlist") }
-    composable(Screen.Drinks.route) { Text("Drinklist") }
+fun DrinkList(viewModel: DrinkListViewModel) {
+  val drinks by viewModel.drinks.collectAsState()
+  LazyColumn {
+    items(drinks) { drink ->
+      Text(drink.name)
+    }
   }
 }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/routes/main/MainLayoutRoute.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/routes/main/MainLayoutRoute.kt
new file mode 100644
index 0000000000000000000000000000000000000000..e009649fcb75d3fd38f8bbf5f1c57da0f837a624
--- /dev/null
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/routes/main/MainLayoutRoute.kt
@@ -0,0 +1,74 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.meteroid.routes.main
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material3.Icon
+import androidx.compose.material3.LinearProgressIndicator
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import de.chaosdorf.meteroid.RepositorySyncHandler
+import de.chaosdorf.meteroid.SyncHandler
+import de.chaosdorf.meteroid.di.MainLayoutViewModel
+
+@Composable
+fun MainLayoutRoute(viewModel: MainLayoutViewModel) {
+  val syncState by viewModel.syncState.collectAsState()
+
+  Scaffold(
+    topBar = {
+      TopAppBar(
+        title = {
+          Text("Meteroid")
+        },
+        navigationIcon = {
+          Icon(
+            Icons.AutoMirrored.Default.ArrowBack,
+            modifier = Modifier.clickable {
+              viewModel.openServerSelection()
+            },
+            contentDescription = "Back"
+          )
+        }
+      )
+    }
+  ) { paddingValues ->
+    Column(Modifier.padding(paddingValues)) {
+      if (syncState == SyncHandler.State.Syncing) {
+        LinearProgressIndicator()
+      }
+      DrinkList(viewModel.drinkListViewModel)
+    }
+  }
+}
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/routes/setup/AddServerRoute.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/routes/setup/AddServerRoute.kt
new file mode 100644
index 0000000000000000000000000000000000000000..0866e2653f418c21727aaeb3b3268b23d21cc9a1
--- /dev/null
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/routes/setup/AddServerRoute.kt
@@ -0,0 +1,95 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.meteroid.routes.setup
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Button
+import androidx.compose.material3.LinearProgressIndicator
+import androidx.compose.material3.ListItem
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextField
+import androidx.compose.material3.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import coil.compose.AsyncImage
+import de.chaosdorf.meteroid.di.AddServerViewModel
+
+@Composable
+fun AddServerRoute(viewModel: AddServerViewModel) {
+  val url by viewModel.url.collectAsState()
+  val server by viewModel.server.collectAsState()
+  val loading by viewModel.loading.collectAsState()
+
+  Scaffold(
+    topBar = {
+      TopAppBar(title = {
+        Text("Add Server")
+      })
+    }
+  ) { paddingValues ->
+    Column(modifier = Modifier.padding(paddingValues)) {
+      if (loading) {
+        LinearProgressIndicator()
+      }
+
+      TextField(
+        label = {
+          Text("Server URL")
+        },
+        value = url,
+        onValueChange = { value ->
+          viewModel.url.value = value
+        }
+      )
+
+      Button(
+        onClick = viewModel::submit
+      ) {
+        Text("Save")
+      }
+
+      if (!viewModel.isFirstServer) {
+        Button(
+          onClick = viewModel::cancel
+        ) {
+          Text("Cancel")
+        }
+      }
+
+      server?.let {
+        ListItem(
+          headlineContent = { Text(it.name ?: it.url) },
+          leadingContent = {
+            AsyncImage(model = it.logoUrl, contentDescription = null)
+          }
+        )
+      }
+    }
+  }
+}
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/routes/setup/ServerSelectionRoute.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/routes/setup/ServerSelectionRoute.kt
new file mode 100644
index 0000000000000000000000000000000000000000..bee5ecc82e7e2eee8b17d2b62425d61ece0415c1
--- /dev/null
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/routes/setup/ServerSelectionRoute.kt
@@ -0,0 +1,91 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.meteroid.routes.setup
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Add
+import androidx.compose.material.icons.filled.Delete
+import androidx.compose.material3.FloatingActionButton
+import androidx.compose.material3.Icon
+import androidx.compose.material3.ListItem
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import coil.compose.AsyncImage
+import de.chaosdorf.meteroid.di.ServerSelectionViewModel
+
+@Composable
+fun ServerSelectionRoute(viewModel: ServerSelectionViewModel) {
+  val servers by viewModel.servers.collectAsState()
+
+  Scaffold(
+    topBar = {
+      TopAppBar(
+        title = { Text("Select Server") },
+      )
+    },
+    floatingActionButton = {
+      FloatingActionButton(
+        onClick = { viewModel.addServer() }
+      ) {
+        Icon(
+          Icons.Default.Add,
+          contentDescription = "Select Server"
+        )
+      }
+    }
+  ) { paddingValues ->
+    LazyColumn(modifier = Modifier.padding(paddingValues)) {
+      items(servers) { server ->
+        ListItem(
+          modifier = Modifier.clickable {
+            viewModel.select(server.id)
+          },
+          headlineContent = { Text(server.name ?: server.url) },
+          leadingContent = {
+            AsyncImage(model = server.logoUrl, contentDescription = null)
+          },
+          trailingContent = {
+            Icon(
+              Icons.Default.Delete,
+              modifier = Modifier.clickable {
+                viewModel.remove(server.id)
+              },
+              contentDescription = "Delete"
+            )
+          }
+        )
+      }
+    }
+  }
+}
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/routes/setup/SetupView.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/routes/setup/SetupView.kt
new file mode 100644
index 0000000000000000000000000000000000000000..63de80b72a282a02d4da65c2c0dd48638eb71eee
--- /dev/null
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/routes/setup/SetupView.kt
@@ -0,0 +1,38 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.meteroid.routes.setup
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import de.chaosdorf.meteroid.routes.SetupRouter
+import de.chaosdorf.meteroid.di.SetupViewModel
+
+@Composable
+fun SetupView(viewModel: SetupViewModel) {
+  val route by viewModel.route.collectAsState()
+
+  SetupRouter(route)
+}
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/MeteroidBottomBar.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/MeteroidBottomBar.kt
deleted file mode 100644
index 146dd7f75bc512ac2f6bfd83a899dec7d52a94e2..0000000000000000000000000000000000000000
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/MeteroidBottomBar.kt
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * The MIT License (MIT)
- *
- * Copyright (c) 2013-2023 Chaosdorf e.V.
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- * THE SOFTWARE.
- */
-
-package de.chaosdorf.meteroid.ui
-
-import androidx.compose.material3.Icon
-import androidx.compose.material3.NavigationBar
-import androidx.compose.material3.NavigationBarItem
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.navigation.NavController
-import androidx.navigation.NavDestination.Companion.hierarchy
-import androidx.navigation.NavGraph.Companion.findStartDestination
-import androidx.navigation.compose.currentBackStackEntryAsState
-
-@Composable
-fun MeteroidBottomBar(navController: NavController) {
-  val screens = listOf(Screen.Drinks, Screen.Users)
-  val navBackStackEntry by navController.currentBackStackEntryAsState()
-  val currentDestination = navBackStackEntry?.destination
-
-    NavigationBar {
-        for (screen in screens) {
-            val selected = currentDestination?.hierarchy?.any {
-                it.route == screen.route
-            } == true
-
-            NavigationBarItem(
-                label = { Text(screen.label) },
-                icon = {
-                    Icon(
-                        if (selected) screen.selectedIcon
-                        else screen.icon,
-                        screen.label
-                    )
-                },
-                selected = selected,
-                onClick = {
-                    navController.navigate(screen.route) {
-                        // Pop up to the start destination of the graph to
-                        // avoid building up a large stack of destinations
-                        // on the back stack as users select items
-                        popUpTo(navController.graph.findStartDestination().id) {
-                            saveState = true
-                        }
-                        // Avoid multiple copies of the same destination when
-                        // reselecting the same item
-                        launchSingleTop = true
-                        // Restore state when reselecting a previously selected item
-                        restoreState = true
-                    }
-                }
-            )
-        }
-    }
-}
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/util/OkHttpClientExtensions.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/util/OkHttpClientExtensions.kt
new file mode 100644
index 0000000000000000000000000000000000000000..606c83c23406ec4b7e29443d2f8cad10d2556570
--- /dev/null
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/util/OkHttpClientExtensions.kt
@@ -0,0 +1,58 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.meteroid.util
+
+import kotlinx.coroutines.suspendCancellableCoroutine
+import okhttp3.Call
+import okhttp3.Callback
+import okhttp3.Response
+import java.io.IOException
+import kotlin.coroutines.resume
+import kotlin.coroutines.resumeWithException
+
+
+suspend fun Call.await() = suspendCancellableCoroutine { continuation ->
+  continuation.invokeOnCancellation {
+    cancel()
+  }
+  enqueue(object : Callback {
+    override fun onFailure(call: Call, e: IOException) {
+      continuation.resumeWithException(e)
+    }
+
+    override fun onResponse(call: Call, response: Response) {
+      if (response.isSuccessful) {
+        val body = response.body
+        if (body == null) {
+          continuation.resumeWithException(KotlinNullPointerException("Response from ${call.request().method}.${call.request().url} was null but response body type was declared as non-null"))
+        } else {
+          continuation.resume(body)
+        }
+      } else {
+        continuation.resumeWithException(IOException("Response from ${call.request().method}.${call.request().url} returned a non-successful response code: ${response.code}"))
+      }
+    }
+  })
+}
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/util/PwaManifestExtension.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/util/PwaManifestExtension.kt
new file mode 100644
index 0000000000000000000000000000000000000000..cd7ecd4dc8fddc4a5adc7f90df3ed638fea08372
--- /dev/null
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/util/PwaManifestExtension.kt
@@ -0,0 +1,36 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.meteroid.util
+
+import de.chaosdorf.mete.PwaIcon
+import de.chaosdorf.mete.PwaManifest
+import okhttp3.HttpUrl.Companion.toHttpUrl
+
+fun PwaManifest.findBestIcon(): PwaIcon? = icons.maxByOrNull {
+  it.sizes?.split("x")?.firstOrNull()?.toIntOrNull() ?: 0
+}
+
+fun PwaIcon.resolve(baseUrl: String): String? =
+  this.src?.let { baseUrl.toHttpUrl().resolve(it) }?.toString()
diff --git a/gradle/convention/src/main/kotlin/AndroidApplicationConvention.kt b/gradle/convention/src/main/kotlin/AndroidApplicationConvention.kt
index 683e704d3b3a3e93193cb727732272a40aaf0756..629dd50abcdc62ac44805f8b32c58f58cf89e311 100644
--- a/gradle/convention/src/main/kotlin/AndroidApplicationConvention.kt
+++ b/gradle/convention/src/main/kotlin/AndroidApplicationConvention.kt
@@ -16,11 +16,11 @@ class AndroidApplicationConvention : Plugin<Project> {
       }
 
       extensions.configure<ApplicationExtension> {
-        compileSdk = 33
+        compileSdk = 34
 
         defaultConfig {
           minSdk = 21
-          targetSdk = 33
+          targetSdk = 34
 
           applicationId = "${rootProject.group}.${rootProject.name.lowercase(Locale.ROOT)}"
           versionCode = cmd("git", "rev-list", "--count", "HEAD")?.toIntOrNull() ?: 1
diff --git a/gradle/convention/src/main/kotlin/AndroidLibraryConvention.kt b/gradle/convention/src/main/kotlin/AndroidLibraryConvention.kt
index feafcc4950807c8f5d8fe30d5239fbae86020caa..c048de7841d78191de848ec5808a009d99f0fa52 100644
--- a/gradle/convention/src/main/kotlin/AndroidLibraryConvention.kt
+++ b/gradle/convention/src/main/kotlin/AndroidLibraryConvention.kt
@@ -13,7 +13,7 @@ class AndroidLibraryConvention : Plugin<Project> {
       }
 
       extensions.configure<LibraryExtension> {
-        compileSdk = 33
+        compileSdk = 34
 
         defaultConfig {
           minSdk = 21
diff --git a/gradle/convention/src/main/kotlin/KotlinConvention.kt b/gradle/convention/src/main/kotlin/KotlinConvention.kt
index 9b364add5f645e899a1c890959a00d2475fbbdfa..d99f88171db54cdf468239e7c182cf8221b3d722 100644
--- a/gradle/convention/src/main/kotlin/KotlinConvention.kt
+++ b/gradle/convention/src/main/kotlin/KotlinConvention.kt
@@ -5,10 +5,11 @@ import org.gradle.api.plugins.JavaPluginExtension
 import org.gradle.api.tasks.testing.Test
 import org.gradle.jvm.toolchain.JavaLanguageVersion
 import org.gradle.kotlin.dsl.configure
-import org.gradle.kotlin.dsl.dependencies
-import org.gradle.kotlin.dsl.kotlin
 import org.gradle.kotlin.dsl.withType
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
 import org.jetbrains.kotlin.gradle.dsl.KotlinJvmOptions
+import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension
+import org.jetbrains.kotlin.gradle.dsl.kotlinExtension
 
 class KotlinConvention : Plugin<Project> {
   override fun apply(target: Project) {
@@ -17,8 +18,8 @@ class KotlinConvention : Plugin<Project> {
         apply("org.jetbrains.kotlin.jvm")
       }
 
-      extensions.configure<KotlinJvmOptions> {
-        freeCompilerArgs = freeCompilerArgs + listOf(
+      extensions.configure<KotlinJvmProjectExtension> {
+        compilerOptions.freeCompilerArgs.addAll(
           "-opt-in=kotlin.RequiresOptIn",
           // Enable experimental coroutines APIs, including Flow
           "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
@@ -27,7 +28,7 @@ class KotlinConvention : Plugin<Project> {
           "-opt-in=kotlin.ExperimentalUnsignedTypes",
         )
 
-        jvmTarget = JavaVersion.VERSION_17.toString()
+        compilerOptions.jvmTarget.set(JvmTarget.JVM_17)
       }
 
       tasks.withType<Test> {
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 77c021b6017d8087d59a5c892a7b8d7710fb053e..b1ef11609752905ed6396ae66043312ab8ff1367 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,19 +1,22 @@
 [versions]
-androidGradlePlugin = "8.0.1"
-androidx-activity = "1.7.1"
+androidGradlePlugin = "8.1.2"
+androidx-activity = "1.8.0"
 androidx-appcompat = "1.6.1"
-androidx-compose-bom = "2023.05.01"
-androidx-compose-compiler = "1.4.7"
+androidx-compose-bom = "2023.10.01"
+androidx-compose-compiler = "1.5.3"
 androidx-compose-material = "1.5.0-alpha04"
-androidx-compose-material3 = "1.2.0-alpha01"
-androidx-compose-runtimetracing = "1.0.0-alpha03"
-androidx-compose-tooling = "1.5.0-alpha04"
-androidx-navigation = "2.6.0-rc01"
-kotlin = "1.8.21"
+androidx-compose-material3 = "1.2.0-alpha10"
+androidx-compose-runtimetracing = "1.0.0-alpha04"
+androidx-compose-tooling = "1.6.0-alpha08"
+androidx-navigation = "2.7.4"
+androidx-room = "2.6.0"
+coil = "2.4.0"
+kotlin = "1.9.10"
+kotlin-ksp = "1.9.10-1.0.13"
 kotlinxCoroutines = "1.7.1"
 kotlinxDatetime = "0.4.0"
 kotlinxSerializationJson = "1.5.1"
-junit = "5.9.3"
+junit = "5.10.0"
 
 [libraries]
 androidx-activity = { module = "androidx.activity:activity-ktx", version.ref = "androidx-activity" }
@@ -27,7 +30,7 @@ androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", versi
 androidx-compose-animation = { group = "androidx.compose.animation", name = "animation" }
 androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation" }
 androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout" }
-androidx-compose-material-icons = { group = "androidx.compose.material", name = "material-icons-extended", version.ref = "androidx-compose-material3" }
+androidx-compose-material-icons = { group = "androidx.compose.material", name = "material-icons-extended", version = "1.6.0-alpha08" }
 androidx-compose-material = { group = "androidx.compose.material3", name = "material3", version.ref = "androidx-compose-material3" }
 androidx-compose-material-windowSizeClass = { group = "androidx.compose.material3", name = "material3-window-size-class", version.ref = "androidx-compose-material3" }
 androidx-compose-runtime = { group = "androidx.compose.runtime", name = "runtime" }
@@ -39,6 +42,17 @@ androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-toolin
 androidx-compose-ui-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview", version.ref = "androidx-compose-tooling" }
 androidx-compose-ui-util = { group = "androidx.compose.ui", name = "ui-util" }
 
+androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "androidx-room" }
+androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "androidx-room" }
+androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "androidx-room" }
+androidx-room-paging = { module = "androidx.room:room-paging", version.ref = "androidx-room" }
+
+coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" }
+
+okhttp = { module = "com.squareup.okhttp3:okhttp", version = "4.12.0" }
+retrofit-core = { module = "com.squareup.retrofit2:retrofit", version = "2.9.0" }
+retrofit-converter-kotlinx = { module = "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter", version = "1.0.0" }
+
 androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "androidx-navigation" }
 
 junit-api = { group = "org.junit.jupiter", name = "junit-jupiter-api", version.ref = "junit" }
@@ -47,6 +61,7 @@ junit-engine = { group = "org.junit.jupiter", name = "junit-jupiter-engine" }
 
 kotlin-stdlib = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib-jdk8", version.ref = "kotlin" }
 kotlin-test = { group = "org.jetbrains.kotlin", name = "kotlin-test-junit5", version.ref = "kotlin" }
+kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinxCoroutines" }
 kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinxCoroutines" }
 kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinxCoroutines" }
 kotlinx-datetime = { group = "org.jetbrains.kotlinx", name = "kotlinx-datetime", version.ref = "kotlinxDatetime" }
@@ -62,3 +77,4 @@ android-library = { id = "com.android.library", version.ref = "androidGradlePlug
 android-test = { id = "com.android.test", version.ref = "androidGradlePlugin" }
 kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
 kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
+kotlin-ksp = { id = "com.google.devtools.ksp", version.ref = "kotlin.ksp" }
diff --git a/persistence/build.gradle.kts b/persistence/build.gradle.kts
new file mode 100644
index 0000000000000000000000000000000000000000..56a70fe39af3e93fc9daca1c463a792a8f130cc5
--- /dev/null
+++ b/persistence/build.gradle.kts
@@ -0,0 +1,56 @@
+@file:Suppress("UnstableApiUsage")
+
+/*
+ * Quasseldroid - Quassel client for Android
+ *
+ * Copyright (c) 2019 Janne Mareike Koschinski
+ * Copyright (c) 2019 The Quassel Project
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3 as published
+ * by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+plugins {
+  id("justjanne.android.library")
+  alias(libs.plugins.kotlin.serialization)
+  alias(libs.plugins.kotlin.ksp)
+}
+
+android {
+  namespace = "de.chaosdorf.meteroid.persistence"
+}
+
+ksp {
+  arg("room.generateKotlin", "true")
+  arg("room.schemaLocation", "$projectDir/room/schemas")
+}
+
+dependencies {
+  implementation(libs.kotlin.stdlib)
+
+  implementation(libs.kotlinx.coroutines.core)
+  testImplementation(libs.kotlinx.coroutines.test)
+
+  testImplementation(libs.kotlin.test)
+  testImplementation(libs.junit.api)
+  testImplementation(libs.junit.params)
+  testRuntimeOnly(libs.junit.engine)
+
+  implementation(libs.androidx.room.runtime)
+  ksp(libs.androidx.room.compiler)
+  implementation(libs.androidx.room.ktx)
+  implementation(libs.androidx.room.paging)
+
+  implementation(libs.kotlinx.datetime)
+  implementation(libs.kotlinx.serialization.json)
+  implementation(project(":api"))
+}
diff --git a/persistence/proguard-rules.pro b/persistence/proguard-rules.pro
new file mode 100644
index 0000000000000000000000000000000000000000..690a4be68db285dea8841626be81fa215a589a84
--- /dev/null
+++ b/persistence/proguard-rules.pro
@@ -0,0 +1,54 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in /usr/lib/android-sdk/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.kts.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
+
+# The project is open source anyway, obfuscation is useless.
+-dontobfuscate
+
+# remove unnecessary warnings
+# Android HTTP Libs
+-dontnote android.net.http.**
+-dontnote org.apache.http.**
+# Kotlin stuff
+-dontnote kotlin.**
+# Gson
+-dontnote com.google.gson.**
+# Dagger
+-dontwarn com.google.errorprone.annotations.*
+# Retrofit
+-dontwarn retrofit2.**
+# Annotation used by Retrofit on Java 8 VMs
+-dontwarn javax.annotation.Nullable
+-dontwarn javax.annotation.ParametersAreNonnullByDefault
+-dontwarn javax.annotation.concurrent.GuardedBy
+# Retain generic type information for use by reflection by converters and adapters.
+-keepattributes Signature
+# Retain declared checked exceptions for use by a Proxy instance.
+-keepattributes Exceptions
+# Okio
+-dontwarn okio.**
+-dontwarn org.conscrypt.**
+# OkHttp3
+-dontwarn okhttp3.**
diff --git a/persistence/room/schemas/de.chaosdorf.meteroid.MeteroidDatabase/1.json b/persistence/room/schemas/de.chaosdorf.meteroid.MeteroidDatabase/1.json
new file mode 100644
index 0000000000000000000000000000000000000000..ab7f5e79cd26de6948213a5e361d2783ae728c9f
--- /dev/null
+++ b/persistence/room/schemas/de.chaosdorf.meteroid.MeteroidDatabase/1.json
@@ -0,0 +1,144 @@
+{
+  "formatVersion": 1,
+  "database": {
+    "version": 1,
+    "identityHash": "e8f8c6c1efa75d20c3aa764efc3037aa",
+    "entities": [
+      {
+        "tableName": "Drink",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `volume` REAL NOT NULL, `caffeine` INTEGER, `price` REAL NOT NULL, `active` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `logoUrl` TEXT NOT NULL, `logoFileName` TEXT NOT NULL, `logoContentType` TEXT NOT NULL, `logoFileSize` INTEGER NOT NULL, `logoUpdatedAt` INTEGER NOT NULL, PRIMARY KEY(`id`))",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "name",
+            "columnName": "name",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "volume",
+            "columnName": "volume",
+            "affinity": "REAL",
+            "notNull": true
+          },
+          {
+            "fieldPath": "caffeine",
+            "columnName": "caffeine",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "price",
+            "columnName": "price",
+            "affinity": "REAL",
+            "notNull": true
+          },
+          {
+            "fieldPath": "active",
+            "columnName": "active",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "createdAt",
+            "columnName": "createdAt",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "updatedAt",
+            "columnName": "updatedAt",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "logoUrl",
+            "columnName": "logoUrl",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "logoFileName",
+            "columnName": "logoFileName",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "logoContentType",
+            "columnName": "logoContentType",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "logoFileSize",
+            "columnName": "logoFileSize",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "logoUpdatedAt",
+            "columnName": "logoUpdatedAt",
+            "affinity": "INTEGER",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "autoGenerate": false,
+          "columnNames": [
+            "id"
+          ]
+        },
+        "indices": [],
+        "foreignKeys": []
+      },
+      {
+        "tableName": "Server",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `url` TEXT NOT NULL, `logoUrl` TEXT, PRIMARY KEY(`id`))",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "name",
+            "columnName": "name",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "url",
+            "columnName": "url",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "logoUrl",
+            "columnName": "logoUrl",
+            "affinity": "TEXT",
+            "notNull": false
+          }
+        ],
+        "primaryKey": {
+          "autoGenerate": false,
+          "columnNames": [
+            "id"
+          ]
+        },
+        "indices": [],
+        "foreignKeys": []
+      }
+    ],
+    "views": [],
+    "setupQueries": [
+      "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+      "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e8f8c6c1efa75d20c3aa764efc3037aa')"
+    ]
+  }
+}
\ No newline at end of file
diff --git a/persistence/room/schemas/de.chaosdorf.meteroid.MeteroidDatabase/2.json b/persistence/room/schemas/de.chaosdorf.meteroid.MeteroidDatabase/2.json
new file mode 100644
index 0000000000000000000000000000000000000000..846eb8cc9382c7fc6b285df977e24458b3cf8531
--- /dev/null
+++ b/persistence/room/schemas/de.chaosdorf.meteroid.MeteroidDatabase/2.json
@@ -0,0 +1,144 @@
+{
+  "formatVersion": 1,
+  "database": {
+    "version": 2,
+    "identityHash": "e8f8c6c1efa75d20c3aa764efc3037aa",
+    "entities": [
+      {
+        "tableName": "Drink",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `volume` REAL NOT NULL, `caffeine` INTEGER, `price` REAL NOT NULL, `active` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `logoUrl` TEXT NOT NULL, `logoFileName` TEXT NOT NULL, `logoContentType` TEXT NOT NULL, `logoFileSize` INTEGER NOT NULL, `logoUpdatedAt` INTEGER NOT NULL, PRIMARY KEY(`id`))",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "name",
+            "columnName": "name",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "volume",
+            "columnName": "volume",
+            "affinity": "REAL",
+            "notNull": true
+          },
+          {
+            "fieldPath": "caffeine",
+            "columnName": "caffeine",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "price",
+            "columnName": "price",
+            "affinity": "REAL",
+            "notNull": true
+          },
+          {
+            "fieldPath": "active",
+            "columnName": "active",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "createdAt",
+            "columnName": "createdAt",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "updatedAt",
+            "columnName": "updatedAt",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "logoUrl",
+            "columnName": "logoUrl",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "logoFileName",
+            "columnName": "logoFileName",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "logoContentType",
+            "columnName": "logoContentType",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "logoFileSize",
+            "columnName": "logoFileSize",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "logoUpdatedAt",
+            "columnName": "logoUpdatedAt",
+            "affinity": "INTEGER",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "autoGenerate": false,
+          "columnNames": [
+            "id"
+          ]
+        },
+        "indices": [],
+        "foreignKeys": []
+      },
+      {
+        "tableName": "Server",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `url` TEXT NOT NULL, `logoUrl` TEXT, PRIMARY KEY(`id`))",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "name",
+            "columnName": "name",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "url",
+            "columnName": "url",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "logoUrl",
+            "columnName": "logoUrl",
+            "affinity": "TEXT",
+            "notNull": false
+          }
+        ],
+        "primaryKey": {
+          "autoGenerate": false,
+          "columnNames": [
+            "id"
+          ]
+        },
+        "indices": [],
+        "foreignKeys": []
+      }
+    ],
+    "views": [],
+    "setupQueries": [
+      "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+      "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e8f8c6c1efa75d20c3aa764efc3037aa')"
+    ]
+  }
+}
\ No newline at end of file
diff --git a/persistence/room/schemas/de.chaosdorf.meteroid.MeteroidDatabase/3.json b/persistence/room/schemas/de.chaosdorf.meteroid.MeteroidDatabase/3.json
new file mode 100644
index 0000000000000000000000000000000000000000..67da8743f1fcb087a306db3aacf2121138c34b1b
--- /dev/null
+++ b/persistence/room/schemas/de.chaosdorf.meteroid.MeteroidDatabase/3.json
@@ -0,0 +1,144 @@
+{
+  "formatVersion": 1,
+  "database": {
+    "version": 3,
+    "identityHash": "e8f8c6c1efa75d20c3aa764efc3037aa",
+    "entities": [
+      {
+        "tableName": "Drink",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `volume` REAL NOT NULL, `caffeine` INTEGER, `price` REAL NOT NULL, `active` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `logoUrl` TEXT NOT NULL, `logoFileName` TEXT NOT NULL, `logoContentType` TEXT NOT NULL, `logoFileSize` INTEGER NOT NULL, `logoUpdatedAt` INTEGER NOT NULL, PRIMARY KEY(`id`))",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "name",
+            "columnName": "name",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "volume",
+            "columnName": "volume",
+            "affinity": "REAL",
+            "notNull": true
+          },
+          {
+            "fieldPath": "caffeine",
+            "columnName": "caffeine",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "price",
+            "columnName": "price",
+            "affinity": "REAL",
+            "notNull": true
+          },
+          {
+            "fieldPath": "active",
+            "columnName": "active",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "createdAt",
+            "columnName": "createdAt",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "updatedAt",
+            "columnName": "updatedAt",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "logoUrl",
+            "columnName": "logoUrl",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "logoFileName",
+            "columnName": "logoFileName",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "logoContentType",
+            "columnName": "logoContentType",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "logoFileSize",
+            "columnName": "logoFileSize",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "logoUpdatedAt",
+            "columnName": "logoUpdatedAt",
+            "affinity": "INTEGER",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "autoGenerate": false,
+          "columnNames": [
+            "id"
+          ]
+        },
+        "indices": [],
+        "foreignKeys": []
+      },
+      {
+        "tableName": "Server",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `url` TEXT NOT NULL, `logoUrl` TEXT, PRIMARY KEY(`id`))",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "name",
+            "columnName": "name",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "url",
+            "columnName": "url",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "logoUrl",
+            "columnName": "logoUrl",
+            "affinity": "TEXT",
+            "notNull": false
+          }
+        ],
+        "primaryKey": {
+          "autoGenerate": false,
+          "columnNames": [
+            "id"
+          ]
+        },
+        "indices": [],
+        "foreignKeys": []
+      }
+    ],
+    "views": [],
+    "setupQueries": [
+      "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+      "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e8f8c6c1efa75d20c3aa764efc3037aa')"
+    ]
+  }
+}
\ No newline at end of file
diff --git a/persistence/src/main/kotlin/de/chaosdorf/meteroid/MeteroidDatabase.kt b/persistence/src/main/kotlin/de/chaosdorf/meteroid/MeteroidDatabase.kt
new file mode 100644
index 0000000000000000000000000000000000000000..20c64ac94d5e3668f2ad6801b84d6ca553a7e29b
--- /dev/null
+++ b/persistence/src/main/kotlin/de/chaosdorf/meteroid/MeteroidDatabase.kt
@@ -0,0 +1,49 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.meteroid
+
+import androidx.room.AutoMigration
+import androidx.room.Database
+import androidx.room.RoomDatabase
+import androidx.room.TypeConverters
+import de.chaosdorf.meteroid.model.Drink
+import de.chaosdorf.meteroid.model.DrinkDao
+import de.chaosdorf.meteroid.model.Server
+import de.chaosdorf.meteroid.model.ServerDao
+import de.chaosdorf.meteroid.util.KotlinDatetimeTypeConverter
+
+@Database(
+  version = 1,
+  entities = [
+    Drink::class,
+    Server::class
+  ],
+  autoMigrations = [],
+)
+@TypeConverters(value = [KotlinDatetimeTypeConverter::class])
+abstract class MeteroidDatabase : RoomDatabase() {
+  abstract fun drinks(): DrinkDao
+  abstract fun server(): ServerDao
+}
diff --git a/persistence/src/main/kotlin/de/chaosdorf/meteroid/Repository.kt b/persistence/src/main/kotlin/de/chaosdorf/meteroid/Repository.kt
new file mode 100644
index 0000000000000000000000000000000000000000..26dfb12059dd4e75446ee523d8ebe940d4568641
--- /dev/null
+++ b/persistence/src/main/kotlin/de/chaosdorf/meteroid/Repository.kt
@@ -0,0 +1,38 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.meteroid
+
+import kotlinx.coroutines.flow.Flow
+
+interface Repository<K, V> {
+  fun getKey(value: V): K
+  suspend fun get(key: K): V?
+  fun getFlow(key: K): Flow<V?>
+  suspend fun getAll(): List<V>
+  fun getAllFlow(): Flow<List<V>>
+  suspend fun save(value: V)
+  suspend fun delete(key: K)
+  suspend fun deleteAll()
+}
diff --git a/persistence/src/main/kotlin/de/chaosdorf/meteroid/RepositorySyncHandler.kt b/persistence/src/main/kotlin/de/chaosdorf/meteroid/RepositorySyncHandler.kt
new file mode 100644
index 0000000000000000000000000000000000000000..752839909edf666bc1d461fa159b7255a07ff3e1
--- /dev/null
+++ b/persistence/src/main/kotlin/de/chaosdorf/meteroid/RepositorySyncHandler.kt
@@ -0,0 +1,59 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.meteroid
+
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+
+class RepositorySyncHandler<K, V>(
+  private val withTransaction: suspend (block: suspend () -> Any?) -> Any?,
+  private val repository: Repository<K, V>,
+  private val loader: suspend () -> List<V>,
+) : SyncHandler {
+  private val _state = MutableStateFlow<SyncHandler.State>(SyncHandler.State.Idle)
+  override val state: StateFlow<SyncHandler.State> = _state
+
+  override suspend fun doSync() {
+    _state.value = SyncHandler.State.Syncing
+    val values =
+      try {
+        loader()
+      } catch (e: Exception) {
+        _state.value = SyncHandler.State.Error("Error while syncing: $e")
+        return
+      }
+    withTransaction {
+      val existing = repository.getAll().map(repository::getKey).toSet()
+      val deletedEntries = existing - values.map(repository::getKey).toSet()
+      for (deletedEntry in deletedEntries) {
+        repository.delete(deletedEntry)
+      }
+      for (entry in values) {
+        repository.save(entry)
+      }
+    }
+    _state.value = SyncHandler.State.Idle
+  }
+}
diff --git a/persistence/src/main/kotlin/de/chaosdorf/meteroid/SyncHandler.kt b/persistence/src/main/kotlin/de/chaosdorf/meteroid/SyncHandler.kt
new file mode 100644
index 0000000000000000000000000000000000000000..8a671a841fb5889abe36eee2b7cd60be68d008c9
--- /dev/null
+++ b/persistence/src/main/kotlin/de/chaosdorf/meteroid/SyncHandler.kt
@@ -0,0 +1,39 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.meteroid
+
+import kotlinx.coroutines.flow.StateFlow
+
+interface SyncHandler {
+  val state: StateFlow<State>
+
+  suspend fun doSync()
+
+  sealed class State {
+    data object Idle : State()
+    data object Syncing : State()
+    data class Error(val message: String) : State()
+  }
+}
diff --git a/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Drink.kt b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Drink.kt
new file mode 100644
index 0000000000000000000000000000000000000000..1bf745e8f3f0e7d4edaaf68d092b475dc65c047a
--- /dev/null
+++ b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Drink.kt
@@ -0,0 +1,99 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.meteroid.model
+
+import androidx.room.Dao
+import androidx.room.Entity
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.PrimaryKey
+import androidx.room.Query
+import de.chaosdorf.mete.DrinkId
+import de.chaosdorf.mete.v1.DrinkModelV1
+import de.chaosdorf.meteroid.Repository
+import kotlinx.coroutines.flow.Flow
+import kotlinx.datetime.Instant
+
+@Entity
+data class Drink(
+  @PrimaryKey
+  val id: DrinkId,
+  val name: String,
+  val volume: Double,
+  val caffeine: Int?,
+  val price: Double,
+  val active: Boolean,
+  val createdAt: Instant,
+  val updatedAt: Instant,
+  val logoUrl: String,
+  val logoFileName: String,
+  val logoContentType: String,
+  val logoFileSize: Long,
+  val logoUpdatedAt: Instant
+) {
+  companion object {
+    fun fromModelV1(value: DrinkModelV1) = Drink(
+      value.id,
+      value.name,
+      value.bottleSize,
+      value.caffeine,
+      value.price,
+      value.active,
+      value.createdAt,
+      value.updatedAt,
+      value.logoUrl,
+      value.logoFileName,
+      value.logoContentType,
+      value.logoFileSize,
+      value.logoUpdatedAt
+    )
+  }
+}
+
+@Dao
+interface DrinkDao : Repository<DrinkId, Drink> {
+  override fun getKey(value: Drink): DrinkId = value.id
+
+  @Query("SELECT * FROM Drink WHERE id = :id LIMIT 1")
+  override suspend fun get(id: DrinkId): Drink?
+
+  @Query("SELECT * FROM Drink WHERE id = :id LIMIT 1")
+  override fun getFlow(id: DrinkId): Flow<Drink?>
+
+  @Query("SELECT * FROM Drink")
+  override suspend fun getAll(): List<Drink>
+
+  @Query("SELECT * FROM Drink")
+  override fun getAllFlow(): Flow<List<Drink>>
+
+  @Insert(onConflict = OnConflictStrategy.REPLACE)
+  override suspend fun save(drink: Drink)
+
+  @Query("DELETE FROM Drink WHERE id = :id")
+  override suspend fun delete(id: DrinkId)
+
+  @Query("DELETE FROM Drink")
+  override suspend fun deleteAll()
+}
diff --git a/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Server.kt b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Server.kt
new file mode 100644
index 0000000000000000000000000000000000000000..c735ca8efa45f1d81bd8145ae82158a23afbd8e8
--- /dev/null
+++ b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Server.kt
@@ -0,0 +1,73 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.meteroid.model
+
+import androidx.room.Dao
+import androidx.room.Entity
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.PrimaryKey
+import androidx.room.Query
+import de.chaosdorf.mete.PwaManifest
+import de.chaosdorf.meteroid.Repository
+import kotlinx.coroutines.flow.Flow
+
+@JvmInline
+value class ServerId(val value: Long)
+
+@Entity
+data class Server(
+  @PrimaryKey
+  val id: ServerId,
+  val name: String?,
+  val url: String,
+  val logoUrl: String?
+)
+
+@Dao
+interface ServerDao : Repository<ServerId, Server> {
+  override fun getKey(value: Server): ServerId = value.id
+
+  @Query("SELECT * FROM Server WHERE id = :id LIMIT 1")
+  override suspend fun get(id: ServerId): Server?
+
+  @Query("SELECT * FROM Server WHERE id = :id LIMIT 1")
+  override fun getFlow(id: ServerId): Flow<Server?>
+
+  @Query("SELECT * FROM Server")
+  override suspend fun getAll(): List<Server>
+
+  @Query("SELECT * FROM Server")
+  override fun getAllFlow(): Flow<List<Server>>
+
+  @Insert(onConflict = OnConflictStrategy.REPLACE)
+  override suspend fun save(drink: Server)
+
+  @Query("DELETE FROM Server WHERE id = :id")
+  override suspend fun delete(id: ServerId)
+
+  @Query("DELETE FROM Server")
+  override suspend fun deleteAll()
+}
diff --git a/persistence/src/main/kotlin/de/chaosdorf/meteroid/util/KotlinDatetimeTypeConverter.kt b/persistence/src/main/kotlin/de/chaosdorf/meteroid/util/KotlinDatetimeTypeConverter.kt
new file mode 100644
index 0000000000000000000000000000000000000000..289ea21afd72e5fce225fbf92445cf4930f55241
--- /dev/null
+++ b/persistence/src/main/kotlin/de/chaosdorf/meteroid/util/KotlinDatetimeTypeConverter.kt
@@ -0,0 +1,35 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.meteroid.util
+
+import androidx.room.TypeConverter
+import kotlinx.datetime.Instant
+
+class KotlinDatetimeTypeConverter {
+  @TypeConverter
+  fun load(value: Long): Instant = Instant.fromEpochMilliseconds(value)
+  @TypeConverter
+  fun store(value: Instant): Long = value.toEpochMilliseconds()
+}
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 564449ee08aa15ab093d2a24a4d4714864439411..4ff6281f603d29e099c6f78f0bcc6a6b530732c7 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -40,4 +40,4 @@ dependencyResolutionManagement {
   }
 }
 
-include(":app")
+include(":app", ":api", ":persistence")