diff --git a/mailbox-cli/src/main/java/org/briarproject/mailbox/cli/Main.kt b/mailbox-cli/src/main/java/org/briarproject/mailbox/cli/Main.kt
index 24784e8ba8a1a0c2b205a4ed2282a68950c36e82..cb2abda0e79e9e6fb4f6ea9125b93a18f0bb2d9f 100644
--- a/mailbox-cli/src/main/java/org/briarproject/mailbox/cli/Main.kt
+++ b/mailbox-cli/src/main/java/org/briarproject/mailbox/cli/Main.kt
@@ -8,7 +8,10 @@ import com.github.ajalt.clikt.parameters.options.flag
 import com.github.ajalt.clikt.parameters.options.option
 import org.briarproject.mailbox.core.CoreEagerSingletons
 import org.briarproject.mailbox.core.JavaCliEagerSingletons
+import org.briarproject.mailbox.core.db.Database
 import org.briarproject.mailbox.core.lifecycle.LifecycleManager
+import org.briarproject.mailbox.core.setup.QrCodeEncoder
+import org.briarproject.mailbox.core.setup.SetupManager
 import org.slf4j.LoggerFactory.getLogger
 import java.util.logging.Level.ALL
 import java.util.logging.Level.INFO
@@ -38,6 +41,15 @@ class Main : CliktCommand(
     @Inject
     internal lateinit var lifecycleManager: LifecycleManager
 
+    @Inject
+    internal lateinit var db: Database
+
+    @Inject
+    internal lateinit var setupManager: SetupManager
+
+    @Inject
+    internal lateinit var qrCodeEncoder: QrCodeEncoder
+
     override fun run() {
         // logging
         val levelSlf4j = if (debug) Level.DEBUG else when (verbosity) {
@@ -68,6 +80,18 @@ class Main : CliktCommand(
 
         lifecycleManager.startServices()
         lifecycleManager.waitForStartup()
+
+        // TODO this is obviously not the final code, just a stub to get us started
+        val setupTokenExists = db.transactionWithResult(true) { txn ->
+            setupManager.getSetupToken(txn) != null
+        }
+        val ownerTokenExists = db.transactionWithResult(true) { txn ->
+            setupManager.getOwnerToken(txn) != null
+        }
+        if (!setupTokenExists && !ownerTokenExists) setupManager.restartSetup()
+        qrCodeEncoder.getQrCodeBitMatrix()?.let {
+            println(QrCodeRenderer.getQrString(it))
+        }
     }
 
 }
diff --git a/mailbox-cli/src/main/java/org/briarproject/mailbox/cli/QrCodeRenderer.kt b/mailbox-cli/src/main/java/org/briarproject/mailbox/cli/QrCodeRenderer.kt
new file mode 100644
index 0000000000000000000000000000000000000000..c96f6123b021fb36a270a23d9d587e5f22bf7e5e
--- /dev/null
+++ b/mailbox-cli/src/main/java/org/briarproject/mailbox/cli/QrCodeRenderer.kt
@@ -0,0 +1,20 @@
+package org.briarproject.mailbox.cli
+
+import com.google.zxing.common.BitMatrix
+
+object QrCodeRenderer {
+
+    private const val SET = "██"
+    private const val UNSET = "  "
+
+    internal fun getQrString(bitMatrix: BitMatrix): String = StringBuilder().apply {
+        append(System.lineSeparator())
+        for (y in 0 until bitMatrix.height) {
+            for (x in 0 until bitMatrix.width) {
+                append(if (bitMatrix[x, y]) SET else UNSET)
+            }
+            append(System.lineSeparator())
+        }
+    }.toString()
+
+}
diff --git a/mailbox-core/build.gradle b/mailbox-core/build.gradle
index 43bc03c273d58bb1bc2f4c32dc23e030d7016e00..7d4568cd69a806bef811c46100ca126db6a57f0c 100644
--- a/mailbox-core/build.gradle
+++ b/mailbox-core/build.gradle
@@ -30,6 +30,8 @@ dependencies {
 
     // Base 32
     implementation 'dev.keiji.rfc4648:rfc4648:1.0.0'
+    // QrCode
+    implementation 'com.google.zxing:core:3.4.1'
 
     testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
     testImplementation "org.junit.jupiter:junit-jupiter-api:$junit_version"
diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/setup/QrCodeEncoder.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/setup/QrCodeEncoder.kt
index 4857bcc1d1158e0bd55eaf29adee76d3e5fb021e..334482d378562efaa5e64ce376ae791b75c8933c 100644
--- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/setup/QrCodeEncoder.kt
+++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/setup/QrCodeEncoder.kt
@@ -1,5 +1,8 @@
 package org.briarproject.mailbox.core.setup
 
+import com.google.zxing.BarcodeFormat.QR_CODE
+import com.google.zxing.common.BitMatrix
+import com.google.zxing.qrcode.QRCodeWriter
 import dev.keiji.util.Base32
 import org.briarproject.mailbox.core.db.Database
 import org.briarproject.mailbox.core.db.DbException
@@ -8,6 +11,7 @@ import org.briarproject.mailbox.core.util.LogUtils.logException
 import org.briarproject.mailbox.core.util.StringUtils.fromHexString
 import org.slf4j.LoggerFactory.getLogger
 import java.nio.ByteBuffer
+import java.nio.charset.Charset
 import javax.inject.Inject
 
 private const val VERSION = 32
@@ -19,7 +23,14 @@ class QrCodeEncoder @Inject constructor(
     private val torPlugin: TorPlugin,
 ) {
 
-    fun getQrCodeBytes(): ByteArray? {
+    fun getQrCodeBitMatrix(edgeLen: Int = 0): BitMatrix? {
+        val bytes = getQrCodeBytes() ?: return null
+        // Use ISO 8859-1 to encode bytes directly as a string
+        val content = String(bytes, Charset.forName("ISO-8859-1"))
+        return QRCodeWriter().encode(content, QR_CODE, edgeLen, edgeLen)
+    }
+
+    private fun getQrCodeBytes(): ByteArray? {
         val hiddenServiceBytes = getHiddenServiceBytes() ?: return null
         val setupTokenBytes = getSetupTokenBytes() ?: return null
         return ByteBuffer.allocate(65)