diff --git a/CHANGELOG.md b/CHANGELOG.md
index a0fdf9be..4bfc4f9c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,4 @@
+* Allow the supervisor to work in offline mode [Pablo]
 * Fix duplicate logs issue [Kostas]
 * **[Breaking]** Do not bind mount /run/dbus to /run/dbus [Pablo]
 * Default to not bind mounting kmod if container distro can't be found [Pablo]
diff --git a/src/app.coffee b/src/app.coffee
index d7df0183..bd8a1031 100644
--- a/src/app.coffee
+++ b/src/app.coffee
@@ -20,16 +20,17 @@ knex.init.then ->
 		utils.mixpanelProperties.uuid = uuid
 
 		api = require './api'
-		application = require('./application')(logsChannel)
+		application = require('./application')(logsChannel, bootstrap.offlineMode)
 		device = require './device'
 
+		console.log('Starting API server..')
+		apiServer = api(application).listen(config.listenPort)
+		apiServer.timeout = config.apiTimeout
+
 		bootstrap.done
 		.then ->
 			device.getOSVersion()
 		.then (osVersion) ->
-			console.log('Starting API server..')
-			apiServer = api(application).listen(config.listenPort)
-			apiServer.timeout = config.apiTimeout
 			# Let API know what version we are, and our api connection info.
 			console.log('Updating supervisor version and api info')
 			device.updateState(
diff --git a/src/application.coffee b/src/application.coffee
index 67d87cea..13f59bf2 100644
--- a/src/application.coffee
+++ b/src/application.coffee
@@ -703,10 +703,11 @@ application.initialize = ->
 		application.poll()
 		application.update()
 
-module.exports = (logsChannel) ->
+module.exports = (logsChannel, offlineMode) ->
 	logger.init(
 		dockerSocket: config.dockerSocket
 		pubnub: config.pubnub
 		channel: "device-#{logsChannel}-logs"
+		offlineMode: offlineMode
 	)
 	return application
diff --git a/src/bootstrap.coffee b/src/bootstrap.coffee
index 7281f6fa..a321c562 100644
--- a/src/bootstrap.coffee
+++ b/src/bootstrap.coffee
@@ -64,12 +64,12 @@ bootstrap = ->
 		.tap ->
 			bootstrapper.doneBootstrapping()
 
-readConfigAndEnsureUUID = ->
-	# Load config file
+readConfig = ->
 	fs.readFileAsync(configPath, 'utf8')
 	.then(JSON.parse)
-	.then (configFromFile) ->
-		userConfig = configFromFile
+
+readConfigAndEnsureUUID = ->
+	Promise.try ->
 		return userConfig.uuid if userConfig.uuid?
 		deviceRegister.generateUUID()
 		.then (uuid) ->
@@ -84,6 +84,8 @@ readConfigAndEnsureUUID = ->
 
 bootstrapOrRetry = ->
 	utils.mixpanelTrack('Device bootstrap')
+	# If we're in offline mode, we don't start the provisioning process so bootstrap.done will never fulfill
+	return if bootstrapper.offlineMode
 	bootstrap().catch (err) ->
 		utils.mixpanelTrack('Device bootstrap failed, retrying', { error: err, delay: config.bootstrapRetryDelay })
 		setTimeout(bootstrapOrRetry, config.bootstrapRetryDelay)
@@ -95,10 +97,15 @@ bootstrapper.done = new Promise (resolve) ->
 
 bootstrapper.bootstrapped = false
 bootstrapper.startBootstrapping = ->
-	knex('config').select('value').where(key: 'uuid')
+	# Load config file
+	readConfig()
+	.then (configFromFile) ->
+		userConfig = configFromFile
+		bootstrapper.offlineMode = Boolean(userConfig.supervisorOfflineMode)
+		knex('config').select('value').where(key: 'uuid')
 	.then ([ uuid ]) ->
 		if uuid?.value
-			bootstrapper.doneBootstrapping()
+			bootstrapper.doneBootstrapping() if !bootstrapper.offlineMode
 			return uuid.value
 		console.log('New device detected. Bootstrapping..')
 		readConfigAndEnsureUUID()
diff --git a/src/device.coffee b/src/device.coffee
index babaa953..a191940b 100644
--- a/src/device.coffee
+++ b/src/device.coffee
@@ -9,6 +9,7 @@ configPath = '/boot/config.json'
 request = Promise.promisifyAll(require('request'))
 execAsync = Promise.promisify(require('child_process').exec)
 fs = Promise.promisifyAll(require('fs'))
+bootstrap = require './bootstrap'
 
 exports.getID = do ->
 	deviceIdPromise = null
@@ -17,10 +18,14 @@ exports.getID = do ->
 		deviceIdPromise ?= Promise.rejected()
 		# Only fetch the device id once (when successful, otherwise retry for each request)
 		deviceIdPromise = deviceIdPromise.catch ->
-			Promise.all([
-				knex('config').select('value').where(key: 'apiKey')
-				knex('config').select('value').where(key: 'uuid')
-			])
+			# Wait for bootstrapping to be done before querying the Resin API
+			# This will also block users of getID (like applyState below until this is resolved)
+			bootstrap.done
+			.then ->
+				Promise.all([
+					knex('config').select('value').where(key: 'apiKey')
+					knex('config').select('value').where(key: 'uuid')
+				])
 			.spread ([{ value: apiKey }], [{ value: uuid }]) ->
 				resinApi.get(
 					resource: 'device'
diff --git a/src/lib/logger.coffee b/src/lib/logger.coffee
index 9f9a1f71..eb6ae7b0 100644
--- a/src/lib/logger.coffee
+++ b/src/lib/logger.coffee
@@ -23,6 +23,10 @@ publish = do ->
 	publishQueue = []
 
 	initialised.then (config) ->
+		if config.offlineMode
+			publish = _.noop
+			publishQueue = null
+			return
 		pubnub = PUBNUB.init(config.pubnub)
 		channel = config.channel
 
diff --git a/src/utils.coffee b/src/utils.coffee
index d6119f05..9240ad59 100644
--- a/src/utils.coffee
+++ b/src/utils.coffee
@@ -18,10 +18,14 @@ tagExtra = process.env.SUPERVISOR_TAG_EXTRA
 version += '+' + tagExtra if !_.isEmpty(tagExtra)
 exports.supervisorVersion = version
 
-mixpanelClient = mixpanel.init(config.mixpanelToken)
+configJson = require('/boot/config.json')
+if Boolean(configJson.supervisorOfflineMode)
+	mixpanelClient = mixpanel.init(config.mixpanelToken)
+else
+	mixpanelClient = { track: _.noop }
 
 exports.mixpanelProperties = mixpanelProperties =
-	username: require('/boot/config.json').username
+	username: configJson.username
 
 exports.mixpanelTrack = (event, properties = {}) ->
 	# Allow passing in an error directly and having it assigned to the error property.