diff --git a/signalc/migrations/changelog.postgresql.sql b/signalc/migrations/changelog.postgresql.sql
index ceaff1c0c56677e672cfa28bf78bb717c23e5227..acb2dade9a645245a922b3cbaf224be2af6f4d96 100644
--- a/signalc/migrations/changelog.postgresql.sql
+++ b/signalc/migrations/changelog.postgresql.sql
@@ -167,4 +167,51 @@ CREATE TABLE IF NOT EXISTS contacts (
 );
 CREATE INDEX contacts_account_id_uuid ON contacts (account_id, uuid);
 CREATE INDEX contacts_account_id_phone_number ON contacts (account_id, phone_number);
--- rollback DROP TABLE contacts;
\ No newline at end of file
+-- rollback DROP TABLE contacts;
+
+
+-- changeset aguestuser:1623100049239-1 failOnError:true
+DELETE FROM accounts;
+DELETE FROM contacts;
+DELETE FROM identities;
+DELETE FROM ownidentities;
+DELETE FROM prekeys;
+DELETE FROM profiles;
+DELETE FROM senderkeys;
+DELETE FROM sessions;
+DELETE FROM signedprekeys;
+
+ALTER TABLE identities DROP CONSTRAINT pk_identities;
+ALTER TABLE identities DROP COLUMN contact_id;
+ALTER TABLE identities ADD COLUMN contact_id INT;
+ALTER TABLE identities ADD CONSTRAINT pk_identities PRIMARY KEY (account_id, contact_id);
+
+ALTER TABLE sessions DROP CONSTRAINT pk_sessions;
+ALTER TABLE sessions DROP COLUMN contact_id;
+ALTER TABLE sessions ADD COLUMN contact_id INT;
+ALTER TABLE sessions ADD CONSTRAINT pk_sessions PRIMARY KEY (account_id, contact_id, device_id);
+-- rollback DELETE FROM accounts;
+-- rollback DELETE FROM contacts;
+-- rollback DELETE FROM identities;
+-- rollback DELETE FROM ownidentities;
+-- rollback DELETE FROM prekeys;
+-- rollback DELETE FROM profiles;
+-- rollback DELETE FROM senderkeys;
+-- rollback DELETE FROM sessions;
+-- rollback DELETE FROM signedprekeys;
+-- rollback
+-- rollback ALTER TABLE identities DROP CONSTRAINT pk_identities;
+-- rollback ALTER TABLE identities DROP COLUMN contact_id;
+-- rollback ALTER TABLE identities ADD COLUMN contact_id VARCHAR(255) NOT NULL;
+-- rollback ALTER TABLE identities ADD CONSTRAINT pk_identities PRIMARY KEY (account_id, contact_id);
+-- rollback
+-- rollback ALTER TABLE sessions DROP CONSTRAINT pk_sessions;
+-- rollback ALTER TABLE sessions DROP COLUMN contact_id;
+-- rollback ALTER TABLE sessions ADD COLUMN contact_id VARCHAR(255) NOT NULL;
+-- rollback ALTER TABLE sessions ADD CONSTRAINT pk_sessions PRIMARY KEY (account_id, contact_id, device_id);
+
+-- changeset aguestuser:1623100049239-2 failOnError:true
+DROP INDEX identities_identity_key_bytes;
+-- rollback CREATE INDEX identities_identity_key_bytes ON identities (identity_key_bytes);
+
+-- TODO: uniqueness constraint on contacts account_id_uuid
\ No newline at end of file
diff --git a/signalc/src/main/kotlin/info/signalboost/signalc/Application.kt b/signalc/src/main/kotlin/info/signalboost/signalc/Application.kt
index 042d4c36b5ba2a7f32d274e7ed8d66ed9a1d3a04..3f93ce0c4bad5f07dfeb43294ecfca1621bc0a48 100644
--- a/signalc/src/main/kotlin/info/signalboost/signalc/Application.kt
+++ b/signalc/src/main/kotlin/info/signalboost/signalc/Application.kt
@@ -7,7 +7,7 @@ import info.signalboost.signalc.logging.LibSignalLogger
 import info.signalboost.signalc.logic.*
 import info.signalboost.signalc.metrics.Metrics
 import info.signalboost.signalc.store.AccountStore
-import info.signalboost.signalc.store.ProfileStore
+import info.signalboost.signalc.store.ContactStore
 import info.signalboost.signalc.store.ProtocolStore
 import io.mockk.coEvery
 import io.mockk.mockk
