diff --git a/build.gradle b/build.gradle
index c4af8a64492ab7e43c17cf37458b714df3e4e6cb..85ad63cd5782cd2b608a5946fbd5bb1487255ddb 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,5 +1,5 @@
 buildscript {
-    ext.kotlin_version = '1.5.21'
+    ext.kotlin_version = '1.5.30'
     ext.hilt_version = '2.38.1'
     ext.tor_version = '0.3.5.15'
     ext.obfs4_version = '0.0.12-dev-40245c4a'
@@ -11,7 +11,7 @@ buildscript {
         mavenCentral()
     }
     dependencies {
-        classpath 'com.android.tools.build:gradle:7.0.0'
+        classpath 'com.android.tools.build:gradle:7.0.1'
         classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
         classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
     }
diff --git a/mailbox-core/build.gradle b/mailbox-core/build.gradle
index 33f0927ea6db736a0c4093673186d6525bff656e..cbf2b5ce91682019cceb3b02207fc2646bfa496b 100644
--- a/mailbox-core/build.gradle
+++ b/mailbox-core/build.gradle
@@ -19,9 +19,10 @@ dependencies {
 
     implementation 'org.briarproject:jtorctl:0.3'
 
-    def ktorVersion = '1.6.2'
-    implementation "io.ktor:ktor-server-core:$ktorVersion"
-    implementation "io.ktor:ktor-server-netty:$ktorVersion"
+    def ktor_version = '1.6.2'
+    implementation "io.ktor:ktor-server-core:$ktor_version"
+    implementation "io.ktor:ktor-server-netty:$ktor_version"
+    implementation "io.ktor:ktor-auth:$ktor_version"
     api "org.slf4j:slf4j-api:1.7.32"
 
     implementation 'com.h2database:h2:1.4.192' // The last version that supports Java 1.6
@@ -32,7 +33,7 @@ dependencies {
     testImplementation "org.junit.jupiter:junit-jupiter-engine:$junit_version"
     testImplementation "io.mockk:mockk:$mockk_version"
     testImplementation "ch.qos.logback:logback-classic:1.2.5"
-    testImplementation "io.ktor:ktor-client-cio:$ktorVersion"
+    testImplementation "io.ktor:ktor-client-cio:$ktor_version"
     testImplementation "com.google.dagger:hilt-core:$hilt_version"
     kaptTest "com.google.dagger:dagger-compiler:$hilt_version"
 }
diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/Database.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/Database.kt
index 56e1dd9fa81c6d384e4b8a7a9a5440dfeca440c7..e108f9da656e355e7ef445cca88cde27ed214cc1 100644
--- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/Database.kt
+++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/Database.kt
@@ -29,7 +29,13 @@ interface Database : TransactionManager {
     @Throws(DbException::class)
     fun getContact(txn: Transaction, id: Int): Contact?
 
+    @Throws(DbException::class)
+    fun getContacts(txn: Transaction): List<Contact>
+
     @Throws(DbException::class)
     fun removeContact(txn: Transaction, id: Int)
 
+    @Throws(DbException::class)
+    fun getContactWithToken(txn: Transaction, token: String): Contact?
+
 }
diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/JdbcDatabase.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/JdbcDatabase.kt
index b907191fc10b505e5048dab19c4b78f46f2627a0..651263331b3c9c85bf447f1d22a93e188eaeab7c 100644
--- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/JdbcDatabase.kt
+++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/JdbcDatabase.kt
@@ -147,8 +147,8 @@ abstract class JdbcDatabase(private val dbTypes: DatabaseTypes, private val cloc
         return true
     }
 
-    // Public access for testing
-    fun getMigrations(): List<Migration<Connection>> {
+    @Suppress("MemberVisibilityCanBePrivate") // visible for testing
+    internal fun getMigrations(): List<Migration<Connection>> {
         return Arrays.asList<Migration<Connection>>(
             // Migration1_2(dbTypes),
         )
@@ -488,6 +488,33 @@ abstract class JdbcDatabase(private val dbTypes: DatabaseTypes, private val cloc
         }
     }
 
+    @Throws(DbException::class)
+    override fun getContacts(txn: Transaction): List<Contact> {
+        val contacts = ArrayList<Contact>()
+        val connection: Connection = txn.unbox()
+        var ps: PreparedStatement? = null
+        var rs: ResultSet? = null
+        try {
+            val sql = "SELECT contactId, token, inbox, outbox FROM contacts"
+            ps = connection.prepareStatement(sql)
+            rs = ps.executeQuery()
+            while (rs.next()) {
+                val id = rs.getInt(1)
+                val token = rs.getString(2)
+                val inboxId = rs.getString(3)
+                val outboxId = rs.getString(4)
+                contacts.add(Contact(id, token, inboxId, outboxId))
+            }
+            rs.close()
+            ps.close()
+            return contacts
+        } catch (e: SQLException) {
+            tryToClose(rs, LOG)
+            tryToClose(ps, LOG)
+            throw DbException(e)
+        }
+    }
+
     @Throws(DbException::class)
     override fun removeContact(txn: Transaction, id: Int) {
         val connection: Connection = txn.unbox()
@@ -505,6 +532,31 @@ abstract class JdbcDatabase(private val dbTypes: DatabaseTypes, private val cloc
         }
     }
 
+    override fun getContactWithToken(txn: Transaction, token: String): Contact? {
+        val connection: Connection = txn.unbox()
+        var ps: PreparedStatement? = null
+        var rs: ResultSet? = null
+        try {
+            val sql = """SELECT contactId, inbox, outbox FROM contacts
+                                WHERE token = ?
+            """.trimIndent()
+            ps = connection.prepareStatement(sql)
+            ps.setString(1, token)
+            rs = ps.executeQuery()
+            if (!rs.next()) return null
+            val id = rs.getInt(1)
+            val inboxId = rs.getString(2)
+            val outboxId = rs.getString(3)
+            rs.close()
+            ps.close()
+            return Contact(id, token, inboxId, outboxId)
+        } catch (e: SQLException) {
+            tryToClose(rs, LOG)
+            tryToClose(ps, LOG)
+            throw DbException(e)
+        }
+    }
+
     /**
      * Commits a transaction to the database.
      */
diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/files/FileManager.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/files/FileManager.kt
new file mode 100644
index 0000000000000000000000000000000000000000..1c8453b8516351a225252ef6f31941bc5acb41ad
--- /dev/null
+++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/files/FileManager.kt
@@ -0,0 +1,112 @@
+package org.briarproject.mailbox.core.files
+
+import io.ktor.application.ApplicationCall
+import io.ktor.auth.principal
+import io.ktor.http.HttpStatusCode
+import io.ktor.response.respond
+import org.briarproject.mailbox.core.db.Database
+import org.briarproject.mailbox.core.server.AuthException
+import org.briarproject.mailbox.core.server.AuthManager
+import org.briarproject.mailbox.core.server.MailboxPrincipal
+import org.briarproject.mailbox.core.system.InvalidIdException
+import org.briarproject.mailbox.core.system.RandomIdManager
+import javax.inject.Inject
+
+class FileManager @Inject constructor(
+    private val db: Database,
+    private val authManager: AuthManager,
+    private val randomIdManager: RandomIdManager,
+) {
+
+    /**
+     * Used by contacts to send files to the owner and by the owner to send files to contacts.
+     *
+     * Checks if the authenticated [MailboxPrincipal] is allowed to upload to given [folderId],
+     * Responds with 200 (OK) if upload was successful
+     * (no 201 as the uploader doesn't need to know the $fileId)
+     * The mailbox chooses a random ID string for the file ID.
+     */
+    @Throws(AuthException::class, InvalidIdException::class)
+    suspend fun postFile(call: ApplicationCall, folderId: String) {
+        val principal: MailboxPrincipal? = call.principal()
+        randomIdManager.assertIsRandomId(folderId)
+        authManager.assertCanPostToFolder(principal, folderId)
+
+        // TODO implement
+
+        call.respond(HttpStatusCode.OK, "post: Not yet implemented. folderId: $folderId}")
+    }
+
+    /**
+     * Used by owner and contacts to list their files to retrieve.
+     *
+     * Checks if the authenticated [MailboxPrincipal] is allowed to download from [folderId].
+     * Responds with 200 (OK) with the list of files in JSON.
+     */
+    suspend fun listFiles(call: ApplicationCall, folderId: String) {
+        val principal: MailboxPrincipal? = call.principal()
+        randomIdManager.assertIsRandomId(folderId)
+        authManager.assertCanDownloadFromFolder(principal, folderId)
+
+        // TODO implement
+
+        call.respond(HttpStatusCode.OK, "get: Not yet implemented. folderId: $folderId")
+    }
+
+    /**
+     * Used by owner and contacts to retrieve a file.
+     *
+     * Checks if the authenticated [MailboxPrincipal] is allowed to download from $folderId
+     * Returns 200 (OK) if successful with the files' bytes in the response body
+     */
+    @Throws(AuthException::class, InvalidIdException::class)
+    suspend fun getFile(call: ApplicationCall, folderId: String, fileId: String) {
+        val principal: MailboxPrincipal? = call.principal()
+        randomIdManager.assertIsRandomId(folderId)
+        randomIdManager.assertIsRandomId(fileId)
+        authManager.assertCanDownloadFromFolder(principal, folderId)
+
+        // TODO implement
+
+        call.respond(
+            HttpStatusCode.OK,
+            "get: Not yet implemented. folderId: $folderId fileId: $fileId"
+        )
+    }
+
+    /**
+     * Used by owner and contacts to delete files.
+     *
+     * Checks if the authenticated [MailboxPrincipal] is allowed to download from [folderId].
+     * Responds with 200 (OK) if deletion was successful.
+     */
+    suspend fun deleteFile(call: ApplicationCall, folderId: String, fileId: String) {
+        val principal: MailboxPrincipal? = call.principal()
+        randomIdManager.assertIsRandomId(folderId)
+        randomIdManager.assertIsRandomId(fileId)
+        authManager.assertCanDownloadFromFolder(principal, folderId)
+
+        // TODO implement
+
+        call.respond(
+            HttpStatusCode.OK,
+            "delete: Not yet implemented. folderId: $folderId fileId: $fileId"
+        )
+    }
+
+    /**
+     * Used by owner only to list all folders that have files available for download.
+     *
+     * Checks if provided auth token is the owner.
+     * Responds with 200 (OK) with the list of folders with files in JSON.
+     */
+    suspend fun listFoldersWithFiles(call: ApplicationCall) {
+        val principal: MailboxPrincipal? = call.principal()
+        authManager.assertIsOwner(principal)
+
+        // TODO implement
+
+        call.respond(HttpStatusCode.OK, "get: Not yet implemented")
+    }
+
+}
diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/AuthManager.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/AuthManager.kt
new file mode 100644
index 0000000000000000000000000000000000000000..6d4f5a8a1f533c359862249344b757dfe51018c9
--- /dev/null
+++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/AuthManager.kt
@@ -0,0 +1,91 @@
+package org.briarproject.mailbox.core.server
+
+import io.ktor.auth.Principal
+import org.briarproject.mailbox.core.api.Contact
+import org.briarproject.mailbox.core.db.Database
+import org.briarproject.mailbox.core.server.MailboxPrincipal.ContactPrincipal
+import org.briarproject.mailbox.core.server.MailboxPrincipal.OwnerPrincipal
+import org.briarproject.mailbox.core.settings.SettingsManager
+import org.briarproject.mailbox.core.system.RandomIdManager
+import javax.inject.Inject
+import javax.inject.Singleton
+
+// We might want to move this somewhere else later
+internal const val SETTINGS_NAMESPACE_OWNER = "owner"
+internal const val SETTINGS_OWNER_TOKEN = "ownerToken"
+
+@Singleton
+class AuthManager @Inject constructor(
+    private val db: Database,
+    private val settingsManager: SettingsManager,
+    private val randomIdManager: RandomIdManager,
+) {
+
+    /**
+     * Returns the principal the given token belongs to
+     * or null if this token doesn't belong to any principal.
+     */
+    fun getPrincipal(token: String): MailboxPrincipal? {
+        randomIdManager.assertIsRandomId(token)
+        return db.transactionWithResult(true) { txn ->
+            val contact = db.getContactWithToken(txn, token)
+            if (contact != null) {
+                ContactPrincipal(contact)
+            } else {
+                val settings = settingsManager.getSettings(txn, SETTINGS_NAMESPACE_OWNER)
+                if (token == settings[SETTINGS_OWNER_TOKEN]) OwnerPrincipal
+                else null
+            }
+        }
+    }
+
+    /**
+     * @throws [AuthException] when given [principal] is NOT allowed
+     * to download or delete from the given [folderId] which is assumed to be validated already.
+     */
+    @Throws(AuthException::class)
+    fun assertCanDownloadFromFolder(principal: MailboxPrincipal?, folderId: String) {
+        if (principal == null) throw AuthException()
+
+        if (principal is OwnerPrincipal) {
+            val contacts = db.transactionWithResult(true) { txn -> db.getContacts(txn) }
+            val noOutboxFound = contacts.none { c -> folderId == c.outboxId }
+            if (noOutboxFound) throw AuthException()
+        } else if (principal is ContactPrincipal) {
+            if (folderId != principal.contact.inboxId) throw AuthException()
+        }
+    }
+
+    /**
+     * @throws [AuthException] when given [principal] is NOT allowed
+     * to post to the given [folderId] which is assumed to be validated already.
+     */
+    @Throws(AuthException::class)
+    fun assertCanPostToFolder(principal: MailboxPrincipal?, folderId: String) {
+        if (principal == null) throw AuthException()
+
+        if (principal is OwnerPrincipal) {
+            val contacts = db.transactionWithResult(true) { txn -> db.getContacts(txn) }
+            val noInboxFound = contacts.none { c -> folderId == c.inboxId }
+            if (noInboxFound) throw AuthException()
+        } else if (principal is ContactPrincipal) {
+            if (folderId != principal.contact.outboxId) throw AuthException()
+        }
+    }
+
+    /**
+     * @throws [AuthException] when given [principal] is NOT the mailbox owner.
+     */
+    @Throws(AuthException::class)
+    fun assertIsOwner(principal: MailboxPrincipal?) {
+        if (principal !is OwnerPrincipal) throw AuthException()
+    }
+
+}
+
+sealed class MailboxPrincipal : Principal {
+    object OwnerPrincipal : MailboxPrincipal()
+    class ContactPrincipal(val contact: Contact) : MailboxPrincipal()
+}
+
+class AuthException : IllegalStateException()
diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/BearerAuthenticationProvider.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/BearerAuthenticationProvider.kt
new file mode 100644
index 0000000000000000000000000000000000000000..808f764838e74bb6b399b6830aa638a50497cf30
--- /dev/null
+++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/BearerAuthenticationProvider.kt
@@ -0,0 +1,122 @@
+package org.briarproject.mailbox.core.server
+
+import io.ktor.application.ApplicationCall
+import io.ktor.application.call
+import io.ktor.auth.Authentication
+import io.ktor.auth.AuthenticationContext
+import io.ktor.auth.AuthenticationFailedCause
+import io.ktor.auth.AuthenticationFunction
+import io.ktor.auth.AuthenticationPipeline
+import io.ktor.auth.AuthenticationProvider
+import io.ktor.auth.UnauthorizedResponse
+import io.ktor.auth.parseAuthorizationHeader
+import io.ktor.http.auth.HttpAuthHeader
+import io.ktor.request.httpMethod
+import io.ktor.response.respond
+import io.ktor.util.pipeline.PipelineContext
+import org.briarproject.mailbox.core.util.LogUtils.debug
+import org.slf4j.LoggerFactory.getLogger
+
+private val AUTH_KEY_BEARER: Any = "BearerAuth"
+private val LOG = getLogger(BearerAuthenticationProvider::class.java)
+
+internal class BearerAuthenticationProvider constructor(config: Configuration) :
+    AuthenticationProvider(config) {
+
+    internal val realm: String = "Briar Mailbox"
+    internal val authHeader: (ApplicationCall) -> HttpAuthHeader? = { call ->
+        try {
+            call.request.parseAuthorizationHeader()
+        } catch (ex: IllegalArgumentException) {
+            LOG.warn("Illegal HTTP auth header", ex)
+            null
+        }
+    }
+    internal val authenticationFunction = config.authenticationFunction
+
+    internal class Configuration internal constructor(name: String?) :
+        AuthenticationProvider.Configuration(name) {
+
+        /**
+         * This function is applied to every call with a [String] auth token.
+         * @return a [MailboxPrincipal] or `null`
+         */
+        var authenticationFunction: AuthenticationFunction<String> = {
+            throw NotImplementedError(
+                "Bearer auth authenticationFunction is not specified." +
+                    "Use bearer { authenticationFunction = { ... } } to fix."
+            )
+        }
+
+        internal fun build() = BearerAuthenticationProvider(this)
+    }
+
+}
+
+/**
+ * Installs Bearer token Authentication mechanism
+ */
+internal fun Authentication.Configuration.bearer(
+    name: String? = null,
+    configure: BearerAuthenticationProvider.Configuration.() -> Unit,
+) {
+    val provider = BearerAuthenticationProvider.Configuration(name).apply(configure).build()
+    provider.pipeline.intercept(AuthenticationPipeline.RequestAuthentication) { context ->
+        authenticate(context, provider, name)
+    }
+    register(provider)
+}
+
+private suspend fun PipelineContext<AuthenticationContext, ApplicationCall>.authenticate(
+    context: AuthenticationContext,
+    provider: BearerAuthenticationProvider,
+    name: String?,
+) {
+    val authHeader = provider.authHeader(call)
+    if (authHeader == null) {
+        context.unauthorizedResponse(AuthenticationFailedCause.NoCredentials, provider)
+        return
+    }
+
+    try {
+        val token = (authHeader as? HttpAuthHeader.Single)?.blob
+        if (token == null) {
+            context.unauthorizedResponse(AuthenticationFailedCause.InvalidCredentials, provider)
+            return
+        }
+
+        // TODO remove logging before release
+        LOG.debug { "name: $name" }
+        LOG.debug { "httpMethod: ${call.request.httpMethod}" }
+
+        val principal = provider.authenticationFunction(call, token)
+        if (principal == null) {
+            context.unauthorizedResponse(AuthenticationFailedCause.InvalidCredentials, provider)
+        } else {
+            context.principal(principal)
+        }
+    } catch (cause: Throwable) {
+        val message = cause.message ?: cause.javaClass.simpleName
+        LOG.debug { "Bearer verification failed: $message" }
+        context.error(AUTH_KEY_BEARER, AuthenticationFailedCause.Error(message))
+    }
+}
+
+private fun AuthenticationContext.unauthorizedResponse(
+    cause: AuthenticationFailedCause,
+    provider: BearerAuthenticationProvider,
+) {
+    challenge(AUTH_KEY_BEARER, cause) {
+        call.respond(
+            UnauthorizedResponse(
+                HttpAuthHeader.Parameterized(
+                    authScheme = "Bearer",
+                    parameters = mapOf(HttpAuthHeader.Parameters.Realm to provider.realm)
+                )
+            )
+        )
+        if (!it.completed && call.response.status() != null) {
+            it.complete()
+        }
+    }
+}
diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/Routing.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/Routing.kt
index f4c9829b087188893e7eba301f99a1ef89e3b441..526ff542f8d8d094d5b264ccdcc9659b175efa0c 100644
--- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/Routing.kt
+++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/Routing.kt
@@ -1,14 +1,112 @@
 package org.briarproject.mailbox.core.server
 
 import io.ktor.application.Application
+import io.ktor.application.ApplicationCall
 import io.ktor.application.call
+import io.ktor.auth.authenticate
 import io.ktor.http.ContentType
+import io.ktor.http.HttpStatusCode
+import io.ktor.response.respond
 import io.ktor.response.respondText
+import io.ktor.routing.delete
 import io.ktor.routing.get
+import io.ktor.routing.post
+import io.ktor.routing.put
+import io.ktor.routing.route
 import io.ktor.routing.routing
+import io.ktor.util.getOrFail
+import org.briarproject.mailbox.core.files.FileManager
+import org.briarproject.mailbox.core.system.InvalidIdException
 
-internal fun Application.configureRouting() = routing {
-    get("/") {
-        call.respondText("Hello world!", ContentType.Text.Plain)
+internal const val V = "/" // TODO set to "/v1" for release
+
+internal fun Application.configureBasicApi() = routing {
+    route(V) {
+        get {
+            call.respondText("Hello world!", ContentType.Text.Plain)
+        }
+        authenticate {
+            delete {
+                call.respond(HttpStatusCode.OK, "delete: Not yet implemented")
+            }
+            put("/setup") {
+                call.respond(HttpStatusCode.OK, "put: Not yet implemented")
+            }
+        }
+    }
+}
+
+internal fun Application.configureContactApi() = routing {
+    authenticate {
+        route("$V/contacts") {
+            put("/{contactId}") {
+                call.respond(
+                    HttpStatusCode.OK,
+                    "get: Not yet implemented. " +
+                        "contactId: ${call.parameters["contactId"]}"
+                )
+            }
+            delete("/{contactId}") {
+                call.respond(
+                    HttpStatusCode.OK,
+                    "delete: Not yet implemented. " +
+                        "contactId: ${call.parameters["contactId"]}"
+                )
+            }
+            get {
+                call.respond(HttpStatusCode.OK, "get: Not yet implemented")
+            }
+        }
+    }
+}
+
+internal fun Application.configureFilesApi(fileManager: FileManager) = routing {
+
+    authenticate {
+        route("$V/files/{folderId}") {
+            post {
+                call.handle {
+                    fileManager.postFile(call, call.parameters.getOrFail("folderId"))
+                }
+            }
+            get {
+                call.handle {
+                    fileManager.listFiles(call, call.parameters.getOrFail("folderId"))
+                }
+            }
+            route("/{fileId}") {
+                get {
+                    val folderId = call.parameters.getOrFail("folderId")
+                    val fileId = call.parameters.getOrFail("fileId")
+                    call.handle {
+                        fileManager.getFile(call, folderId, fileId)
+                    }
+                }
+                delete {
+                    val folderId = call.parameters.getOrFail("folderId")
+                    val fileId = call.parameters.getOrFail("fileId")
+                    call.handle {
+                        fileManager.deleteFile(call, folderId, fileId)
+                    }
+                }
+            }
+        }
+    }
+    authenticate {
+        get("$V/folders") {
+            call.handle {
+                fileManager.listFoldersWithFiles(call)
+            }
+        }
+    }
+}
+
+private suspend fun ApplicationCall.handle(block: suspend () -> Unit) {
+    try {
+        block()
+    } catch (e: AuthException) {
+        respond(HttpStatusCode.Unauthorized, HttpStatusCode.Unauthorized.description)
+    } catch (e: InvalidIdException) {
+        respond(HttpStatusCode.BadRequest, "Malformed ID: ${e.id}")
     }
 }
diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/WebServerManager.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/WebServerManager.kt
index 56233b647c0e98a08fbd3d157a8da0fa24880a99..8cecb21ae2b1fa458d5147ed13a6c21dc028bf77 100644
--- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/WebServerManager.kt
+++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/WebServerManager.kt
@@ -1,26 +1,49 @@
 package org.briarproject.mailbox.core.server
 
 import io.ktor.application.install
+import io.ktor.auth.Authentication
 import io.ktor.features.CallLogging
 import io.ktor.server.engine.embeddedServer
 import io.ktor.server.netty.Netty
+import org.briarproject.mailbox.core.files.FileManager
 import org.briarproject.mailbox.core.lifecycle.Service
+import org.briarproject.mailbox.core.server.WebServerManager.Companion.PORT
+import org.slf4j.Logger
 import org.slf4j.LoggerFactory.getLogger
 import javax.inject.Inject
 import javax.inject.Singleton
 
+interface WebServerManager : Service {
+    companion object {
+        const val PORT: Int = 8000
+    }
+}
+
 @Singleton
-class WebServerManager @Inject constructor() : Service {
+internal class WebServerManagerImpl @Inject constructor(
+    private val authManager: AuthManager,
+    private val fileManager: FileManager,
+) : WebServerManager {
 
     internal companion object {
-        internal const val PORT = 8000
-        private val LOG = getLogger(WebServerManager::class.java)
+        private val LOG: Logger = getLogger(WebServerManager::class.java)
     }
 
     private val server by lazy {
         embeddedServer(Netty, PORT, watchPaths = emptyList()) {
             install(CallLogging)
-            configureRouting()
+            install(Authentication) {
+                bearer {
+                    authenticationFunction = { token ->
+                        // TODO: Remove logging of token before release.
+                        LOG.error("token: $token")
+                        authManager.getPrincipal(token)
+                    }
+                }
+            }
+            configureBasicApi()
+            configureContactApi()
+            configureFilesApi(fileManager)
         }
     }
 
diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/WebServerModule.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/WebServerModule.kt
index 37550a3823539a0ab420a0b060c9c0f6a9a6b0aa..be2c92aa2bde8e1495fa490b75971aed979c18b6 100644
--- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/WebServerModule.kt
+++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/WebServerModule.kt
@@ -13,8 +13,10 @@ internal class WebServerModule {
 
     @Provides
     @Singleton
-    fun provideWebServer(lifecycleManager: LifecycleManager): WebServerManager {
-        val webServerManager = WebServerManager()
+    fun provideWebServerManager(
+        lifecycleManager: LifecycleManager,
+        webServerManager: WebServerManagerImpl,
+    ): WebServerManager {
         lifecycleManager.registerService(webServerManager)
         return webServerManager
     }
diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/system/RandomIdManager.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/system/RandomIdManager.kt
index 15bcaa4cbe95d1e6b928c22f21892a53f2b46b8f..e7379b9903d5ebece7825fad8523a8770e57aec3 100644
--- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/system/RandomIdManager.kt
+++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/system/RandomIdManager.kt
@@ -6,6 +6,10 @@ import javax.inject.Inject
 private const val ID_SIZE = 32
 private const val ID_HEX_SIZE = ID_SIZE * 2
 
+/**
+ * Generates and validates random IDs
+ * that are being used for auth tokens, folder IDs and file names.
+ */
 class RandomIdManager @Inject constructor() {
 
     private val secureRandom = SecureRandom()
@@ -22,8 +26,15 @@ class RandomIdManager @Inject constructor() {
         return idRegex.matches(id)
     }
 
+    @Throws(InvalidIdException::class)
+    fun assertIsRandomId(id: String) {
+        if (!isValidRandomId(id)) throw InvalidIdException(id)
+    }
+
 }
 
+class InvalidIdException(val id: String) : IllegalStateException()
+
 fun ByteArray.toHex(): String = joinToString(separator = "") { eachByte ->
     "%02x".format(eachByte)
 }
diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/util/LogUtils.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/util/LogUtils.kt
index 46e7614f9c2a5b0ec1df63790aa06b9e7f7a5f23..fc4368b83a695946a4bc2103025a602178156092 100644
--- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/util/LogUtils.kt
+++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/util/LogUtils.kt
@@ -8,27 +8,27 @@ object LogUtils {
     private const val NANOS_PER_MILLI = 1000 * 1000
 
     @JvmStatic
-    fun Logger.trace(msg: () -> String) {
+    inline fun Logger.trace(msg: () -> String) {
         if (isTraceEnabled) trace(msg())
     }
 
     @JvmStatic
-    fun Logger.debug(msg: () -> String) {
+    inline fun Logger.debug(msg: () -> String) {
         if (isDebugEnabled) debug(msg())
     }
 
     @JvmStatic
-    fun Logger.info(msg: () -> String) {
+    inline fun Logger.info(msg: () -> String) {
         if (isInfoEnabled) info(msg())
     }
 
     @JvmStatic
-    fun Logger.warn(msg: () -> String) {
+    inline fun Logger.warn(msg: () -> String) {
         if (isWarnEnabled) warn(msg())
     }
 
     @JvmStatic
-    fun Logger.error(msg: () -> String) {
+    inline fun Logger.error(msg: () -> String) {
         if (isErrorEnabled) error(msg())
     }
 
diff --git a/mailbox-core/src/test/java/org/briarproject/mailbox/core/TestModule.kt b/mailbox-core/src/test/java/org/briarproject/mailbox/core/TestModule.kt
index 20b9590c42713f4fc9ec144fbc436cdd995ec4db..b269d9dcb7d5e8e5649615da87c6a61dca813134 100644
--- a/mailbox-core/src/test/java/org/briarproject/mailbox/core/TestModule.kt
+++ b/mailbox-core/src/test/java/org/briarproject/mailbox/core/TestModule.kt
@@ -32,7 +32,7 @@ internal class TestModule(private val tempDir: File) {
     @Provides
     fun provideDatabaseConfig() = object : DatabaseConfig {
         override fun getDatabaseDirectory(): File {
-            return tempDir
+            return File(tempDir, "db")
         }
     }
 }
diff --git a/mailbox-core/src/test/java/org/briarproject/mailbox/core/db/JdbcDatabaseTest.kt b/mailbox-core/src/test/java/org/briarproject/mailbox/core/db/JdbcDatabaseTest.kt
index 1fb1141181e6392c6765105dfe09e6110581e7df..ac4d1a7909a2ab29b4725e08b26121b10e32de6a 100644
--- a/mailbox-core/src/test/java/org/briarproject/mailbox/core/db/JdbcDatabaseTest.kt
+++ b/mailbox-core/src/test/java/org/briarproject/mailbox/core/db/JdbcDatabaseTest.kt
@@ -4,6 +4,7 @@ import org.briarproject.mailbox.core.TestUtils.deleteTestDirectory
 import org.briarproject.mailbox.core.api.Contact
 import org.briarproject.mailbox.core.settings.Settings
 import org.briarproject.mailbox.core.system.Clock
+import org.briarproject.mailbox.core.system.RandomIdManager
 import org.junit.jupiter.api.Test
 import org.junit.jupiter.api.io.TempDir
 import java.io.File
@@ -15,6 +16,8 @@ abstract class JdbcDatabaseTest {
     @TempDir
     lateinit var testDir: File
 
+    private val randomIdManager = RandomIdManager()
+
     protected abstract fun createDatabase(
         config: DatabaseConfig,
         clock: Clock,
@@ -37,16 +40,16 @@ abstract class JdbcDatabaseTest {
     open fun testPersistence() {
         // Store some records
         val contact1 = Contact(
-            1,
-            "4291ad1d-897d-4db4-9de9-ea3f78c5262e",
-            "f21467bd-afb0-4c0e-9090-cae45ea1eae9",
-            "880629fb-3226-41d8-a978-7b28cf44d57d"
+            id = 1,
+            token = randomIdManager.getNewRandomId(),
+            inboxId = randomIdManager.getNewRandomId(),
+            outboxId = randomIdManager.getNewRandomId()
         )
         val contact2 = Contact(
-            2,
-            "fbbe9a63-2f28-46d4-a465-e6ca57a5d811",
-            "7931fa7a-077e-403a-8487-63261027d6d2",
-            "12a61ca3-af0a-41d1-acc1-a0f4625f6e42"
+            id = 2,
+            token = randomIdManager.getNewRandomId(),
+            inboxId = randomIdManager.getNewRandomId(),
+            outboxId = randomIdManager.getNewRandomId()
         )
         var db: Database = open(false)
         db.transaction(false) { txn ->
@@ -63,6 +66,9 @@ abstract class JdbcDatabaseTest {
             val contact2Reloaded1 = db.getContact(txn, 2)
             assertEquals(contact1, contact1Reloaded1)
             assertEquals(contact2, contact2Reloaded1)
+            assertEquals(contact1, db.getContactWithToken(txn, contact1.token))
+            assertEquals(contact2, db.getContactWithToken(txn, contact2.token))
+            assertNull(db.getContactWithToken(txn, randomIdManager.getNewRandomId()))
 
             // Delete one of the records
             db.removeContact(txn, 1)
@@ -92,7 +98,7 @@ abstract class JdbcDatabaseTest {
         merged["foo"] = "bar"
         merged["baz"] = "qux"
 
-        var db: Database = open(false)
+        val db: Database = open(false)
         db.transaction(false) { txn ->
             // store 'before'
             db.mergeSettings(txn, before, "namespace")
diff --git a/mailbox-core/src/test/java/org/briarproject/mailbox/core/server/AuthManagerTest.kt b/mailbox-core/src/test/java/org/briarproject/mailbox/core/server/AuthManagerTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..546e5ce16d0328a75371b8ad252e02d1dc543c55
--- /dev/null
+++ b/mailbox-core/src/test/java/org/briarproject/mailbox/core/server/AuthManagerTest.kt
@@ -0,0 +1,5 @@
+package org.briarproject.mailbox.core.server
+
+class AuthManagerTest {
+    // TODO write unit tests
+}