@@ -152,7 +152,7 @@ class Application(val config: Config.App){
     // STORE //
 
     lateinit var accountStore: AccountStore
-    lateinit var profileStore: ProfileStore
+    lateinit var contactStore: ContactStore
     lateinit var protocolStore: ProtocolStore
 
     private lateinit var dataSource: HikariDataSource
@@ -240,7 +240,7 @@ class Application(val config: Config.App){
         // storage resources
         dataSource = initializeDataSource(Mocks.dataSource)
         accountStore = initializeColdComponent(AccountStore::class)
-        profileStore = initializeColdComponent(ProfileStore::class, Mocks.profileStore)
+        contactStore = initializeColdComponent(ContactStore::class, Mocks.contactStore)
         protocolStore = initializeColdComponent(ProtocolStore::class, Mocks.protocolStore)
 
         // network resources
diff --git a/signalc/src/main/kotlin/info/signalboost/signalc/Config.kt b/signalc/src/main/kotlin/info/signalboost/signalc/Config.kt
index 5424c5442125759d71f8a328c8e2c67c93420f17..57dbb78765bba41877d8d0c597e5f0bbf7e80164 100644
--- a/signalc/src/main/kotlin/info/signalboost/signalc/Config.kt
+++ b/signalc/src/main/kotlin/info/signalboost/signalc/Config.kt
@@ -4,7 +4,7 @@ import com.zaxxer.hikari.HikariDataSource
 import info.signalboost.signalc.logic.*
 import info.signalboost.signalc.metrics.Metrics
 import info.signalboost.signalc.store.AccountStore
-import info.signalboost.signalc.store.ProfileStore
+import info.signalboost.signalc.store.ContactStore
 import info.signalboost.signalc.store.ProtocolStore
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.ObsoleteCoroutinesApi
@@ -26,7 +26,7 @@ object Config {
         HikariDataSource::class,
         AccountStore::class,
         ProtocolStore::class,
-        ProfileStore::class,
+        ContactStore::class,
         // components
         AccountManager::class,
         SignalReceiver::class,
diff --git a/signalc/src/main/kotlin/info/signalboost/signalc/Mocks.kt b/signalc/src/main/kotlin/info/signalboost/signalc/Mocks.kt
index 177c7126a96bd5be1517d6ee6b01987b7b330751..b2d793c68c119c56bbc66c36bfaec5ed7c323788 100644
--- a/signalc/src/main/kotlin/info/signalboost/signalc/Mocks.kt
+++ b/signalc/src/main/kotlin/info/signalboost/signalc/Mocks.kt
@@ -5,7 +5,7 @@ import info.signalboost.signalc.logic.*
 import info.signalboost.signalc.metrics.Metrics
 import info.signalboost.signalc.model.SignalcAddress
 import info.signalboost.signalc.model.SignalcSendResult
-import info.signalboost.signalc.store.ProfileStore
+import info.signalboost.signalc.store.ContactStore
 import info.signalboost.signalc.store.ProtocolStore
 import io.mockk.coEvery
 import io.mockk.every
@@ -34,7 +34,7 @@ object Mocks {
     val dataSource: HikariDataSource.() -> Unit = {
         every { closeQuietly() } returns Unit
     }
-    val profileStore: ProfileStore.() -> Unit = {
+    val contactStore: ContactStore.() -> Unit = {
         coEvery { storeProfileKey(any(), any(), any())} returns Unit
         coEvery { loadProfileKey(any(), any())} returns mockk()
     }
diff --git a/signalc/src/main/kotlin/info/signalboost/signalc/db/ContactRecord.kt b/signalc/src/main/kotlin/info/signalboost/signalc/db/ContactRecord.kt
index bfa50aca3978fc8bacc8374f6db0391a9851f949..0d3e5fa173ddc482aa4948b9879b1935dbfe801a 100644
--- a/signalc/src/main/kotlin/info/signalboost/signalc/db/ContactRecord.kt
+++ b/signalc/src/main/kotlin/info/signalboost/signalc/db/ContactRecord.kt
@@ -10,18 +10,18 @@ import org.jetbrains.exposed.sql.statements.UpdateStatement
 
 interface ContactRecord: FieldSet {
     val accountId: Column<String>
-    val contactId: Column<String>
+    val contactId: Column<Int>
 
     companion object {
 
-        fun ContactRecord.findByContactId(accountId: String, contactId: String): ResultRow? {
+        fun ContactRecord.findByContactId(accountId: String, contactId: Int): ResultRow? {
             val table = this
             return table.select {
                 (table.accountId eq accountId).and(table.contactId eq contactId)
             }.singleOrNull()
         }
 
-        fun ContactRecord.findManyByContactId(accountId: String, contactId: String): List<ResultRow> {
+        fun ContactRecord.findManyByContactId(accountId: String, contactId: Int): List<ResultRow> {
             val table = this
             return table.select {
                 (table.accountId eq accountId).and(table.contactId eq contactId)
@@ -30,7 +30,7 @@ interface ContactRecord: FieldSet {
 
         fun ContactRecord.updateByContactId(
             accountId: String,
-            contactId: String,
+            contactId: Int,
             updateStatement: Table.(UpdateStatement) -> Unit
         ): Int {
             val table = this
@@ -39,7 +39,7 @@ interface ContactRecord: FieldSet {
             }, null, updateStatement)
         }
 
-        fun ContactRecord.deleteByContactId(accountId: String, contactId: String): Int {
+        fun ContactRecord.deleteByContactId(accountId: String, contactId: Int): Int {
             val table = this
             return (table as Table).deleteWhere {
                 (table.accountId eq accountId).and(table.contactId eq contactId)
diff --git a/signalc/src/main/kotlin/info/signalboost/signalc/db/Contacts.kt b/signalc/src/main/kotlin/info/signalboost/signalc/db/Contacts.kt
index 6a6a87dee15fd14c5f87f5852396e22293669d0d..0853924f97273ecfaebd18ea280fac108357a849 100644
--- a/signalc/src/main/kotlin/info/signalboost/signalc/db/Contacts.kt
+++ b/signalc/src/main/kotlin/info/signalboost/signalc/db/Contacts.kt
@@ -2,9 +2,9 @@ package info.signalboost.signalc.db
 
 import org.jetbrains.exposed.sql.Table
 
-object Contacts: Table() {
-    val accountId = varchar("account_id", 255)
-    val contactId = integer("contact_id").autoIncrement()
+object Contacts: Table(), ContactRecord {
+    override val accountId = varchar("account_id", 255)
+    override val contactId = integer("contact_id").autoIncrement()
     val uuid = uuid("uuid").nullable()
     val phoneNumber = varchar("phone_number", 255)
     val profileKeyBytes = binary("profile_key_bytes").nullable()
diff --git a/signalc/src/main/kotlin/info/signalboost/signalc/db/DeviceRecord.kt b/signalc/src/main/kotlin/info/signalboost/signalc/db/DeviceRecord.kt
index 24aa4f9bc40005bdd806c3b160b94da4898104f0..669cfe0affcc681a48ac8b86515ae5e9e35963b7 100644
--- a/signalc/src/main/kotlin/info/signalboost/signalc/db/DeviceRecord.kt
+++ b/signalc/src/main/kotlin/info/signalboost/signalc/db/DeviceRecord.kt
@@ -2,43 +2,43 @@ package info.signalboost.signalc.db
 
 import org.jetbrains.exposed.sql.*
 import org.jetbrains.exposed.sql.statements.UpdateStatement
-import org.whispersystems.libsignal.SignalProtocolAddress
 
 interface DeviceRecord: FieldSet {
     val accountId: Column<String>
-    val contactId: Column<String>
+    val contactId: Column<Int>
     val deviceId: Column<Int>
 
     companion object {
 
-        fun DeviceRecord.findByAddress(accountId: String, address: SignalProtocolAddress): ResultRow? {
+        fun DeviceRecord.findByDeviceId(accountId: String, contactId: Int, deviceId: Int): ResultRow? {
             val table = this
             return table.select {
                 (table.accountId eq accountId)
-                    .and(table.contactId eq address.name)
-                    .and(table.deviceId eq address.deviceId)
+                    .and(table.contactId eq contactId)
+                    .and(table.deviceId eq deviceId)
             }.singleOrNull()
         }
 
-        fun DeviceRecord.updateByAddress(
+        fun DeviceRecord.updateByDeviceId(
             accountId: String,
-            address: SignalProtocolAddress,
+            contactId: Int,
+            deviceId: Int,
             updateStatement: Table.(UpdateStatement) -> Unit
         ): Int {
             val table = this
             return (table as Table).update ({
                 (table.accountId eq accountId)
-                    .and(table.contactId eq address.name)
-                    .and(table.deviceId eq address.deviceId)
+                    .and(table.contactId eq contactId)
+                    .and(table.deviceId eq deviceId)
             }, null, updateStatement)
         }
 
-        fun DeviceRecord.deleteByAddress(accountId: String, address: SignalProtocolAddress): Int {
+        fun DeviceRecord.deleteByDeviceId(accountId: String, contactId: Int, deviceId: Int): Int {
             val table = this
             return (table as Table).deleteWhere {
                 (table.accountId eq accountId)
-                    .and(table.contactId eq address.name)
-                    .and(table.deviceId eq address.deviceId)
+                    .and(table.contactId eq contactId)
+                    .and(table.deviceId eq deviceId)
             }
         }
     }
diff --git a/signalc/src/main/kotlin/info/signalboost/signalc/db/Identities.kt b/signalc/src/main/kotlin/info/signalboost/signalc/db/Identities.kt
index 4295ac292be2437fb68ef9e658e1273df6f50223..ac3b608d7a21018606ec8b4f2499698806a3a947 100644
--- a/signalc/src/main/kotlin/info/signalboost/signalc/db/Identities.kt
+++ b/signalc/src/main/kotlin/info/signalboost/signalc/db/Identities.kt
@@ -9,7 +9,7 @@ object Identities: Table(), ContactRecord {
     private const val IDENTITY_KEY_BYTE_ARRAY_LENGTH = 33
 
     override val accountId = varchar("account_id", 255)
-    override val contactId = varchar("contact_id", 255)
+    override val contactId = integer("contact_id")
     val identityKeyBytes = binary("identity_key_bytes", IDENTITY_KEY_BYTE_ARRAY_LENGTH).index()
     val isTrusted = bool("is_trusted").default(true)
     val createdAt = timestamp("created_at").clientDefault { Instant.now() }
diff --git a/signalc/src/main/kotlin/info/signalboost/signalc/db/Sessions.kt b/signalc/src/main/kotlin/info/signalboost/signalc/db/Sessions.kt
index 4ca084f585dd39c2806690a2ed5e459a869c8b59..b0d859c9fd8968362365f145922ddf4a3ac98b23 100644
--- a/signalc/src/main/kotlin/info/signalboost/signalc/db/Sessions.kt
+++ b/signalc/src/main/kotlin/info/signalboost/signalc/db/Sessions.kt
@@ -3,10 +3,9 @@ package info.signalboost.signalc.db
 import org.jetbrains.exposed.sql.Table
 
 
-
 object Sessions: Table(), ContactRecord, DeviceRecord  {
     override val accountId = varchar("account_id", 255)
-    override val contactId = varchar("contact_id", 255)
+    override val contactId = integer("contact_id")
     override val deviceId = integer("device_id")
     val sessionBytes = binary("session_bytes")
 
diff --git a/signalc/src/main/kotlin/info/signalboost/signalc/exception/SignalcError.kt b/signalc/src/main/kotlin/info/signalboost/signalc/exception/SignalcError.kt
index 5e911d9847867c13304809af7372270b0a1f76de..1ade8209d588c15ca80c5646b276833e4a589bbc 100644
--- a/signalc/src/main/kotlin/info/signalboost/signalc/exception/SignalcError.kt
+++ b/signalc/src/main/kotlin/info/signalboost/signalc/exception/SignalcError.kt
@@ -8,9 +8,10 @@ object SignalcError {
     object RegistrationOfRegsisteredUser: Exception("Cannot register account that is already registered")
     object SubscriptionOfUnregisteredUser: Exception("Cannot subscribe to messages for unregistered account")
     class UpdateToNonExistentFingerprint(
-        contactId: String,
+        accountId: String,
+        contactId: Int,
         fingerprint: ByteArray,
-    ): Exception("Cannot update non-existent fingerprint ${fingerprint.toHex()} for contact $contactId")
+    ): Exception("Cannot update non-existent fingerprint ${fingerprint.toHex()} for contact $contactId of account $accountId")
     object UnsubscribeUnregisteredUser: Exception("Cannot unsubscribe to messages for unregistered account")
     object VerificationOfNewUser: Exception("Cannot verify a new (unregistered) account")
     object VerificationOfVerifiedUser: Exception("Cannot verify account that is already verified")
diff --git a/signalc/src/main/kotlin/info/signalboost/signalc/logic/AccountManager.kt b/signalc/src/main/kotlin/info/signalboost/signalc/logic/AccountManager.kt
index ef6c6bfeb9bd984868e18790635997acb5d839ac..56f3ea8de3d3576fe5ef90aeb77589f85d15f640 100644
--- a/signalc/src/main/kotlin/info/signalboost/signalc/logic/AccountManager.kt
+++ b/signalc/src/main/kotlin/info/signalboost/signalc/logic/AccountManager.kt
@@ -156,7 +156,7 @@ class AccountManager(private val app: Application) {
      **/
     @Throws(IOException::class) // if network call to retreive sender cert fails
     suspend fun getUnidentifiedAccessPair(accountId: String, contactId: String): UnidentifiedAccessPair? {
-        val contactAccessKey = app.profileStore.loadProfileKey(accountId, contactId)?.let {
+        val contactAccessKey = app.contactStore.loadProfileKey(accountId, contactId)?.let {
             UnidentifiedAccess.deriveAccessKeyFrom(it)
         } ?: run {
             logger.error { "Could not derive delivery token for $contactId: no profile key found." }
diff --git a/signalc/src/main/kotlin/info/signalboost/signalc/logic/SignalReceiver.kt b/signalc/src/main/kotlin/info/signalboost/signalc/logic/SignalReceiver.kt
index ecb25ba04f197e20d9abbf493b0694cf6137594e..3232b79565f7549a6f2960b9ab50ae9a060ab4cd 100644
--- a/signalc/src/main/kotlin/info/signalboost/signalc/logic/SignalReceiver.kt
+++ b/signalc/src/main/kotlin/info/signalboost/signalc/logic/SignalReceiver.kt
@@ -22,12 +22,12 @@ import org.whispersystems.signalservice.api.SignalServiceMessageReceiver
 import org.whispersystems.signalservice.api.crypto.SignalServiceCipher
 import org.whispersystems.signalservice.api.messages.SignalServiceAttachment
 import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer
-import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage
 import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope
 import org.whispersystems.signalservice.api.util.UptimeSleepTimer
 import java.io.File
 import java.io.IOException
 import java.nio.file.Files
+import java.util.*
 import java.util.concurrent.CancellationException
 import java.util.concurrent.ConcurrentHashMap
 import java.util.concurrent.TimeUnit
@@ -168,41 +168,43 @@ class SignalReceiver(private val app: Application) {
             logger.debug { "Got ${it.asString} from ${envelope.sourceIdentifier ?: "SEALED"} to ${account.username}" }
             Metrics.SignalReceiver.numberOfMessagesReceived.labels(it.asString).inc()
             return when (it) {
+                EnvelopeType.CIPHERTEXT,
+                EnvelopeType.UNIDENTIFIED_SENDER -> processMessage(envelope, account)
+
                 EnvelopeType.PREKEY_BUNDLE -> {
-                    relay(envelope, account)
-                    maybeRefreshPreKeys(account)
+                    processPreKeyBundle(envelope, account)
+                    processMessage(envelope, account)
                 }
 
-                EnvelopeType.UNIDENTIFIED_SENDER,
-                EnvelopeType.CIPHERTEXT -> relay(envelope, account)
+                EnvelopeType.RECEIPT -> processReceipt(envelope, account)
 
                 EnvelopeType.KEY_EXCHANGE, // TODO: handle this to process "reset secure session" events
-                EnvelopeType.RECEIPT, // signal android basically drops these, so do we!
                 EnvelopeType.UNKNOWN -> drop(envelope, account)
             }
-
         }
     }
 
-    private suspend fun relay(envelope: SignalServiceEnvelope, account: VerifiedAccount): Job {
+    private suspend fun processMessage(envelope: SignalServiceEnvelope, account: VerifiedAccount): Job {
         // Attempt to decrypt envelope in a new coroutine then relay result to socket message sender for handling.
-        val (sender, recipient) = Pair(envelope.asSignalcAddress(), account.address)
         return app.coroutineScope.launch(Concurrency.Dispatcher) {
+            var contactAddress: SignalcAddress? = null // not available until after decryption for sealed-sender msgs
             try {
                 messagesInFlight.getAndIncrement()
-                val dataMessage: SignalServiceDataMessage = cipherOf(account).decrypt(envelope).dataMessage.orNull()
+                val contents = cipherOf(account).decrypt(envelope)
+                val dataMessage = contents.dataMessage.orNull()
                     ?: return@launch // drop other message types (eg: typing message, sync message, etc)
 
+                contactAddress = contents.sender.asSignalcAddress()
                 val body = dataMessage.body?.orNull() ?: ""
                 val attachments = dataMessage.attachments.orNull() ?: emptyList()
                 dataMessage.profileKey.orNull()?.let {
-                    app.profileStore.storeProfileKey(recipient.identifier, sender.identifier, it)
+                    app.contactStore.storeProfileKey(account.id, contactAddress.identifier, it)
                 }
 
                 app.socketSender.send(
                     SocketResponse.Cleartext.of(
-                        sender,
-                        recipient,
+                        contactAddress,
+                        account.address,
                         body, // we allow empty message bodies b/c that's how expiry timer changes are sent
                         attachments.mapNotNull { it.retrieveFor(account) },
                         dataMessage.expiresInSeconds,
@@ -210,20 +212,44 @@ class SignalReceiver(private val app: Application) {
                     )
                 )
             } catch(err: Throwable) {
-                handleRelayError(err, sender, recipient)
+                // TODO: we don't always have a contactAddress here anymore... wat do?
+                handleRelayError(err, account.address, contactAddress)
             } finally {
                 messagesInFlight.getAndDecrement()
             }
         }
     }
 
-    private fun maybeRefreshPreKeys(account: VerifiedAccount) = app.coroutineScope.launch(Concurrency.Dispatcher) {
-        // If we are receiving a prekey bundle, this is the beginning of a new session, the initiation
-        // of which might have depleted our prekey reserves below the level we want to keep on hand
-        // to start new sessions. So: launch a background job to check our prekey reserve and replenish it if needed!
-        app.accountManager.refreshPreKeysIfDepleted(account)
+    private suspend fun processReceipt(envelope: SignalServiceEnvelope, account: VerifiedAccount): Job? {
+        if(!envelope.isUnidentifiedSender) {
+            // NOTE: we should only receive N of these upon initiating a conversation with a new contact...
+            // (where N is the number of devices a recipient has)
+            // - q: should this also be able to store a phone number if we only have a UUID?
+            app.contactStore.storeUuid(
+                accountId = account.username,
+                contactPhoneNumber =  envelope.sourceE164.get(),
+                contactUuid =  UUID.fromString(envelope.sourceUuid.get()),
+            )
+        }
+        return null
     }
 
+
+    // TODO: make this function suspend and async/await!!
+    private fun processPreKeyBundle(envelope: SignalServiceEnvelope, account: VerifiedAccount) =
+        // here: app.coroutineScope.async(...)
+        app.coroutineScope.launch(Concurrency.Dispatcher) {
+            app.contactStore.create(
+                accountId = account.username,
+                phoneNumber = envelope.sourceE164.get(),
+                uuid = UUID.fromString(envelope.sourceUuid.get()),
+            )
+            // If we are receiving a prekey bundle, this is the beginning of a new session, the initiation
+            // of which might have depleted our prekey reserves below the level we want to keep on hand
+            // to start new sessions. So: launch a background job to check our prekey reserve and replenish it if needed!
+            app.accountManager.refreshPreKeysIfDepleted(account)
+        }
+
     private suspend fun drop(envelope: SignalServiceEnvelope, account: VerifiedAccount): Job? {
         app.socketSender.send(
             SocketResponse.Dropped(envelope.asSignalcAddress(), account.address, envelope)
@@ -231,7 +257,7 @@ class SignalReceiver(private val app: Application) {
         return null
     }
 
-    private suspend fun handleRelayError(err: Throwable, sender: SignalcAddress, recipient: SignalcAddress) {
+    private suspend fun handleRelayError(err: Throwable, account: SignalcAddress, contact: SignalcAddress?) {
         when (err) {
             is ProtocolUntrustedIdentityException -> {
                 // When we get this exception we return a null fingerprint to the client, with the intention of causing
@@ -240,14 +266,14 @@ class SignalReceiver(private val app: Application) {
                 // and can be handled (and trusted) from the send path.
                 app.socketSender.send(
                     SocketResponse.InboundIdentityFailure.of(
-                        recipient,
-                        sender,
+                        account,
+                        contact,
                         null,
                     )
                 )
             }
             else -> {
-                app.socketSender.send(SocketResponse.DecryptionError(sender, recipient, err))
+                app.socketSender.send(SocketResponse.DecryptionError(account, contact, err))
                 logger.error { "Decryption Error:\n ${err.stackTraceToString()}" }
             }
         }
diff --git a/signalc/src/main/kotlin/info/signalboost/signalc/logic/SignalSender.kt b/signalc/src/main/kotlin/info/signalboost/signalc/logic/SignalSender.kt
index e1e2a962d2ca14511c4d3d8a7628fb4b1a178418..f42e66ca5ec04d8b70e3a7326297dac3ece74d81 100644
--- a/signalc/src/main/kotlin/info/signalboost/signalc/logic/SignalSender.kt
+++ b/signalc/src/main/kotlin/info/signalboost/signalc/logic/SignalSender.kt
@@ -153,8 +153,8 @@ class SignalSender(private val app: Application) {
         // Try to send all messages sealed-sender by deriving `unidentifiedAccessPair`s from profile keys. Without such
         // tokens, message are sent unsealed, and are treated by Signal as spam. To avoid being blocked by Signal,
         // we expose a toggle to prevent all unsealed messages from being sent.
-        val unidentifiedAccessPair = app.accountManager.getUnidentifiedAccessPair(sender.identifier,recipient.identifier).also {
-            metrics.numberOfUnsealedMessagesProduced.labels(sender.identifier).inc()
+        val unidentifiedAccessPair = app.accountManager.getUnidentifiedAccessPair(sender.id, recipient.identifier).also {
+            metrics.numberOfUnsealedMessagesProduced.labels(sender.id).inc()
             if(it == null && app.toggles.blockUnsealedMessages) return@async SignalcSendResult.Blocked(recipient)
         }
         try {
diff --git a/signalc/src/main/kotlin/info/signalboost/signalc/model/Account.kt b/signalc/src/main/kotlin/info/signalboost/signalc/model/Account.kt
index 645b0a31637ee1b6ba607df18e7dd211fed5e10f..cb71f6bfce948836b7242682ed542bb9e282b83f 100644
--- a/signalc/src/main/kotlin/info/signalboost/signalc/model/Account.kt
+++ b/signalc/src/main/kotlin/info/signalboost/signalc/model/Account.kt
@@ -196,6 +196,11 @@ data class VerifiedAccount(
         }
     }
 
+    // accessor that returns what signalboost (currently) uses: a phone number
+    val id: String
+      get() = username
+
+    // accessor that returns what signalservice address would return: a uuid
     val identifier: String
       get() = uuid.toString()
 
diff --git a/signalc/src/main/kotlin/info/signalboost/signalc/model/SignalcAddress.kt b/signalc/src/main/kotlin/info/signalboost/signalc/model/SignalcAddress.kt
index 339ae0cad8ad91b3b725cf70862e2b7c84cf2ccf..abf383d903d5c1b28f39e2c12235e059186324a9 100644
--- a/signalc/src/main/kotlin/info/signalboost/signalc/model/SignalcAddress.kt
+++ b/signalc/src/main/kotlin/info/signalboost/signalc/model/SignalcAddress.kt
@@ -34,6 +34,10 @@ data class SignalcAddress(
             }
         }
     }
+
+    val id: String
+       get() = number!!
+
     val identifier: String
         get() = uuid?.toString() ?: number!!
 
diff --git a/signalc/src/main/kotlin/info/signalboost/signalc/model/SocketResponse.kt b/signalc/src/main/kotlin/info/signalboost/signalc/model/SocketResponse.kt
index 1d8f8033cb43486b20df74e13e34e5f804e43e79..9cbe2866a28a17e48cc52a6337ae39f9ee94dbe0 100644
--- a/signalc/src/main/kotlin/info/signalboost/signalc/model/SocketResponse.kt
+++ b/signalc/src/main/kotlin/info/signalboost/signalc/model/SocketResponse.kt
@@ -112,8 +112,9 @@ sealed class SocketResponse {
     @Serializable
     @SerialName("decryption_error")
     data class DecryptionError(
-        val sender: SignalcAddress,
         val recipient: SignalcAddress,
+        @Required
+        val sender: SignalcAddress?,
         @Serializable(ThrowableSerializer::class)
         val error: Throwable,
     ): SocketResponse()
@@ -130,7 +131,8 @@ sealed class SocketResponse {
         @Serializable
         data class Data(
             val local_address: LocalAddress,
-            val remote_address: RemoteAddress,
+            @Required
+            val remote_address: RemoteAddress?,
             @Required
             val fingerprint: String?
         )
@@ -144,8 +146,14 @@ sealed class SocketResponse {
         data class RemoteAddress(val number: String)
 
         companion object {
-            fun of(localAddress: SignalcAddress, remoteAddress: SignalcAddress, fingerprint: String? = null) =
-                InboundIdentityFailure(Data(LocalAddress(localAddress.number!!), RemoteAddress(remoteAddress.number!!), fingerprint))
+            fun of(localAddress: SignalcAddress, remoteAddress: SignalcAddress?, fingerprint: String? = null) =
+                InboundIdentityFailure(
+                    Data(
+                        LocalAddress(localAddress.number!!),
+                        remoteAddress?.let{ RemoteAddress(it.identifier) },
+                        fingerprint
+                    )
+                )
         }
     }
 
diff --git a/signalc/src/main/kotlin/info/signalboost/signalc/store/ContactStore.kt b/signalc/src/main/kotlin/info/signalboost/signalc/store/ContactStore.kt
new file mode 100644
index 0000000000000000000000000000000000000000..3ee67148f7c7c209b378f013f7192f660bdba92c
--- /dev/null
+++ b/signalc/src/main/kotlin/info/signalboost/signalc/store/ContactStore.kt
@@ -0,0 +1,136 @@
+package info.signalboost.signalc.store
+
+import info.signalboost.signalc.Application
+import info.signalboost.signalc.db.ContactRecord.Companion.findByContactId
+import info.signalboost.signalc.db.ContactRecord.Companion.updateByContactId
+import info.signalboost.signalc.db.Contacts
+import info.signalboost.signalc.db.Profiles
+import info.signalboost.signalc.dispatchers.Concurrency
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.ObsoleteCoroutinesApi
+import kotlinx.coroutines.runBlocking
+import mu.KLoggable
+import org.jetbrains.exposed.sql.*
+import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction
+import org.jetbrains.exposed.sql.transactions.transaction
+import org.signal.zkgroup.profiles.ProfileKey
+import java.util.UUID
+import kotlin.io.path.ExperimentalPathApi
+import kotlin.time.ExperimentalTime
+
+@ExperimentalCoroutinesApi
+@ObsoleteCoroutinesApi
+@ExperimentalPathApi
+@ExperimentalTime
+class ContactStore(
+    app: Application,
+    // TODO: need to consume session lock for a given account?
+) {
+
+    companion object: Any(), KLoggable {
+        override val logger = logger()
+        private const val VALID_PROFILE_KEY_SIZE = 32
+    }
+
+    val dispatcher = Concurrency.Dispatcher
+    val db = app.db
+
+    suspend fun create(
+        accountId: String,
+        phoneNumber: String? = null,
+        uuid: UUID? = null,
+        profileKey: ByteArray? = null
+    ): Int =
+        newSuspendedTransaction(dispatcher, db) {
+            Contacts.insert {
+                it[Contacts.accountId] = accountId
+                it[Contacts.phoneNumber] = phoneNumber
+                it[Contacts.uuid] = uuid
+                it[profileKeyBytes] = profileKey
+            }
+        }.resultedValues!!.single()[Contacts.contactId]
+
+    suspend fun resolveContactIdSuspend(accountId: String, identifier: String): Int =
+        newSuspendedTransaction (dispatcher, db) {
+            resolveContactId(accountId, identifier)
+        }
+
+    fun resolveContactIdBlocking(accountId: String, identifier: String): Int =
+        transaction(db) {
+            resolveContactId(accountId, identifier)
+        }
+
+    private fun resolveContactId(accountId: String, identifier: String): Int {
+        val uuid = parseUuid(identifier)
+        return run {
+            uuid
+                ?.let {
+                    logger.debug { "Found uuid: $uuid" }
+                    Contacts.select { (Contacts.accountId eq accountId).and(Contacts.uuid eq it) }
+                }
+                ?: Contacts.select {
+                    logger.debug { "Found phone number: $identifier" }
+                    (Contacts.accountId eq accountId).and(Contacts.phoneNumber eq identifier)
+                }
+        }.single().let { it[Contacts.contactId] }
+//            }.singleOrNull()
+//            ?.let {
+//                it[Contacts.contactId]
+//            }
+//            ?: runBlocking {
+//                logger.debug { "Creating new contact w/ identifier: $uuid" }
+//                val phoneNumber: String? = if(uuid == null) identifier else null
+//                create(accountId, phoneNumber, uuid)
+//            }
+    }
+
+    private fun parseUuid(identifier: String): UUID? =
+        try {
+            UUID.fromString(identifier)
+        } catch(ignored: Throwable) {
+            null
+        }
+
+    suspend fun storeUuid(accountId: String, contactPhoneNumber: String, contactUuid: UUID) {
+        // TODO: this currently only adds a uuid to a phonenumber-based contact record
+        // it shoudl be able to add either a uuid or aphonenumber
+        newSuspendedTransaction(dispatcher, db) {
+            val contactId = resolveContactId(accountId, contactPhoneNumber)
+            Contacts.updateByContactId(accountId, contactId) {
+                it[Contacts.uuid] = contactUuid
+            }
+        }
+    }
+
+    suspend fun storeProfileKey(accountId: String, contactIdentifier: String, profileKey: ByteArray) {
+        if (profileKey.size != VALID_PROFILE_KEY_SIZE) {
+            logger.warn { "Received profile key of invalid size ${profileKey.size} for account: $contactIdentifier" }
+            return
+        }
+        val contactId = resolveContactIdSuspend(accountId, contactIdentifier)
+        return newSuspendedTransaction(dispatcher, db) {
+            Contacts.updateByContactId(accountId, contactId) {
+                it[Contacts.profileKeyBytes] = profileKey
+            }
+        }
+    }
+
+    suspend fun loadProfileKey(accountId: String, contactIdentifier: String): ProfileKey? =
+        newSuspendedTransaction(dispatcher, db) {
+            val contactId = resolveContactIdSuspend(accountId, contactIdentifier)
+            Contacts.findByContactId(accountId, contactId)?.let {
+                ProfileKey(it[Contacts.profileKeyBytes])
+            }
+        }
+
+    // TESTING FUNCTIONS
+    internal suspend fun count(): Long = newSuspendedTransaction(dispatcher, db) {
+        Profiles.selectAll().count()
+    }
+
+    internal suspend fun deleteAllFor(accountId: String) = newSuspendedTransaction(dispatcher, db) {
+        Profiles.deleteWhere {
+            Profiles.accountId eq accountId
+        }
+    }
+}
\ No newline at end of file
diff --git a/signalc/src/main/kotlin/info/signalboost/signalc/store/ProfileStore.kt b/signalc/src/main/kotlin/info/signalboost/signalc/store/ProfileStore.kt
deleted file mode 100644
index b45a451501b4a8691aa9119b5f43bd158ffd2a2c..0000000000000000000000000000000000000000
--- a/signalc/src/main/kotlin/info/signalboost/signalc/store/ProfileStore.kt
+++ /dev/null
@@ -1,67 +0,0 @@
-package info.signalboost.signalc.store
-
-import info.signalboost.signalc.Application
-import info.signalboost.signalc.db.Profiles
-import info.signalboost.signalc.dispatchers.Concurrency
-import info.signalboost.signalc.serialization.ByteArrayEncoding.toPostgresHex
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.ObsoleteCoroutinesApi
-import mu.KLoggable
-import org.jetbrains.exposed.sql.*
-import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction
-import org.signal.zkgroup.profiles.ProfileKey
-import kotlin.io.path.ExperimentalPathApi
-import kotlin.time.ExperimentalTime
-
-@ExperimentalCoroutinesApi
-@ObsoleteCoroutinesApi
-@ExperimentalPathApi
-@ExperimentalTime
-class ProfileStore(app: Application) {
-    companion object: Any(), KLoggable {
-        override val logger = logger()
-        private const val VALID_PROFILE_KEY_SIZE = 32
-    }
-
-    val dispatcher = Concurrency.Dispatcher
-    val db = app.db
-
-    suspend fun storeProfileKey(accountId: String, contactId: String, profileKey: ByteArray): Unit {
-        if (profileKey.size != VALID_PROFILE_KEY_SIZE) {
-            logger.warn { "Received profile key of invalid size ${profileKey.size} for account: $accountId" }
-            return
-        }
-        return newSuspendedTransaction(dispatcher, db) {
-            exec(
-                "INSERT into profiles (account_id, contact_id, profile_key_bytes) " +
-                        "VALUES('$accountId','$contactId', ${profileKey.toPostgresHex()}) " +
-                        "ON CONFLICT (account_id, contact_id)" +
-                        "DO UPDATE set profile_key_bytes = EXCLUDED.profile_key_bytes;"
-            )
-        }
-    }
-
-    suspend fun loadProfileKey(accountId: String, contactId: String): ProfileKey? =
-        newSuspendedTransaction(dispatcher, db) {
-            Profiles.select {
-                (Profiles.accountId eq accountId).and(
-                    Profiles.contactId eq contactId
-                )
-            }.singleOrNull()?.let {
-                ProfileKey(it[Profiles.profileKeyBytes])
-            }
-        }
-
-    // TESTING FUNCTIONS
-    internal suspend fun count(): Long = newSuspendedTransaction(dispatcher, db) {
-        Profiles.selectAll().count()
-    }
-
-    internal suspend fun deleteAllFor(accountId: String) = newSuspendedTransaction(dispatcher, db) {
-        Profiles.deleteWhere {
-            Profiles.accountId eq accountId
-        }
-    }
-
-
-}
\ No newline at end of file
diff --git a/signalc/src/main/kotlin/info/signalboost/signalc/store/ProtocolStore.kt b/signalc/src/main/kotlin/info/signalboost/signalc/store/ProtocolStore.kt
index 4e0573e8bd4dc77a119a1963bc2b8096299556fa..516830e81267bce7376694e84b88f73b298ed38f 100644
--- a/signalc/src/main/kotlin/info/signalboost/signalc/store/ProtocolStore.kt
+++ b/signalc/src/main/kotlin/info/signalboost/signalc/store/ProtocolStore.kt
@@ -14,7 +14,6 @@ import org.whispersystems.libsignal.groups.state.SenderKeyStore
 import org.whispersystems.libsignal.state.*
 import org.whispersystems.signalservice.api.SignalServiceProtocolStore
 import org.whispersystems.signalservice.api.SignalServiceSessionStore
-import org.whispersystems.signalservice.api.push.SignalServiceAddress
 import java.time.Instant
 import kotlin.io.path.ExperimentalPathApi
 import kotlin.time.ExperimentalTime
@@ -24,9 +23,15 @@ import kotlin.time.ExperimentalTime
 @ObsoleteCoroutinesApi
 @ExperimentalPathApi
 @ExperimentalTime
-class ProtocolStore(app: Application) {
+class ProtocolStore(val app: Application) {
     val db = app.db
-    fun of(account: Account): AccountProtocolStore = AccountProtocolStore(db, account.username)
+    fun of(account: Account): AccountProtocolStore = AccountProtocolStore(
+        db,
+        account.username,
+        // TODO: if we need to resolve race in signal sender for first incoming message,
+        //  we might need to construct this in the constructor such that it consumes a session lock (for the account)...
+        app.contactStore::resolveContactIdBlocking
+    )
 
     fun countOwnIdentities(): Long =
         transaction(db) { OwnIdentities.selectAll().count() }
@@ -34,11 +39,13 @@ class ProtocolStore(app: Application) {
     class AccountProtocolStore(
         private val db: Database,
         private val accountId: String,
+        private val resolveContactId: (String, String) -> Int,
         val lock: SessionLock = SessionLock(),
-        private val identityStore: IdentityKeyStore = SignalcIdentityStore(db, accountId, lock),
+        // if we need to do above, we ahve to construct ContactStore
+        private val identityStore: IdentityKeyStore = SignalcIdentityStore(db, accountId, lock, resolveContactId),
         private val preKeyStore: PreKeyStore = SignalcPreKeyStore(db, accountId, lock),
         private val senderKeyStore: SenderKeyStore = SignalcSenderKeyStore(db, accountId, lock),
-        private val sessionStore: SignalServiceSessionStore = SignalcSessionStore(db, accountId, lock),
+        private val sessionStore: SignalServiceSessionStore = SignalcSessionStore(db, accountId, lock, resolveContactId),
         private val signedPreKeyStore: SignedPreKeyStore = SignalcSignedPreKeyStore(db, accountId, lock),
     ) : SignalServiceProtocolStore,
         IdentityKeyStore by identityStore,
diff --git a/signalc/src/main/kotlin/info/signalboost/signalc/store/protocol/SignalcIdentityStore.kt b/signalc/src/main/kotlin/info/signalboost/signalc/store/protocol/SignalcIdentityStore.kt
index 19f55a2e54f679af2207843f61a496db96e47c47..6827e049e81c64afe80d44c94aea9abe5132610b 100644
--- a/signalc/src/main/kotlin/info/signalboost/signalc/store/protocol/SignalcIdentityStore.kt
+++ b/signalc/src/main/kotlin/info/signalboost/signalc/store/protocol/SignalcIdentityStore.kt
@@ -25,6 +25,7 @@ class SignalcIdentityStore(
     val db: Database,
     val accountId: String,
     val lock: SessionLock,
+    val resolveContactId: (String, String) -> Int,
 ): IdentityKeyStore{
 
     override fun getIdentityKeyPair(): IdentityKeyPair =
@@ -57,8 +58,9 @@ class SignalcIdentityStore(
     @Throws(SignalcError.UpdateToNonExistentFingerprint::class)
     fun trustFingerprint(address: SignalProtocolAddress, fingerprint: ByteArray) {
         lock.acquireForTransaction(db) {
-            rejectUnknownFingerprint(address, fingerprint)
-            Identities.updateByContactId(accountId, address.name) {
+            val contactId = resolveContactId(accountId, address.name)
+            rejectUnknownFingerprint(contactId, fingerprint)
+            Identities.updateByContactId(accountId, contactId) {
                 it[isTrusted] = true
                 it[updatedAt] = Instant.now()
             }
@@ -68,22 +70,22 @@ class SignalcIdentityStore(
     @Throws(SignalcError.UpdateToNonExistentFingerprint::class)
     fun untrustFingerprint(address: SignalProtocolAddress, fingerprint: ByteArray) {
         lock.acquireForTransaction(db) {
-            rejectUnknownFingerprint(address, fingerprint)
-            Identities.updateByContactId(accountId, address.name) {
+            val contactId = resolveContactId(accountId, address.name)
+            rejectUnknownFingerprint(contactId, fingerprint)
+            Identities.updateByContactId(accountId, contactId) {
                 it[isTrusted] = false
                 it[updatedAt] = Instant.now()
             }
         }
     }
 
-    private fun rejectUnknownFingerprint(address: SignalProtocolAddress, fingerprint: ByteArray) {
+    private fun rejectUnknownFingerprint(contactId: Int, fingerprint: ByteArray) {
         transaction(db) {
-            val contactId = address.name
             val knownFingerprint = Identities.findByContactId(accountId, contactId)?.let {
                 it[identityKeyBytes]
             }
             if (!fingerprint.contentEquals(knownFingerprint))
-                throw SignalcError.UpdateToNonExistentFingerprint(contactId, fingerprint)
+                throw SignalcError.UpdateToNonExistentFingerprint(accountId, contactId, fingerprint)
         }
     }
 
@@ -93,7 +95,7 @@ class SignalcIdentityStore(
         // - deny trust for subsequent identity keys for same address
         // Returns true if this save was an update to an existing record, false otherwise
         lock.acquireForTransaction(db) {
-            val contactId = address.name
+            val contactId = resolveContactId(accountId, address.name)
             Identities.findByContactId(accountId, contactId)
                 ?.let { existingKey ->
                     Identities.updateByContactId(accountId, contactId) {
@@ -121,7 +123,7 @@ class SignalcIdentityStore(
     ): Boolean =
         lock.acquireForTransaction(db) {
             // trust a key if...
-            Identities.findByContactId(accountId, address.name)?.let {
+            Identities.findByContactId(accountId, resolveContactId(accountId, address.name))?.let {
                 // it matches a key we have seen before
                 it[identityKeyBytes] contentEquals identityKey.serialize() &&
                         // and we have not flagged it as untrusted
@@ -131,14 +133,14 @@ class SignalcIdentityStore(
 
     override fun getIdentity(address: SignalProtocolAddress): IdentityKey? =
         lock.acquireForTransaction(db) {
-            Identities.findByContactId(accountId, address.name)?.let {
+            Identities.findByContactId(accountId, resolveContactId(accountId, address.name))?.let {
                 IdentityKey(it[identityKeyBytes], 0)
             }
         }
 
     fun removeIdentity(address: SignalProtocolAddress) {
         lock.acquireForTransaction(db) {
-            Identities.deleteByContactId(accountId, address.name)
+            Identities.deleteByContactId(accountId, resolveContactId(accountId, address.name))
         }
     }
 
@@ -158,8 +160,7 @@ class SignalcIdentityStore(
 
     fun whenIdentityLastUpdated(address: SignalProtocolAddress): Instant? =
         transaction(db) {
-            val contactId = address.name
-            Identities.findByContactId(accountId, contactId)?.let {
+            Identities.findByContactId(accountId, resolveContactId(accountId, address.name))?.let {
                 it[updatedAt]
             }
         }
diff --git a/signalc/src/main/kotlin/info/signalboost/signalc/store/protocol/SignalcSessionStore.kt b/signalc/src/main/kotlin/info/signalboost/signalc/store/protocol/SignalcSessionStore.kt
index d09a1d2ad695c49030f09de889c7d5ac57c837ed..4423a5fd3b3684a49ed88aeb1267ca07868d6b59 100644
--- a/signalc/src/main/kotlin/info/signalboost/signalc/store/protocol/SignalcSessionStore.kt
+++ b/signalc/src/main/kotlin/info/signalboost/signalc/store/protocol/SignalcSessionStore.kt
@@ -1,10 +1,11 @@
 package info.signalboost.signalc.store.protocol
 
 import info.signalboost.signalc.db.*
+import info.signalboost.signalc.db.ContactRecord.Companion.deleteByContactId
 import info.signalboost.signalc.db.ContactRecord.Companion.findManyByContactId
-import info.signalboost.signalc.db.DeviceRecord.Companion.deleteByAddress
-import info.signalboost.signalc.db.DeviceRecord.Companion.findByAddress
-import info.signalboost.signalc.db.DeviceRecord.Companion.updateByAddress
+import info.signalboost.signalc.db.DeviceRecord.Companion.deleteByDeviceId
+import info.signalboost.signalc.db.DeviceRecord.Companion.findByDeviceId
+import info.signalboost.signalc.db.DeviceRecord.Companion.updateByDeviceId
 import info.signalboost.signalc.db.Sessions.sessionBytes
 import org.jetbrains.exposed.sql.*
 import org.whispersystems.libsignal.SignalProtocolAddress
@@ -16,11 +17,13 @@ class SignalcSessionStore(
     val db: Database,
     val accountId: String,
     val lock: SessionLock,
+    val resolveContactId: (String, String) -> Int,
 ): SignalServiceSessionStore {
 
     override fun loadSession(address: SignalProtocolAddress): SessionRecord =
         lock.acquireForTransaction(db) {
-            Sessions.findByAddress(accountId, address)?.let {
+            val contactId = resolveContactId(accountId, address.name)
+            Sessions.findByDeviceId(accountId, contactId, address.deviceId)?.let {
                 SessionRecord(it[sessionBytes])
             } ?: SessionRecord()
         }
@@ -28,34 +31,35 @@ class SignalcSessionStore(
     override fun getSubDeviceSessions(name: String): MutableList<Int> =
         lock.acquireForTransaction(db) {
             Sessions.select {
-                Sessions.accountId eq accountId and (Sessions.contactId eq name)
+                (Sessions.accountId eq accountId).and(Sessions.contactId eq resolveContactId(accountId, name))
             }.mapTo(mutableListOf()) { it[Sessions.deviceId] }
         }
 
     override fun storeSession(address: SignalProtocolAddress, record: SessionRecord) {
         // upsert the session record for a given address
         lock.acquireForTransaction(db) {
-                Sessions.updateByAddress(accountId, address) {
-                    it[sessionBytes] = record.serialize()
-                }.let { numUpdated ->
-                    if (numUpdated == 0) {
-                        Sessions.insert {
-                            it[accountId] = this@SignalcSessionStore.accountId
-                            it[contactId] = address.name
-                            it[deviceId] = address.deviceId
-                            it[sessionBytes] = record.serialize()
-                        }
+            val contactId = resolveContactId(accountId, address.name)
+            Sessions.updateByDeviceId(accountId, contactId, address.deviceId) {
+                it[sessionBytes] = record.serialize()
+            }.let { numUpdated ->
+                if (numUpdated == 0) {
+                    Sessions.insert {
+                        it[accountId] = this@SignalcSessionStore.accountId
+                        it[Sessions.contactId] = contactId
+                        it[deviceId] = address.deviceId
+                        it[sessionBytes] = record.serialize()
                     }
                 }
             }
         }
+    }
 
     override fun containsSession(address: SignalProtocolAddress): Boolean =
         lock.acquireForTransaction(db) {
-            Sessions.findByAddress(accountId, address)?.let {
+            val contactId = resolveContactId(accountId, address.name)
+            Sessions.findByDeviceId(accountId, contactId, address.deviceId)?.let {
                 val sessionRecord = SessionRecord(it[sessionBytes])
-                sessionRecord.hasSenderChain()
-                        && sessionRecord.sessionVersion == CiphertextMessage.CURRENT_VERSION;
+                sessionRecord.hasSenderChain() && sessionRecord.sessionVersion == CiphertextMessage.CURRENT_VERSION
             } ?: false
         }
 
@@ -63,22 +67,22 @@ class SignalcSessionStore(
 
     override fun deleteSession(address: SignalProtocolAddress) {
         lock.acquireForTransaction(db) {
-            Sessions.deleteByAddress(accountId, address)
+            val contactId = resolveContactId(accountId, address.name)
+            Sessions.deleteByDeviceId(accountId, contactId, address.deviceId)
         }
     }
 
 
     override fun deleteAllSessions(name: String) {
         lock.acquireForTransaction(db) {
-            Sessions.deleteWhere {
-                Sessions.accountId eq accountId and (Sessions.contactId eq name)
-            }
+            Sessions.deleteByContactId(accountId, resolveContactId(accountId, name))
         }
     }
 
     override fun archiveSession(address: SignalProtocolAddress) {
         lock.acquireForTransaction(db) {
-            Sessions.findByAddress(accountId, address)?.let {
+            val contactId = resolveContactId(accountId, address.name)
+            Sessions.findByDeviceId(accountId, contactId, address.deviceId)?.let {
                 val session = SessionRecord(it[sessionBytes])
                 session.archiveCurrentState()
                 storeSession(address, session)
@@ -88,7 +92,7 @@ class SignalcSessionStore(
 
     fun archiveAllSessions(address: SignalProtocolAddress) {
         lock.acquireForTransaction(db) {
-            val contactId = address.name
+            val contactId = resolveContactId(accountId, address.name)
             Sessions.findManyByContactId(accountId, contactId).forEach {
                 val session = SessionRecord(it[sessionBytes])
                 session.archiveCurrentState()
@@ -96,6 +100,4 @@ class SignalcSessionStore(
             }
         }
     }
-
-
 }
\ No newline at end of file
diff --git a/signalc/src/test/kotlin/info/signalboost/signalc/logic/AccountManagerTest.kt b/signalc/src/test/kotlin/info/signalboost/signalc/logic/AccountManagerTest.kt
index a05427a9c071567ba7d0e4d16af2b8bc49e8d6bc..8c352ad7d4b29ae40fb8fac944dc05027c7f997e 100644
--- a/signalc/src/test/kotlin/info/signalboost/signalc/logic/AccountManagerTest.kt
+++ b/signalc/src/test/kotlin/info/signalboost/signalc/logic/AccountManagerTest.kt
@@ -293,7 +293,7 @@ class AccountManagerTest : FreeSpec({
             } returns senderCert.serialized
 
             coEvery {
-                app.profileStore.loadProfileKey(verifiedAccount.identifier, any())
+                app.contactStore.loadProfileKey(verifiedAccount.identifier, any())
             } coAnswers  {
                 if (secondArg<String>() == knownContactId) contactProfileKey
                 else null
diff --git a/signalc/src/test/kotlin/info/signalboost/signalc/logic/SignalReceiverTest.kt b/signalc/src/test/kotlin/info/signalboost/signalc/logic/SignalReceiverTest.kt
index 284eed27ef3d37112ba2152824608bf1477b43ec..6ca55166a29b1e1b61004704408612657aad9933 100644
--- a/signalc/src/test/kotlin/info/signalboost/signalc/logic/SignalReceiverTest.kt
+++ b/signalc/src/test/kotlin/info/signalboost/signalc/logic/SignalReceiverTest.kt
@@ -428,7 +428,7 @@ class SignalReceiverTest : FreeSpec({
                         messageReceiver.subscribe(recipientAccount)
                         eventually(timeout) {
                             coVerify {
-                                app.profileStore.storeProfileKey(
+                                app.contactStore.storeProfileKey(
                                     recipientAccount.address.identifier,
                                     senderAddress.identifier,
                                     fakeProfileKey,
diff --git a/signalc/src/test/kotlin/info/signalboost/signalc/model/SocketResponseTest.kt b/signalc/src/test/kotlin/info/signalboost/signalc/model/SocketResponseTest.kt
index 4cd4882c894ad2babc8a13e35900f6f319d92e91..553391e6f38039b2a6bd365975698e27b6c62036 100644
--- a/signalc/src/test/kotlin/info/signalboost/signalc/model/SocketResponseTest.kt
+++ b/signalc/src/test/kotlin/info/signalboost/signalc/model/SocketResponseTest.kt
@@ -181,8 +181,8 @@ class SocketResponseTest : FreeSpec({
                 |{
                    |"type":"decryption_error",
                    |"sender":{
-                      |"number":"${response.sender.number}",
-                      |"uuid":"${response.sender.uuid}"
+                      |"number":"${response.sender?.number}",
+                      |"uuid":"${response.sender?.uuid}"
                    |},
                    |"recipient":{
                       |"number":"${response.recipient.number}",
@@ -219,7 +219,7 @@ class SocketResponseTest : FreeSpec({
                       |"number":"${response.data.local_address.number}"
                     |},
                     |"remote_address":{
-                      |"number":"${response.data.remote_address.number}"
+                      |"number":"${response.data.remote_address?.number}"
                     |},
                     |"fingerprint":"${response.data.fingerprint}"
                   |}
diff --git a/signalc/src/test/kotlin/info/signalboost/signalc/store/ProfileStoreTest.kt b/signalc/src/test/kotlin/info/signalboost/signalc/store/ContactStoreTest.kt
similarity index 64%
rename from signalc/src/test/kotlin/info/signalboost/signalc/store/ProfileStoreTest.kt
rename to signalc/src/test/kotlin/info/signalboost/signalc/store/ContactStoreTest.kt
index 8abba4eebecfdd5c8a8998002d01e762a85eeba3..ea6b6ab750be708866f315e0e9ac79e43f6aeb4d 100644
--- a/signalc/src/test/kotlin/info/signalboost/signalc/store/ProfileStoreTest.kt
+++ b/signalc/src/test/kotlin/info/signalboost/signalc/store/ContactStoreTest.kt
@@ -1,7 +1,6 @@
 package info.signalboost.signalc.store
 
 import info.signalboost.signalc.util.KeyUtil.genRandomBytes
-import org.signal.zkgroup.profiles.ProfileKey
 import com.zaxxer.hikari.HikariDataSource
 import info.signalboost.signalc.Application
 import info.signalboost.signalc.Config
@@ -19,10 +18,10 @@ import kotlin.time.ExperimentalTime
 @ExperimentalTime
 @ObsoleteCoroutinesApi
 @ExperimentalCoroutinesApi
-class ProfileStoreTest: FreeSpec({
+class ContactStoreTest: FreeSpec({
     runBlockingTest {
         val testScope = this
-        val config = Config.mockAllExcept(ProfileStore::class, HikariDataSource::class)
+        val config = Config.mockAllExcept(ContactStore::class, HikariDataSource::class)
         val app = Application(config).run(testScope)
 
         val accountId = genPhoneNumber()
@@ -37,45 +36,45 @@ class ProfileStoreTest: FreeSpec({
 
         "Profile store" - {
             afterTest {
-                app.profileStore.deleteAllFor(accountId)
+                app.contactStore.deleteAllFor(accountId)
             }
 
             "#loadProfileKey" - {
                 beforeTest {
-                    app.profileStore.storeProfileKey(accountId, contactId, contactProfileKey)
+                    app.contactStore.storeProfileKey(accountId, contactId, contactProfileKey)
                 }
 
                 "returns profile key for a contact if it exists" {
-                    app.profileStore.loadProfileKey(accountId, contactId)?.serialize() shouldBe contactProfileKey
+                    app.contactStore.loadProfileKey(accountId, contactId)?.serialize() shouldBe contactProfileKey
                 }
 
                 "returns null if no profile key exists for contact" {
-                    app.profileStore.loadProfileKey(accountId, genPhoneNumber()) shouldBe null
+                    app.contactStore.loadProfileKey(accountId, genPhoneNumber()) shouldBe null
                 }
             }
 
             "#storeProfileKey" - {
                 "stores a new profile key for a new contact" {
-                    val startingCount = app.profileStore.count()
-                    app.profileStore.storeProfileKey(accountId, contactId, contactProfileKey)
-                    app.profileStore.count() shouldBe startingCount + 1
-                    app.profileStore.loadProfileKey(accountId, contactId)?.serialize() shouldBe contactProfileKey
+                    val startingCount = app.contactStore.count()
+                    app.contactStore.storeProfileKey(accountId, contactId, contactProfileKey)
+                    app.contactStore.count() shouldBe startingCount + 1
+                    app.contactStore.loadProfileKey(accountId, contactId)?.serialize() shouldBe contactProfileKey
                 }
 
                 "overwrites the old profile key for an existing contact" {
-                    val startingCount = app.profileStore.count()
-                    app.profileStore.storeProfileKey(accountId, contactId, contactProfileKey)
-                    app.profileStore.storeProfileKey(accountId, contactId, contactUpdatedProfileKey)
-                    app.profileStore.count() shouldBe startingCount + 1
-                    app.profileStore.loadProfileKey(accountId, contactId)?.serialize() shouldBe contactUpdatedProfileKey
+                    val startingCount = app.contactStore.count()
+                    app.contactStore.storeProfileKey(accountId, contactId, contactProfileKey)
+                    app.contactStore.storeProfileKey(accountId, contactId, contactUpdatedProfileKey)
+                    app.contactStore.count() shouldBe startingCount + 1
+                    app.contactStore.loadProfileKey(accountId, contactId)?.serialize() shouldBe contactUpdatedProfileKey
                 }
 
                 "safely stores the same profile key twice" {
-                    val startingCount = app.profileStore.count()
-                    app.profileStore.storeProfileKey(accountId, contactId, contactProfileKey)
-                    app.profileStore.storeProfileKey(accountId, contactId, contactProfileKey)
-                    app.profileStore.count() shouldBe startingCount + 1
-                    app.profileStore.loadProfileKey(accountId, contactId)?.serialize() shouldBe contactProfileKey
+                    val startingCount = app.contactStore.count()
+                    app.contactStore.storeProfileKey(accountId, contactId, contactProfileKey)
+                    app.contactStore.storeProfileKey(accountId, contactId, contactProfileKey)
+                    app.contactStore.count() shouldBe startingCount + 1
+                    app.contactStore.loadProfileKey(accountId, contactId)?.serialize() shouldBe contactProfileKey
                 }
             }
         }
diff --git a/signalc/src/test/kotlin/info/signalboost/signalc/testSupport/db/MigrationUtil.kt b/signalc/src/test/kotlin/info/signalboost/signalc/testSupport/db/MigrationUtil.kt
index ca0393078f2ac8c6ddd7766fb60f14bf9848193d..6fc0f581b898fcd0a8aae32cd2b0f1f33fe23ace 100644
--- a/signalc/src/test/kotlin/info/signalboost/signalc/testSupport/db/MigrationUtil.kt
+++ b/signalc/src/test/kotlin/info/signalboost/signalc/testSupport/db/MigrationUtil.kt
@@ -1,8 +1,6 @@
 package info.signalboost.signalc.testSupport.db
 
-import info.signalboost.signalc.db.Identities
-import info.signalboost.signalc.db.Profiles
-import info.signalboost.signalc.db.SenderKeys
+import info.signalboost.signalc.db.Contacts
 import org.jetbrains.exposed.sql.*
 import org.jetbrains.exposed.sql.transactions.transaction
 
@@ -31,7 +29,7 @@ import org.jetbrains.exposed.sql.transactions.transaction
 
 fun main() {
     // change this assignment to
-    val table = Profiles
+    val table = Contacts
     val db = Database.connect(
         driver = "org.postgresql.Driver",
         url = "jdbc:postgresql://localhost:5432/signalc_scratch",
diff --git a/signalc/src/test/kotlin/info/signalboost/signalc/testSupport/matchers/SocketResponseMatchers.kt b/signalc/src/test/kotlin/info/signalboost/signalc/testSupport/matchers/SocketResponseMatchers.kt
index b0d277fdde334dc41188f0eda189c40ac2143039..32918f4cb3f65ba4d5b6cbfb961948e5c2e6d860 100644
--- a/signalc/src/test/kotlin/info/signalboost/signalc/testSupport/matchers/SocketResponseMatchers.kt
+++ b/signalc/src/test/kotlin/info/signalboost/signalc/testSupport/matchers/SocketResponseMatchers.kt
@@ -52,7 +52,7 @@ object SocketResponseMatchers {
         fingerprint: String?
     ): SocketResponse.InboundIdentityFailure = match {
         it.data.local_address.number == recipient.number &&
-                it.data.remote_address.number == sender.number &&
+                it.data.remote_address?.number == sender.number &&
                 it.data.fingerprint == fingerprint
     }