From f8c5f7b9670548c9e08400fa688e5f5991c3c76c Mon Sep 17 00:00:00 2001
From: Ben Gotow <bengotow@gmail.com>
Date: Fri, 28 Aug 2015 11:12:53 -0700
Subject: [PATCH] refactor(db): change ID system to have clientIDs and
 serverIDs

Summary: Major ID refactor

Test Plan: edgehill --test

Reviewers: bengotow, dillon

Differential Revision: https://phab.nylas.com/D1946
---
 build/Gruntfile.coffee                        |  16 +-
 build/tasks/nylaslint-task.coffee             |   2 +-
 exports/nylas-exports.coffee                  |   1 -
 .../attachments/lib/attachment-component.cjsx |   4 +-
 .../composer/lib/composer-view.cjsx           |  50 +++---
 internal_packages/composer/lib/main.cjsx      |   2 +-
 .../composer/spec/composer-view-spec.cjsx     |  56 +++----
 .../spec/participants-text-field-spec.cjsx    |  13 +-
 internal_packages/events/lib/main.cjsx        |   2 +-
 .../message-list/lib/message-controls.cjsx    |  17 +-
 .../lib/message-item-container.cjsx           |  13 +-
 .../message-list/lib/message-list.cjsx        |  10 +-
 .../spec/message-item-container-spec.cjsx     |   4 +-
 .../message-list/spec/message-item-spec.cjsx  |   2 +-
 .../message-list/spec/message-list-spec.cjsx  |  25 ++-
 .../spec/message-timestamp-spec.cjsx          |   2 +-
 .../lib/template-picker.cjsx                  |   4 +-
 .../lib/template-status-bar.cjsx              |   6 +-
 .../lib/template-store.coffee                 |  10 +-
 .../spec/template-store-spec.coffee           |  12 +-
 .../thread-list/lib/draft-list-store.coffee   |   7 +-
 .../thread-list/lib/draft-list.cjsx           |   6 +-
 .../thread-list/lib/thread-list-icon.cjsx     |   2 +-
 .../thread-list/spec/thread-list-spec.cjsx    |  13 +-
 internal_packages/undo-redo/lib/main.cjsx     |   2 +-
 .../spec/nylas-sync-worker-spec.coffee        |   4 +-
 .../worker-ui/lib/developer-bar-store.coffee  |   3 +-
 spec-nylas/action-bridge-spec.coffee          |  18 +--
 .../tokenizing-text-field-spec.cjsx           |  10 +-
 spec-nylas/fixtures/db-test-model.coffee      |  40 +++++
 spec-nylas/models/model-spec.coffee           |  46 ++++--
 spec-nylas/models/thread-spec.coffee          |   1 -
 spec-nylas/stores/contact-store-spec.coffee   |   6 +-
 .../database-setup-query-builder-spec.coffee  |   2 +-
 spec-nylas/stores/database-store-spec.coffee  |  14 +-
 spec-nylas/stores/draft-store-spec.coffee     |  39 ++---
 spec-nylas/stores/event-store-spec.coffee     |  22 ++-
 .../stores/file-download-store-spec.coffee    |   8 +-
 .../stores/file-upload-store-spec.coffee      |  12 +-
 .../stores/focused-category-store-spec.coffee |   2 +-
 .../stores/focused-contacts-store-spec.coffee |  12 +-
 spec-nylas/stores/task-queue-spec.coffee      |   2 -
 .../stores/unread-count-store-spec.coffee     |   4 +-
 spec-nylas/task-spec.coffee                   |  44 ------
 spec-nylas/tasks/event-rsvp-spec.coffee       |   2 +-
 spec-nylas/tasks/file-upload-task-spec.coffee |  26 +--
 spec-nylas/tasks/send-draft-spec.coffee       |  39 ++---
 spec-nylas/tasks/syncback-draft-spec.coffee   |  50 ++----
 spec/fixtures/coffee.coffee                   |   4 +-
 ...ample-with-tabs-and-leading-comment.coffee |   4 -
 spec/fixtures/sample-with-tabs.coffee         |   4 -
 spec/spec-helper.coffee                       |  13 +-
 src/browser/application.coffee                |   8 +-
 src/components/injected-component.cjsx        |   4 +-
 src/flux/actions.coffee                       |   9 +-
 src/flux/attributes.coffee                    |   3 +
 .../attributes/attribute-collection.coffee    |  16 +-
 src/flux/attributes/attribute-object.coffee   |   2 +-
 src/flux/attributes/attribute-serverid.coffee |  22 +++
 src/flux/models/event.coffee                  |   5 -
 src/flux/models/local-link.coffee             |  18 ---
 src/flux/models/message.coffee                |   5 +-
 src/flux/models/model.coffee                  |  55 +++++--
 src/flux/models/thread.coffee                 |  14 --
 src/flux/models/utils.coffee                  |   2 +-
 src/flux/nylas-api.coffee                     |  69 ++++----
 src/flux/stores/analytics-store.coffee        |   8 +-
 src/flux/stores/database-store.coffee         | 149 ++----------------
 src/flux/stores/draft-store-proxy.coffee      |  27 ++--
 src/flux/stores/draft-store.coffee            | 119 +++++++-------
 src/flux/stores/file-download-store.coffee    |   4 +-
 src/flux/stores/file-upload-store.coffee      |  26 +--
 src/flux/stores/focused-contacts-store.coffee |  20 +--
 src/flux/stores/message-store.coffee          | 104 ++++++------
 src/flux/stores/model-view.coffee             |   2 +-
 src/flux/stores/task-queue.coffee             |   3 +-
 src/flux/tasks/change-folder-task.coffee      |   2 +-
 src/flux/tasks/change-mail-task.coffee        |   2 +-
 src/flux/tasks/destroy-draft.coffee           |  26 +--
 src/flux/tasks/file-upload-task.coffee        |  10 +-
 src/flux/tasks/send-draft.coffee              |  32 ++--
 src/flux/tasks/syncback-draft.coffee          |  50 +++---
 .../lib/my-composer-button.cjsx               |   6 +-
 .../spec/my-composer-button-spec.cjsx         |   4 +-
 84 files changed, 654 insertions(+), 884 deletions(-)
 delete mode 100644 spec-nylas/task-spec.coffee
 delete mode 100644 spec/fixtures/sample-with-tabs-and-leading-comment.coffee
 delete mode 100644 spec/fixtures/sample-with-tabs.coffee
 create mode 100644 src/flux/attributes/attribute-serverid.coffee
 delete mode 100644 src/flux/models/local-link.coffee

diff --git a/build/Gruntfile.coffee b/build/Gruntfile.coffee
index 61ef5f380..ac1faf873 100644
--- a/build/Gruntfile.coffee
+++ b/build/Gruntfile.coffee
@@ -208,9 +208,9 @@ module.exports = (grunt) ->
         'exports/**/*.coffee'
         'src/**/*.coffee'
         'src/**/*.cjsx'
-        'spec/*.coffee'
-        'spec-nylas/*.cjsx'
-        'spec-nylas/*.coffee'
+        'spec/**/*.coffee'
+        'spec-nylas/**/*.cjsx'
+        'spec-nylas/**/*.coffee'
       ]
 
     coffeelint:
@@ -229,9 +229,13 @@ module.exports = (grunt) ->
         'build/Gruntfile.coffee'
       ]
       test: [
-        'spec/*.coffee'
-        'spec-nylas/*.cjsx'
-        'spec-nylas/*.coffee'
+        'spec/**/*.coffee'
+        'spec-nylas/**/*.cjsx'
+        'spec-nylas/**/*.coffee'
+      ]
+      static: [
+        'static/**/*.coffee'
+        'static/**/*.cjsx'
       ]
       target:
         grunt.option("target")?.split(" ") or []
diff --git a/build/tasks/nylaslint-task.coffee b/build/tasks/nylaslint-task.coffee
index 074851144..7f33baefc 100644
--- a/build/tasks/nylaslint-task.coffee
+++ b/build/tasks/nylaslint-task.coffee
@@ -18,4 +18,4 @@ module.exports = (grunt) ->
           done(new Error("#{f} contains a bad require including an coffee / cjsx / jsx extension. Remove the extension!"))
           return
 
-    done(null)
\ No newline at end of file
+    done(null)
diff --git a/exports/nylas-exports.coffee b/exports/nylas-exports.coffee
index a5c1d7042..df0a7e01b 100644
--- a/exports/nylas-exports.coffee
+++ b/exports/nylas-exports.coffee
@@ -60,7 +60,6 @@ class NylasExports
   @require "Contact", 'flux/models/contact'
   @require "Calendar", 'flux/models/calendar'
   @require "Metadata", 'flux/models/metadata'
-  @require "LocalLink", 'flux/models/local-link'
   @require "DatabaseObjectRegistry", "database-object-registry"
 
   # Exported so 3rd party packages can subclass Model
diff --git a/internal_packages/attachments/lib/attachment-component.cjsx b/internal_packages/attachments/lib/attachment-component.cjsx
index 7417180dc..185113be7 100644
--- a/internal_packages/attachments/lib/attachment-component.cjsx
+++ b/internal_packages/attachments/lib/attachment-component.cjsx
@@ -13,7 +13,7 @@ class AttachmentComponent extends React.Component
     download: React.PropTypes.object
     removable: React.PropTypes.bool
     targetPath: React.PropTypes.string
-    messageLocalId: React.PropTypes.string
+    messageClientId: React.PropTypes.string
 
   constructor: (@props) ->
     @state = progressPercent: 0
@@ -79,7 +79,7 @@ class AttachmentComponent extends React.Component
   _onClickRemove: (event) =>
     Actions.removeFile
       file: @props.file
-      messageLocalId: @props.messageLocalId
+      messageClientId: @props.messageClientId
     event.stopPropagation() # Prevent 'onClickView'
 
   _onClickDownload: (event) =>
diff --git a/internal_packages/composer/lib/composer-view.cjsx b/internal_packages/composer/lib/composer-view.cjsx
index 2042d16db..572629c3f 100644
--- a/internal_packages/composer/lib/composer-view.cjsx
+++ b/internal_packages/composer/lib/composer-view.cjsx
@@ -37,7 +37,7 @@ class ComposerView extends React.Component
   @containerRequired: false
 
   @propTypes:
-    localId: React.PropTypes.string.isRequired
+    draftClientId: React.PropTypes.string.isRequired
 
     # Either "inline" or "fullwindow"
     mode: React.PropTypes.string
@@ -65,10 +65,10 @@ class ComposerView extends React.Component
       showbcc: false
       showsubject: false
       showQuotedText: false
-      uploads: FileUploadStore.uploadsForMessage(@props.localId) ? []
+      uploads: FileUploadStore.uploadsForMessage(@props.draftClientId) ? []
 
   componentWillMount: =>
-    @_prepareForDraft(@props.localId)
+    @_prepareForDraft(@props.draftClientId)
 
   shouldComponentUpdate: (nextProps, nextState) =>
     not Utils.isEqualReact(nextProps, @props) or
@@ -113,24 +113,24 @@ class ComposerView extends React.Component
 
   componentWillReceiveProps: (newProps) =>
     @_ignoreNextTrigger = false
-    if newProps.localId isnt @props.localId
-      # When we're given a new draft localId, we have to stop listening to our
+    if newProps.draftClientId isnt @props.draftClientId
+      # When we're given a new draft draftClientId, we have to stop listening to our
       # current DraftStoreProxy, create a new one and listen to that. The simplest
       # way to do this is to just re-call registerListeners.
       @_teardownForDraft()
-      @_prepareForDraft(newProps.localId)
+      @_prepareForDraft(newProps.draftClientId)
 
-  _prepareForDraft: (localId) =>
+  _prepareForDraft: (draftClientId) =>
     @unlisteners = []
-    return unless localId
+    return unless draftClientId
 
     # UndoManager must be ready before we call _onDraftChanged for the first time
     @undoManager = new UndoManager
-    DraftStore.sessionForLocalId(localId).then(@_setupSession)
+    DraftStore.sessionForClientId(draftClientId).then(@_setupSession)
 
   _setupSession: (proxy) =>
     return if @_unmounted
-    return unless proxy.draftLocalId is @props.localId
+    return unless proxy.draftClientId is @props.draftClientId
     @_proxy = proxy
     @_preloadImages(@_proxy.draft()?.files)
     @unlisteners.push @_proxy.listen(@_onDraftChanged)
@@ -317,12 +317,12 @@ class ComposerView extends React.Component
                               tabIndex="109" />
 
   _renderFooterRegions: =>
-    return <div></div> unless @props.localId
+    return <div></div> unless @props.draftClientId
 
     <div className="composer-footer-region">
       <InjectedComponentSet
         matching={role: "Composer:Footer"}
-        exposedProps={draftLocalId:@props.localId, threadId: @props.threadId}/>
+        exposedProps={draftClientId:@props.draftClientId, threadId: @props.threadId}/>
     </div>
 
   _renderAttachments: ->
@@ -347,7 +347,7 @@ class ComposerView extends React.Component
       file: file
       removable: true
       targetPath: targetPath
-      messageLocalId: @props.localId
+      messageClientId: @props.draftClientId
 
     if role is "Attachment"
       className = "file-wrap"
@@ -397,14 +397,14 @@ class ComposerView extends React.Component
     _.compact(uploads.concat(@state.files))
 
   _onFileUploadStoreChange: =>
-    @setState uploads: FileUploadStore.uploadsForMessage(@props.localId)
+    @setState uploads: FileUploadStore.uploadsForMessage(@props.draftClientId)
 
   _renderActionsRegion: =>
-    return <div></div> unless @props.localId
+    return <div></div> unless @props.draftClientId
 
     <InjectedComponentSet className="composer-action-bar-content"
                       matching={role: "Composer:ActionButton"}
-                      exposedProps={draftLocalId:@props.localId, threadId: @props.threadId}>
+                      exposedProps={draftClientId:@props.draftClientId, threadId: @props.threadId}>
 
       <button className="btn btn-toolbar btn-trash" style={order: 100}
               data-tooltip="Delete draft"
@@ -532,14 +532,14 @@ class ComposerView extends React.Component
   _onDrop: (e) =>
     # Accept drops of real files from other applications
     for file in e.dataTransfer.files
-      Actions.attachFilePath({path: file.path, messageLocalId: @props.localId})
+      Actions.attachFilePath({path: file.path, messageClientId: @props.draftClientId})
 
     # Accept drops from attachment components / images within the app
     if (uri = @_nonNativeFilePathForDrop(e))
-      Actions.attachFilePath({path: uri, messageLocalId: @props.localId})
+      Actions.attachFilePath({path: uri, messageClientId: @props.draftClientId})
 
   _onFilePaste: (path) =>
-    Actions.attachFilePath({path: path, messageLocalId: @props.localId})
+    Actions.attachFilePath({path: path, messageClientId: @props.draftClientId})
 
   _onChangeParticipants: (changes={}) =>
     @_addToProxy(changes)
@@ -589,7 +589,7 @@ class ComposerView extends React.Component
     @_saveToHistory(selections) unless source.fromUndoManager
 
   _popoutComposer: =>
-    Actions.composePopoutDraft @props.localId
+    Actions.composePopoutDraft @props.draftClientId
 
   _sendDraft: (options = {}) =>
     return unless @_proxy
@@ -598,7 +598,7 @@ class ComposerView extends React.Component
     # immediately and synchronously updated as soon as this function
     # fires. Since `setState` is asynchronous, if we used that as our only
     # check, then we might get a false reading.
-    return if DraftStore.isSendingDraft(@props.localId)
+    return if DraftStore.isSendingDraft(@props.draftClientId)
 
     draft = @_proxy.draft()
     remote = require('remote')
@@ -651,17 +651,17 @@ class ComposerView extends React.Component
           @_sendDraft({force: true})
       return
 
-    Actions.sendDraft(@props.localId)
+    Actions.sendDraft(@props.draftClientId)
 
   _mentionsAttachment: (body) =>
     body = QuotedHTMLParser.removeQuotedHTML(body.toLowerCase().trim())
     return body.indexOf("attach") >= 0
 
   _destroyDraft: =>
-    Actions.destroyDraft(@props.localId)
+    Actions.destroyDraft(@props.draftClientId)
 
   _attachFile: =>
-    Actions.attachFile({messageLocalId: @props.localId})
+    Actions.attachFile({messageClientId: @props.draftClientId})
 
   _showAndFocusBcc: =>
     @setState {showbcc: true}
@@ -734,7 +734,7 @@ class ComposerView extends React.Component
 
   _deleteDraftIfEmpty: =>
     return unless @_proxy
-    if @_proxy.draft().pristine then Actions.destroyDraft(@props.localId)
+    if @_proxy.draft().pristine then Actions.destroyDraft(@props.draftClientId)
 
 
 module.exports = ComposerView
diff --git a/internal_packages/composer/lib/main.cjsx b/internal_packages/composer/lib/main.cjsx
index 9e01ddd44..93708af10 100644
--- a/internal_packages/composer/lib/main.cjsx
+++ b/internal_packages/composer/lib/main.cjsx
@@ -28,7 +28,7 @@ class ComposerWithWindowProps extends React.Component
 
   render: ->
     <div className="composer-full-window">
-      <ComposerView mode="fullwindow" localId={@state.draftLocalId} />
+      <ComposerView mode="fullwindow" draftClientId={@state.draftClientId} />
     </div>
 
   _showInitialErrorDialog: (msg) ->
diff --git a/internal_packages/composer/spec/composer-view-spec.cjsx b/internal_packages/composer/spec/composer-view-spec.cjsx
index 7f5352e12..c8f5f167a 100644
--- a/internal_packages/composer/spec/composer-view-spec.cjsx
+++ b/internal_packages/composer/spec/composer-view-spec.cjsx
@@ -30,8 +30,6 @@ u5 = new Contact(name: "Ben Gotow",       email: "ben@nylas.com")
 file = new File(id: 'file_1_id', filename: 'a.png', contentType: 'image/png', size: 10, object: "file")
 
 users = [u1, u2, u3, u4, u5]
-AccountStore._current = new Account(
-  {name: u1.name, provider: "inbox", emailAddress: u1.email})
 
 reactStub = (className) ->
   React.createClass({render: -> <div className={className}>{@props.children}</div>})
@@ -45,11 +43,11 @@ passThroughStub = (props={}) ->
   React.createClass
     render: -> <div {...props}>{props.children}</div>
 
-draftStoreProxyStub = (localId, returnedDraft) ->
+draftStoreProxyStub = (draftClientId, returnedDraft) ->
   listen: -> ->
   draft: -> (returnedDraft ? new Message(draft: true))
   draftPristineBody: -> null
-  draftLocalId: localId
+  draftClientId: draftClientId
   cleanup: ->
   changes:
     add: ->
@@ -72,27 +70,21 @@ ComposerView = proxyquire "../lib/composer-view",
     DraftStore: DraftStore
 
 beforeEach ->
-  # spyOn(ComponentRegistry, "findComponentsMatching").andCallFake (matching) ->
-  #   return passThroughStub
-  # spyOn(ComponentRegistry, "showComponentRegions").andReturn true
-
   # The AccountStore isn't set yet in the new window, populate it first.
   AccountStore.populateItems().then ->
-    new Promise (resolve, reject) ->
-      draft = new Message
-        from: [AccountStore.current().me()]
-        date: (new Date)
-        draft: true
-        accountId: AccountStore.current().id
+    draft = new Message
+      from: [AccountStore.current().me()]
+      date: (new Date)
+      draft: true
+      accountId: AccountStore.current().id
 
-      DatabaseStore.persistModel(draft).then ->
-        DatabaseStore.localIdForModel(draft).then(resolve).catch(reject)
-      .catch(reject)
+    DatabaseStore.persistModel(draft).then ->
+      return draft
 
 describe "A blank composer view", ->
   beforeEach ->
     @composer = ReactTestUtils.renderIntoDocument(
-      <ComposerView localId="test123" />
+      <ComposerView draftClientId="test123" />
     )
     @composer.setState
       body: ""
@@ -110,23 +102,23 @@ describe "A blank composer view", ->
 # This will setup the mocks necessary to make the composer element (once
 # mounted) think it's attached to the given draft. This mocks out the
 # proxy system used by the composer.
-DRAFT_LOCAL_ID = "local-123"
+DRAFT_CLIENT_ID = "local-123"
 useDraft = (draftAttributes={}) ->
   @draft = new Message _.extend({draft: true, body: ""}, draftAttributes)
   draft = @draft
-  proxy = draftStoreProxyStub(DRAFT_LOCAL_ID, @draft)
+  proxy = draftStoreProxyStub(DRAFT_CLIENT_ID, @draft)
 
 
   spyOn(ComposerView.prototype, "componentWillMount").andCallFake ->
-    @_prepareForDraft(DRAFT_LOCAL_ID)
+    @_prepareForDraft(DRAFT_CLIENT_ID)
     @_setupSession(proxy)
 
-  # Normally when sessionForLocalId resolves, it will call `_setupSession`
+  # Normally when sessionForClientId resolves, it will call `_setupSession`
   # and pass the new session proxy. However, in our faked
-  # `componentWillMount`, we manually call sessionForLocalId to make this
+  # `componentWillMount`, we manually call sessionForClientId to make this
   # part of the test synchronous. We need to make the `then` block of the
-  # sessionForLocalId do nothing so `_setupSession` is not called twice!
-  spyOn(DraftStore, "sessionForLocalId").andCallFake ->
+  # sessionForClientId do nothing so `_setupSession` is not called twice!
+  spyOn(DraftStore, "sessionForClientId").andCallFake ->
     then: ->
 
 useFullDraft = ->
@@ -141,7 +133,7 @@ useFullDraft = ->
 
 makeComposer = ->
   @composer = ReactTestUtils.renderIntoDocument(
-    <ComposerView localId={DRAFT_LOCAL_ID} />
+    <ComposerView draftClientId={DRAFT_CLIENT_ID} />
   )
 
 describe "populated composer", ->
@@ -245,9 +237,9 @@ describe "populated composer", ->
     describe "if the draft has not yet loaded", ->
       it "should set _focusOnUpdate and focus after the next render", ->
         @draft = new Message(draft: true, body: "")
-        proxy = draftStoreProxyStub(DRAFT_LOCAL_ID, @draft)
+        proxy = draftStoreProxyStub(DRAFT_CLIENT_ID, @draft)
         proxyResolve = null
-        spyOn(DraftStore, "sessionForLocalId").andCallFake ->
+        spyOn(DraftStore, "sessionForClientId").andCallFake ->
           new Promise (resolve, reject) ->
             proxyResolve = resolve
 
@@ -462,7 +454,7 @@ describe "populated composer", ->
       useFullDraft.apply(@); makeComposer.call(@)
       sendBtn = React.findDOMNode(@composer.refs.sendButton)
       ReactTestUtils.Simulate.click sendBtn
-      expect(Actions.sendDraft).toHaveBeenCalledWith(DRAFT_LOCAL_ID)
+      expect(Actions.sendDraft).toHaveBeenCalledWith(DRAFT_CLIENT_ID)
       expect(Actions.sendDraft.calls.length).toBe 1
 
     it "doesn't send twice if you double click", ->
@@ -472,7 +464,7 @@ describe "populated composer", ->
       @isSending.state = true
       DraftStore.trigger()
       ReactTestUtils.Simulate.click sendBtn
-      expect(Actions.sendDraft).toHaveBeenCalledWith(DRAFT_LOCAL_ID)
+      expect(Actions.sendDraft).toHaveBeenCalledWith(DRAFT_CLIENT_ID)
       expect(Actions.sendDraft.calls.length).toBe 1
 
     describe "when sending a message with keyboard inputs", ->
@@ -630,14 +622,14 @@ describe "populated composer", ->
 
       @up1 =
         uploadTaskId: 4
-        messageLocalId: DRAFT_LOCAL_ID
+        messageClientId: DRAFT_CLIENT_ID
         filePath: "/foo/bar/f4.bmp"
         fileName: "f4.bmp"
         fileSize: 1024
 
       @up2 =
         uploadTaskId: 5
-        messageLocalId: DRAFT_LOCAL_ID
+        messageClientId: DRAFT_CLIENT_ID
         filePath: "/foo/bar/f5.zip"
         fileName: "f5.zip"
         fileSize: 1024
diff --git a/internal_packages/composer/spec/participants-text-field-spec.cjsx b/internal_packages/composer/spec/participants-text-field-spec.cjsx
index d41ec7f7d..2ce364bfe 100644
--- a/internal_packages/composer/spec/participants-text-field-spec.cjsx
+++ b/internal_packages/composer/spec/participants-text-field-spec.cjsx
@@ -14,17 +14,22 @@ ParticipantsTextField = proxyquire '../lib/participants-text-field',
   'nylas-exports': {Contact, ContactStore}
 
 participant1 = new Contact
+  id: 'local-1'
   email: 'ben@nylas.com'
 participant2 = new Contact
+  id: 'local-2'
   email: 'ben@example.com'
   name: 'Ben Gotow'
 participant3 = new Contact
+  id: 'local-3'
   email: 'evan@nylas.com'
   name: 'Evan Morikawa'
 participant4 = new Contact
+  id: 'local-4',
   email: 'ben@elsewhere.com',
   name: 'ben Again'
 participant5 = new Contact
+  id: 'local-5',
   email: 'evan@elsewhere.com',
   name: 'EVAN'
 
@@ -54,7 +59,7 @@ describe 'ParticipantsTextField', ->
       ReactTestUtils.Simulate.keyDown(@renderedInput, {key: 'Enter', keyCode: 9})
 
       reviver = (k,v) ->
-        return undefined if k in ["id", "object"]
+        return undefined if k in ["id", "client_id", "server_id", "object"]
         return v
       found = @propChange.mostRecentCall.args[0]
       found = JSON.parse(JSON.stringify(found), reviver)
@@ -102,7 +107,7 @@ describe 'ParticipantsTextField', ->
 
     describe "when text contains Name (Email) formatted data", ->
       it "should correctly parse it into named Contact objects", ->
-        newContact1 = new Contact(name:'Ben Imposter', email:'imposter@nylas.com')
+        newContact1 = new Contact(id: "b1", name:'Ben Imposter', email:'imposter@nylas.com')
         newContact2 = new Contact(name:'Nylas Team', email:'feedback@nylas.com')
 
         inputs = [
@@ -120,8 +125,8 @@ describe 'ParticipantsTextField', ->
 
     describe "when text contains emails mixed with garbage text", ->
       it "should still parse out emails into Contact objects", ->
-        newContact1 = new Contact(name:'garbage-man@nylas.com', email:'garbage-man@nylas.com')
-        newContact2 = new Contact(name:'recycling-guy@nylas.com', email:'recycling-guy@nylas.com')
+        newContact1 = new Contact(id: 'gm', name:'garbage-man@nylas.com', email:'garbage-man@nylas.com')
+        newContact2 = new Contact(id: 'rm', name:'recycling-guy@nylas.com', email:'recycling-guy@nylas.com')
 
         inputs = [
           "Hello world I real. \n asd. garbage-man@nylas.com—he's cool Also 'recycling-guy@nylas.com'!",
diff --git a/internal_packages/events/lib/main.cjsx b/internal_packages/events/lib/main.cjsx
index 681572d80..e82bcc661 100644
--- a/internal_packages/events/lib/main.cjsx
+++ b/internal_packages/events/lib/main.cjsx
@@ -10,4 +10,4 @@ module.exports =
   deactivate: ->
     ComponentRegistry.unregister EventComponent
 
-  serialize: -> @state
\ No newline at end of file
+  serialize: -> @state
diff --git a/internal_packages/message-list/lib/message-controls.cjsx b/internal_packages/message-list/lib/message-controls.cjsx
index a390dcc89..687c8812b 100644
--- a/internal_packages/message-list/lib/message-controls.cjsx
+++ b/internal_packages/message-list/lib/message-controls.cjsx
@@ -100,16 +100,15 @@ class MessageControls extends React.Component
       body: @props.message.body
 
     DatabaseStore.persistModel(draft).then =>
-      DatabaseStore.localIdForModel(draft).then (localId) =>
-        Actions.sendDraft(localId)
+      Actions.sendDraft(draft.clientId)
 
-        dialog = remote.require('dialog')
-        dialog.showMessageBox remote.getCurrentWindow(), {
-          type: 'warning'
-          buttons: ['OK'],
-          message: "Thank you."
-          detail: "The contents of this message have been sent to the Edgehill team and we added to a test suite."
-        }
+      dialog = remote.require('dialog')
+      dialog.showMessageBox remote.getCurrentWindow(), {
+        type: 'warning'
+        buttons: ['OK'],
+        message: "Thank you."
+        detail: "The contents of this message have been sent to the Edgehill team and we added to a test suite."
+      }
 
   _onShowOriginal: =>
     fs = require 'fs'
diff --git a/internal_packages/message-list/lib/message-item-container.cjsx b/internal_packages/message-list/lib/message-item-container.cjsx
index 8fa9ae9a5..b6eb65089 100644
--- a/internal_packages/message-list/lib/message-item-container.cjsx
+++ b/internal_packages/message-list/lib/message-item-container.cjsx
@@ -15,11 +15,6 @@ class MessageItemContainer extends React.Component
   @propTypes =
     thread: React.PropTypes.object.isRequired
     message: React.PropTypes.object.isRequired
-
-    # The localId (in the case of draft's local ID) is a derived
-    # property that only the parent MessageList knows about.
-    localId: React.PropTypes.string
-
     collapsed: React.PropTypes.bool
     isLastMsg: React.PropTypes.bool
     isBeforeReplyArea: React.PropTypes.bool
@@ -65,7 +60,7 @@ class MessageItemContainer extends React.Component
   _renderComposer: =>
     props =
       mode: "inline"
-      localId: @props.localId
+      draftClientId: @props.message.clientId
       threadId: @props.thread.id
       onRequestScrollTo: @props.onRequestScrollTo
 
@@ -81,10 +76,10 @@ class MessageItemContainer extends React.Component
     "message-item-wrap": true
     "before-reply-area": @props.isBeforeReplyArea
 
-  _onSendingStateChanged: (draftLocalId) =>
-    @setState(@_getStateFromStores()) if draftLocalId is @props.localId
+  _onSendingStateChanged: (draftClientId) =>
+    @setState(@_getStateFromStores()) if draftClientId is @props.message.clientId
 
   _getStateFromStores: ->
-    isSending: DraftStore.isSendingDraft(@props.localId)
+    isSending: DraftStore.isSendingDraft(@props.message.clientId)
 
 module.exports = MessageItemContainer
diff --git a/internal_packages/message-list/lib/message-list.cjsx b/internal_packages/message-list/lib/message-list.cjsx
index 1ed8f2d50..f5cc5c0f5 100755
--- a/internal_packages/message-list/lib/message-list.cjsx
+++ b/internal_packages/message-list/lib/message-list.cjsx
@@ -128,8 +128,8 @@ class MessageList extends React.Component
     newDraftIds = _.map(_.filter((@state.messages ? []), (m) -> m.draft), (m) -> m.id)
     return _.difference(newDraftIds, oldDraftIds) ? []
 
-  _getMessageContainer: (id) =>
-    @refs["message-container-#{id}"]
+  _getMessageContainer: (messageId) =>
+    @refs["message-container-#{messageId}"]
 
   _focusDraft: (draftElement) =>
     # Note: We don't want the contenteditable view competing for scroll offset,
@@ -155,7 +155,7 @@ class MessageList extends React.Component
     # in reply to and the draft session and change the participants.
     if last.draft is true
       data =
-        session: DraftStore.sessionForLocalId(@state.messageLocalIds[last.id])
+        session: DraftStore.sessionForClientId(last.clientId)
         replyToMessage: Promise.resolve(@state.messages[@state.messages.length - 2])
         type: type
 
@@ -299,14 +299,11 @@ class MessageList extends React.Component
       isLastMsg = (messages.length - 1 is idx)
       isBeforeReplyArea = isLastMsg and hasReplyArea
 
-      localId = @state.messageLocalIds[message.id]
-
       elements.push(
         <MessageItemContainer key={idx}
                               ref={"message-container-#{message.id}"}
                               thread={@state.currentThread}
                               message={message}
-                              localId={localId}
                               collapsed={collapsed}
                               isLastMsg={isLastMsg}
                               isBeforeReplyArea={isBeforeReplyArea}
@@ -404,7 +401,6 @@ class MessageList extends React.Component
 
   _getStateFromStores: =>
     messages: (MessageStore.items() ? [])
-    messageLocalIds: MessageStore.itemLocalIds()
     messagesExpandedState: MessageStore.itemsExpandedState()
     currentThread: MessageStore.thread()
     loading: MessageStore.itemsLoading()
diff --git a/internal_packages/message-list/spec/message-item-container-spec.cjsx b/internal_packages/message-list/spec/message-item-container-spec.cjsx
index c1d9782bd..aadf634b8 100644
--- a/internal_packages/message-list/spec/message-item-container-spec.cjsx
+++ b/internal_packages/message-list/spec/message-item-container-spec.cjsx
@@ -16,7 +16,7 @@ MessageItemContainer = proxyquire '../lib/message-item-container',
 {InjectedComponent} = require 'nylas-component-kit'
 
 testThread = new Thread(id: "t1")
-testLocalId = "local-id"
+testClientId = "local-id"
 testMessage = new Message(id: "m1", draft: false, unread: true)
 testDraft = new Message(id: "d1", draft: true, unread: true)
 
@@ -30,7 +30,7 @@ describe 'MessageItemContainer', ->
     ReactTestUtils.renderIntoDocument(
       <MessageItemContainer thread={testThread}
                             message={message}
-                            localId={testLocalId} />
+                            draftClientId={testClientId} />
     )
 
   it "shows composer if it's a draft", ->
diff --git a/internal_packages/message-list/spec/message-item-spec.cjsx b/internal_packages/message-list/spec/message-item-spec.cjsx
index e273dd7a7..842f5228c 100644
--- a/internal_packages/message-list/spec/message-item-spec.cjsx
+++ b/internal_packages/message-list/spec/message-item-spec.cjsx
@@ -114,7 +114,7 @@ describe "MessageItem", ->
       snippet: "snippet one..."
       subject: "Subject One"
       threadId: "thread_12345"
-      accountId: "test_account_id"
+      accountId: TEST_ACCOUNT_ID
 
     @thread = new Thread
       id: 'thread-111'
diff --git a/internal_packages/message-list/spec/message-list-spec.cjsx b/internal_packages/message-list/spec/message-list-spec.cjsx
index add7f44e8..82fb24606 100644
--- a/internal_packages/message-list/spec/message-list-spec.cjsx
+++ b/internal_packages/message-list/spec/message-list-spec.cjsx
@@ -33,12 +33,6 @@ MessageItemContainer = proxyquire("../lib/message-item-container", {
 MessageList = proxyquire '../lib/message-list',
   "./message-item-container": MessageItemContainer
 
-me = new Account
-  name: "User One",
-  emailAddress: "user1@nylas.com"
-  provider: "inbox"
-AccountStore._current = me
-
 user_1 = new Contact
   name: "User One"
   email: "user1@nylas.com"
@@ -70,7 +64,7 @@ m1 = (new Message).fromJSON({
   "snippet"   : "snippet one...",
   "subject"   : "Subject One",
   "thread_id" : "thread_12345",
-  "account_id" : "test_account_id"
+  "account_id" : TEST_ACCOUNT_ID
 })
 m2 = (new Message).fromJSON({
   "id"   : "222",
@@ -87,7 +81,7 @@ m2 = (new Message).fromJSON({
   "snippet"   : "snippet Two...",
   "subject"   : "Subject Two",
   "thread_id" : "thread_12345",
-  "account_id" : "test_account_id"
+  "account_id" : TEST_ACCOUNT_ID
 })
 m3 = (new Message).fromJSON({
   "id"   : "333",
@@ -104,7 +98,7 @@ m3 = (new Message).fromJSON({
   "snippet"   : "snippet Three...",
   "subject"   : "Subject Three",
   "thread_id" : "thread_12345",
-  "account_id" : "test_account_id"
+  "account_id" : TEST_ACCOUNT_ID
 })
 m4 = (new Message).fromJSON({
   "id"   : "444",
@@ -121,7 +115,7 @@ m4 = (new Message).fromJSON({
   "snippet"   : "snippet Four...",
   "subject"   : "Subject Four",
   "thread_id" : "thread_12345",
-  "account_id" : "test_account_id"
+  "account_id" : TEST_ACCOUNT_ID
 })
 m5 = (new Message).fromJSON({
   "id"   : "555",
@@ -138,7 +132,7 @@ m5 = (new Message).fromJSON({
   "snippet"   : "snippet Five...",
   "subject"   : "Subject Five",
   "thread_id" : "thread_12345",
-  "account_id" : "test_account_id"
+  "account_id" : TEST_ACCOUNT_ID
 })
 testMessages = [m1, m2, m3, m4, m5]
 draftMessages = [
@@ -157,11 +151,12 @@ draftMessages = [
     "snippet"   : "draft snippet one...",
     "subject"   : "Draft One",
     "thread_id" : "thread_12345",
-    "account_id" : "test_account_id"
+    "account_id" : TEST_ACCOUNT_ID
   }),
 ]
 
 test_thread = (new Thread).fromJSON({
+  "id": "12345"
   "id" : "thread_12345"
   "subject" : "Subject 12345"
 })
@@ -170,8 +165,6 @@ describe "MessageList", ->
   beforeEach ->
     MessageStore._items = []
     MessageStore._threadId = null
-    spyOn(MessageStore, "itemLocalIds").andCallFake ->
-      {"666": "666"}
     spyOn(MessageStore, "itemsLoading").andCallFake ->
       false
 
@@ -224,7 +217,7 @@ describe "MessageList", ->
         messages: msgs.concat(draftMessages)
 
       expect(@messageList._focusDraft).toHaveBeenCalled()
-      expect(@messageList._focusDraft.mostRecentCall.args[0].props.localId).toEqual(draftMessages[0].id)
+      expect(@messageList._focusDraft.mostRecentCall.args[0].props.draftClientId).toEqual(draftMessages[0].draftClientId)
 
     it "includes drafts as message item containers", ->
       msgs = @messageList.state.messages
@@ -306,7 +299,7 @@ describe "MessageList", ->
           draft: => @draft
           changes:
             add: jasmine.createSpy('session.changes.add')
-        spyOn(DraftStore, 'sessionForLocalId').andCallFake =>
+        spyOn(DraftStore, 'sessionForClientId').andCallFake =>
           Promise.resolve(@sessionStub)
 
       it "should not fire a composer action", ->
diff --git a/internal_packages/message-list/spec/message-timestamp-spec.cjsx b/internal_packages/message-list/spec/message-timestamp-spec.cjsx
index 9dbc948c7..e61f18fe9 100644
--- a/internal_packages/message-list/spec/message-timestamp-spec.cjsx
+++ b/internal_packages/message-list/spec/message-timestamp-spec.cjsx
@@ -38,4 +38,4 @@ describe "MessageTimestamp", ->
 
   it "displays month, day, and year for messages over a year ago", ->
     now = msgTime().add(2, 'years')
-    expect(@item._formattedDate(msgTime(), now)).toBe "Feb 14, 2010"
\ No newline at end of file
+    expect(@item._formattedDate(msgTime(), now)).toBe "Feb 14, 2010"
diff --git a/internal_packages/message-templates/lib/template-picker.cjsx b/internal_packages/message-templates/lib/template-picker.cjsx
index da035fbd1..57659e10b 100644
--- a/internal_packages/message-templates/lib/template-picker.cjsx
+++ b/internal_packages/message-templates/lib/template-picker.cjsx
@@ -74,14 +74,14 @@ class TemplatePicker extends React.Component
       templates: @_filteredTemplates(newSearch)
 
   _onChooseTemplate: (template) =>
-    Actions.insertTemplateId({templateId:template.id, draftLocalId: @props.draftLocalId})
+    Actions.insertTemplateId({templateId:template.id, draftClientId: @props.draftClientId})
     @refs.popover.close()
 
   _onManageTemplates: =>
     Actions.showTemplates()
 
   _onNewTemplate: =>
-    Actions.createTemplate({draftLocalId: @props.draftLocalId})
+    Actions.createTemplate({draftClientId: @props.draftClientId})
 
 
 module.exports = TemplatePicker
diff --git a/internal_packages/message-templates/lib/template-status-bar.cjsx b/internal_packages/message-templates/lib/template-status-bar.cjsx
index ba505ecf0..e903ca546 100644
--- a/internal_packages/message-templates/lib/template-status-bar.cjsx
+++ b/internal_packages/message-templates/lib/template-status-bar.cjsx
@@ -11,15 +11,15 @@ class TemplateStatusBar extends React.Component
     margin:'auto'
 
   @propTypes:
-    draftLocalId: React.PropTypes.string
+    draftClientId: React.PropTypes.string
 
   constructor: (@props) ->
     @state = draft: null
 
   componentDidMount: =>
-    DraftStore.sessionForLocalId(@props.draftLocalId).then (_proxy) =>
+    DraftStore.sessionForClientId(@props.draftClientId).then (_proxy) =>
       return if @_unmounted
-      return unless _proxy.draftLocalId is @props.draftLocalId
+      return unless _proxy.draftClientId is @props.draftClientId
       @_proxy = _proxy
       @unsubscribe = @_proxy.listen(@_onDraftChange, @)
       @_onDraftChange()
diff --git a/internal_packages/message-templates/lib/template-store.coffee b/internal_packages/message-templates/lib/template-store.coffee
index 886abd9e7..7431784f3 100644
--- a/internal_packages/message-templates/lib/template-store.coffee
+++ b/internal_packages/message-templates/lib/template-store.coffee
@@ -54,9 +54,9 @@ TemplateStore = Reflux.createStore
           path: path.join(@_templatesDir, filename)
       @trigger(@)
 
-  _onCreateTemplate: ({draftLocalId, name, contents} = {}) ->
-    if draftLocalId
-      DraftStore.sessionForLocalId(draftLocalId).then (session) =>
+  _onCreateTemplate: ({draftClientId, name, contents} = {}) ->
+    if draftClientId
+      DraftStore.sessionForClientId(draftClientId).then (session) =>
         draft = session.draft()
         name ?= draft.subject
         contents ?= draft.body
@@ -92,13 +92,13 @@ TemplateStore = Reflux.createStore
         path: templatePath
       @trigger(@)
 
-  _onInsertTemplateId: ({templateId, draftLocalId} = {}) ->
+  _onInsertTemplateId: ({templateId, draftClientId} = {}) ->
     template = _.find @_items, (item) -> item.id is templateId
     return unless template
 
     fs.readFile template.path, (err, data) ->
       body = data.toString()
-      DraftStore.sessionForLocalId(draftLocalId).then (session) ->
+      DraftStore.sessionForClientId(draftClientId).then (session) ->
         session.changes.add(body: body)
 
 module.exports = TemplateStore
diff --git a/internal_packages/message-templates/spec/template-store-spec.coffee b/internal_packages/message-templates/spec/template-store-spec.coffee
index c7770981c..7b02a25bc 100644
--- a/internal_packages/message-templates/spec/template-store-spec.coffee
+++ b/internal_packages/message-templates/spec/template-store-spec.coffee
@@ -59,13 +59,13 @@ describe "TemplateStore", ->
     it "should insert the template with the given id into the draft with the given id", ->
 
       add = jasmine.createSpy('add')
-      spyOn(DraftStore, 'sessionForLocalId').andCallFake ->
+      spyOn(DraftStore, 'sessionForClientId').andCallFake ->
         Promise.resolve(changes: {add})
 
       runs ->
         TemplateStore._onInsertTemplateId
           templateId: 'template1.html',
-          draftLocalId: 'localid-draft'
+          draftClientId: 'localid-draft'
       waitsFor ->
         add.calls.length > 0
       runs ->
@@ -75,8 +75,8 @@ describe "TemplateStore", ->
   describe "onCreateTemplate", ->
     beforeEach ->
       TemplateStore.init()
-      spyOn(DraftStore, 'sessionForLocalId').andCallFake (draftLocalId) ->
-        if draftLocalId is 'localid-nosubject'
+      spyOn(DraftStore, 'sessionForClientId').andCallFake (draftClientId) ->
+        if draftClientId is 'localid-nosubject'
           d = new Message(subject: '', body: '<p>Body</p>')
         else
           d = new Message(subject: 'Subject', body: '<p>Body</p>')
@@ -118,7 +118,7 @@ describe "TemplateStore", ->
         spyOn(TemplateStore, 'trigger')
         spyOn(TemplateStore, '_populate')
         runs ->
-          TemplateStore._onCreateTemplate({draftLocalId: 'localid-b'})
+          TemplateStore._onCreateTemplate({draftClientId: 'localid-b'})
         waitsFor ->
           TemplateStore.trigger.callCount > 0
         runs ->
@@ -127,7 +127,7 @@ describe "TemplateStore", ->
       it "should display an error if the draft has no subject", ->
         spyOn(TemplateStore, '_displayError')
         runs ->
-          TemplateStore._onCreateTemplate({draftLocalId: 'localid-nosubject'})
+          TemplateStore._onCreateTemplate({draftClientId: 'localid-nosubject'})
         waitsFor ->
           TemplateStore._displayError.callCount > 0
         runs ->
diff --git a/internal_packages/thread-list/lib/draft-list-store.coffee b/internal_packages/thread-list/lib/draft-list-store.coffee
index aadf7b315..6edabfe82 100644
--- a/internal_packages/thread-list/lib/draft-list-store.coffee
+++ b/internal_packages/thread-list/lib/draft-list-store.coffee
@@ -51,11 +51,6 @@ DraftListStore = Reflux.createStore
     selected = @_view.selection.items()
 
     for item in selected
-      DatabaseStore.localIdForModel(item).then (localId) =>
-        Actions.queueTask(new DestroyDraftTask(draftLocalId: localId))
-        # if thread.id is focusedId
-        #   Actions.setFocus(collection: 'thread', item: null)
-        # if thread.id is keyboardId
-        #   Actions.setCursorPosition(collection: 'thread', item: null)
+      Actions.queueTask(new DestroyDraftTask(draftClientId: item.clientId))
 
     @_view.selection.clear()
diff --git a/internal_packages/thread-list/lib/draft-list.cjsx b/internal_packages/thread-list/lib/draft-list.cjsx
index 5a9492a8e..3698f97b4 100644
--- a/internal_packages/thread-list/lib/draft-list.cjsx
+++ b/internal_packages/thread-list/lib/draft-list.cjsx
@@ -64,16 +64,14 @@ class DraftList extends React.Component
       collection="draft" />
 
   _onDoubleClick: (item) =>
-    DatabaseStore.localIdForModel(item).then (localId) ->
-      Actions.composePopoutDraft(localId)
+    Actions.composePopoutDraft(item.clientId)
 
   # Additional Commands
 
   _onDelete: ({focusedId}) =>
     item = DraftListStore.view().getById(focusedId)
     return unless item
-    DatabaseStore.localIdForModel(item).then (localId) ->
-      Actions.destroyDraft(localId)
+    Actions.destroyDraft(item.clientId)
 
 
 module.exports = DraftList
diff --git a/internal_packages/thread-list/lib/thread-list-icon.cjsx b/internal_packages/thread-list/lib/thread-list-icon.cjsx
index db9bf517e..953909f83 100644
--- a/internal_packages/thread-list/lib/thread-list-icon.cjsx
+++ b/internal_packages/thread-list/lib/thread-list-icon.cjsx
@@ -35,7 +35,7 @@ class ThreadListIcon extends React.Component
   _nonDraftMessages: =>
     msgs = @props.thread.metadata
     return [] unless msgs and msgs instanceof Array
-    msgs = _.filter msgs, (m) -> m.isSaved() and not m.draft
+    msgs = _.filter msgs, (m) -> m.serverId and not m.draft
     return msgs
 
   shouldComponentUpdate: (nextProps) =>
diff --git a/internal_packages/thread-list/spec/thread-list-spec.cjsx b/internal_packages/thread-list/spec/thread-list-spec.cjsx
index 63341cb7b..00f0bb498 100644
--- a/internal_packages/thread-list/spec/thread-list-spec.cjsx
+++ b/internal_packages/thread-list/spec/thread-list-spec.cjsx
@@ -34,20 +34,13 @@ ThreadList = require "../lib/thread-list"
 ParticipantsItem = React.createClass
   render: -> <div></div>
 
-me = new Account(
-  "name": "User One",
-  "email": "user1@nylas.com"
-  "provider": "inbox"
-)
-AccountStore._current = me
-
 test_threads = -> [
   (new Thread).fromJSON({
     "id": "111",
     "object": "thread",
     "created_at": null,
     "updated_at": null,
-    "account_id": "test_account_id",
+    "account_id": TEST_ACCOUNT_ID,
     "snippet": "snippet 111",
     "subject": "Subject 111",
     "tags": [
@@ -103,7 +96,7 @@ test_threads = -> [
     "object": "thread",
     "created_at": null,
     "updated_at": null,
-    "account_id": "test_account_id",
+    "account_id": TEST_ACCOUNT_ID,
     "snippet": "snippet 222",
     "subject": "Subject 222",
     "tags": [
@@ -153,7 +146,7 @@ test_threads = -> [
     "object": "thread",
     "created_at": null,
     "updated_at": null,
-    "account_id": "test_account_id",
+    "account_id": TEST_ACCOUNT_ID,
     "snippet": "snippet 333",
     "subject": "Subject 333",
     "tags": [
diff --git a/internal_packages/undo-redo/lib/main.cjsx b/internal_packages/undo-redo/lib/main.cjsx
index c450c6fe0..2440b708d 100644
--- a/internal_packages/undo-redo/lib/main.cjsx
+++ b/internal_packages/undo-redo/lib/main.cjsx
@@ -10,4 +10,4 @@ module.exports =
   deactivate: ->
     ComponentRegistry.unregister UndoRedoComponent
 
-  serialize: -> @state
\ No newline at end of file
+  serialize: -> @state
diff --git a/internal_packages/worker-sync/spec/nylas-sync-worker-spec.coffee b/internal_packages/worker-sync/spec/nylas-sync-worker-spec.coffee
index 94c07c967..4476e5885 100644
--- a/internal_packages/worker-sync/spec/nylas-sync-worker-spec.coffee
+++ b/internal_packages/worker-sync/spec/nylas-sync-worker-spec.coffee
@@ -18,7 +18,7 @@ describe "NylasSyncWorker", ->
 
     spyOn(DatabaseStore, 'persistJSONObject').andReturn(Promise.resolve())
     spyOn(DatabaseStore, 'findJSONObject').andCallFake (key) =>
-      expected = "NylasSyncWorker:account-id"
+      expected = "NylasSyncWorker:#{TEST_ACCOUNT_ID}"
       return throw new Error("Not stubbed! #{key}") unless key is expected
       Promise.resolve _.extend {}, {
         "contacts":
@@ -29,7 +29,7 @@ describe "NylasSyncWorker", ->
           complete: true
       }
 
-    @account = new Account(id: 'account-id', organizationUnit: 'label')
+    @account = new Account(clientId: TEST_ACCOUNT_CLIENT_ID, serverId: TEST_ACCOUNT_ID, organizationUnit: 'label')
     @worker = new NylasSyncWorker(@api, @account)
     @connection = @worker.connection()
     advanceClock()
diff --git a/internal_packages/worker-ui/lib/developer-bar-store.coffee b/internal_packages/worker-ui/lib/developer-bar-store.coffee
index 40f8472ce..5f8a5ceb8 100644
--- a/internal_packages/worker-ui/lib/developer-bar-store.coffee
+++ b/internal_packages/worker-ui/lib/developer-bar-store.coffee
@@ -140,7 +140,6 @@ DeveloperBarStore = Reflux.createStore
         #{debugData}
       """
     DatabaseStore.persistModel(draft).then ->
-      DatabaseStore.localIdForModel(draft).then (localId) ->
-        Actions.composePopoutDraft(localId)
+      Actions.composePopoutDraft(draft.clientId)
 
 module.exports = DeveloperBarStore
diff --git a/spec-nylas/action-bridge-spec.coffee b/spec-nylas/action-bridge-spec.coffee
index 025d280c2..186f3e967 100644
--- a/spec-nylas/action-bridge-spec.coffee
+++ b/spec-nylas/action-bridge-spec.coffee
@@ -51,7 +51,7 @@ describe "ActionBridge", ->
       @bridge = new ActionBridge(ipc)
       @message = new Message
         id: 'test-id'
-        accountId: 'test-account-id'
+        accountId: TEST_ACCOUNT_ID
 
     it "should have the role Role.SECONDARY", ->
       expect(@bridge.role).toBe(ActionBridge.Role.SECONDARY)
@@ -83,19 +83,19 @@ describe "ActionBridge", ->
     describe "when called with TargetWindows.ALL", ->
       it "should broadcast the action over IPC to all windows", ->
         spyOn(ipc, 'send')
-        Actions.didSwapModel.firing = false
-        @bridge.onRebroadcast(ActionBridge.TargetWindows.ALL, 'didSwapModel', [{oldModel: '1', newModel: 2}])
-        expect(ipc.send).toHaveBeenCalledWith('action-bridge-rebroadcast-to-all', 'popout', 'didSwapModel', '[{"oldModel":"1","newModel":2}]')
+        Actions.logout.firing = false
+        @bridge.onRebroadcast(ActionBridge.TargetWindows.ALL, 'logout', [{oldModel: '1', newModel: 2}])
+        expect(ipc.send).toHaveBeenCalledWith('action-bridge-rebroadcast-to-all', 'popout', 'logout', '[{"oldModel":"1","newModel":2}]')
 
     describe "when called with TargetWindows.WORK", ->
       it "should broadcast the action over IPC to the main window only", ->
         spyOn(ipc, 'send')
-        Actions.didSwapModel.firing = false
-        @bridge.onRebroadcast(ActionBridge.TargetWindows.WORK, 'didSwapModel', [{oldModel: '1', newModel: 2}])
-        expect(ipc.send).toHaveBeenCalledWith('action-bridge-rebroadcast-to-work', 'popout', 'didSwapModel', '[{"oldModel":"1","newModel":2}]')
+        Actions.logout.firing = false
+        @bridge.onRebroadcast(ActionBridge.TargetWindows.WORK, 'logout', [{oldModel: '1', newModel: 2}])
+        expect(ipc.send).toHaveBeenCalledWith('action-bridge-rebroadcast-to-work', 'popout', 'logout', '[{"oldModel":"1","newModel":2}]')
 
     it "should not do anything if the current invocation of the Action was triggered by itself", ->
       spyOn(ipc, 'send')
-      Actions.didSwapModel.firing = true
-      @bridge.onRebroadcast(ActionBridge.TargetWindows.ALL, 'didSwapModel', [{oldModel: '1', newModel: 2}])
+      Actions.logout.firing = true
+      @bridge.onRebroadcast(ActionBridge.TargetWindows.ALL, 'logout', [{oldModel: '1', newModel: 2}])
       expect(ipc.send).not.toHaveBeenCalled()
diff --git a/spec-nylas/components/tokenizing-text-field-spec.cjsx b/spec-nylas/components/tokenizing-text-field-spec.cjsx
index da33172b7..c4f20198f 100644
--- a/spec-nylas/components/tokenizing-text-field-spec.cjsx
+++ b/spec-nylas/components/tokenizing-text-field-spec.cjsx
@@ -9,12 +9,6 @@ ReactTestUtils = React.addons.TestUtils
 } = require 'nylas-exports'
 {TokenizingTextField, Menu} = require 'nylas-component-kit'
 
-me = new Account
-  name: 'Test User'
-  email: 'test@example.com'
-  provider: 'inbox'
-AccountStore._current = me
-
 CustomToken = React.createClass
   render: ->
     <span>{@props.item.email}</span>
@@ -125,6 +119,8 @@ describe 'TokenizingTextField', ->
 
   describe "when the user drags and drops a token between two fields", ->
     it "should work properly", ->
+      participant2.clientId = '123'
+
       tokensA = [participant1, participant2, participant3]
       fieldA = @rebuildRenderedField(tokensA)
 
@@ -142,7 +138,7 @@ describe 'TokenizingTextField', ->
       ReactTestUtils.Simulate.dragStart(token, dragStartEvent)
 
       expect(dragStartEventData).toEqual({
-        'nylas-token-item': '{"id":"2","name":"Nylas Burger Basket","email":"burgers@nylas.com","__constructorName":"Contact"}'
+        'nylas-token-item': '{"client_id":"123","server_id":"2","name":"Nylas Burger Basket","email":"burgers@nylas.com","id":"2","__constructorName":"Contact"}'
         'text/plain': 'Nylas Burger Basket <burgers@nylas.com>'
       })
 
diff --git a/spec-nylas/fixtures/db-test-model.coffee b/spec-nylas/fixtures/db-test-model.coffee
index e3cd6b9db..9e0ff2b8d 100644
--- a/spec-nylas/fixtures/db-test-model.coffee
+++ b/spec-nylas/fixtures/db-test-model.coffee
@@ -8,12 +8,30 @@ class TestModel extends Model
       queryable: true
       modelKey: 'id'
 
+    'clientId': Attributes.String
+      queryable: true
+      modelKey: 'clientId'
+      jsonKey: 'client_id'
+
+    'serverId': Attributes.ServerId
+      queryable: true
+      modelKey: 'serverId'
+      jsonKey: 'server_id'
+
 TestModel.configureBasic = ->
   TestModel.additionalSQLiteConfig = undefined
   TestModel.attributes =
     'id': Attributes.String
       queryable: true
       modelKey: 'id'
+    'clientId': Attributes.String
+      queryable: true
+      modelKey: 'clientId'
+      jsonKey: 'client_id'
+    'serverId': Attributes.ServerId
+      queryable: true
+      modelKey: 'serverId'
+      jsonKey: 'server_id'
 
 TestModel.configureWithAllAttributes = ->
   TestModel.additionalSQLiteConfig = undefined
@@ -40,6 +58,14 @@ TestModel.configureWithCollectionAttribute = ->
     'id': Attributes.String
       queryable: true
       modelKey: 'id'
+    'clientId': Attributes.String
+      queryable: true
+      modelKey: 'clientId'
+      jsonKey: 'client_id'
+    'serverId': Attributes.ServerId
+      queryable: true
+      modelKey: 'serverId'
+      jsonKey: 'server_id'
     'labels': Attributes.Collection
       queryable: true
       modelKey: 'labels'
@@ -52,6 +78,14 @@ TestModel.configureWithJoinedDataAttribute = ->
     'id': Attributes.String
       queryable: true
       modelKey: 'id'
+    'clientId': Attributes.String
+      queryable: true
+      modelKey: 'clientId'
+      jsonKey: 'client_id'
+    'serverId': Attributes.ServerId
+      queryable: true
+      modelKey: 'serverId'
+      jsonKey: 'server_id'
     'body': Attributes.JoinedData
       modelTable: 'TestModelBody'
       modelKey: 'body'
@@ -62,6 +96,12 @@ TestModel.configureWithAdditionalSQLiteConfig = ->
     'id': Attributes.String
       queryable: true
       modelKey: 'id'
+    'clientId': Attributes.String
+      modelKey: 'clientId'
+      jsonKey: 'client_id'
+    'serverId': Attributes.ServerId
+      modelKey: 'serverId'
+      jsonKey: 'server_id'
     'body': Attributes.JoinedData
       modelTable: 'TestModelBody'
       modelKey: 'body'
diff --git a/spec-nylas/models/model-spec.coffee b/spec-nylas/models/model-spec.coffee
index 0c0d1f1db..541feb7ab 100644
--- a/spec-nylas/models/model-spec.coffee
+++ b/spec-nylas/models/model-spec.coffee
@@ -1,4 +1,5 @@
 Model = require '../../src/flux/models/model'
+Utils = require '../../src/flux/models/utils'
 Attributes = require '../../src/flux/attributes'
 {isTempId} = require '../../src/flux/models/utils'
 _ = require 'underscore'
@@ -13,22 +14,43 @@ describe "Model", ->
       expect(m.id).toBe(attrs.id)
       expect(m.accountId).toBe(attrs.accountId)
 
-    it "should assign a local- ID to the model if no ID is provided", ->
+    it "by default assigns things passed into the id constructor to the serverId", ->
+      attrs =
+        id: "A",
+      m = new Model(attrs)
+      expect(m.serverId).toBe(attrs.id)
+
+    it "by default assigns values passed into the id constructor that look like localIds to be a localID", ->
+      attrs =
+        id: "A",
+      m = new Model(attrs)
+      expect(m.serverId).toBe(attrs.id)
+
+    it "assigns serverIds and clientIds", ->
+      attrs =
+        clientId: "local-A",
+        serverId: "A",
+      m = new Model(attrs)
+      expect(m.serverId).toBe(attrs.serverId)
+      expect(m.clientId).toBe(attrs.clientId)
+      expect(m.id).toBe(attrs.serverId)
+
+    it "throws an error if you attempt to manually assign the id", ->
+      m = new Model(id: "foo")
+      expect( -> m.id = "bar" ).toThrow()
+
+    it "automatically assigns a clientId (and id) to the model if no id is provided", ->
       m = new Model
-      expect(isTempId(m.id)).toBe(true)
+      expect(Utils.isTempId(m.id)).toBe true
+      expect(Utils.isTempId(m.clientId)).toBe true
+      expect(m.serverId).toBeUndefined()
 
   describe "attributes", ->
-    it "should return the attributes of the class", ->
+    it "should return the attributes of the class EXCEPT the id field", ->
       m = new Model()
-      expect(m.attributes()).toBe(m.constructor.attributes)
-
-  describe "isSaved", ->
-    it "should return false if the object has a temp ID", ->
-      a = new Model()
-      expect(a.isSaved()).toBe(false)
-
-      b = new Model({id: "b"})
-      expect(b.isSaved()).toBe(true)
+      retAttrs = _.clone(m.constructor.attributes)
+      delete retAttrs["id"]
+      expect(m.attributes()).toEqual(retAttrs)
 
   describe "clone", ->
     it "should return a deep copy of the object", ->
diff --git a/spec-nylas/models/thread-spec.coffee b/spec-nylas/models/thread-spec.coffee
index 966017b95..b3b88fc06 100644
--- a/spec-nylas/models/thread-spec.coffee
+++ b/spec-nylas/models/thread-spec.coffee
@@ -1,4 +1,3 @@
-{generateTempId} = require '../../src/flux/models/utils'
 Message = require '../../src/flux/models/message'
 Thread = require '../../src/flux/models/thread'
 _ = require 'underscore'
diff --git a/spec-nylas/stores/contact-store-spec.coffee b/spec-nylas/stores/contact-store-spec.coffee
index 30da09d5e..f11ff835c 100644
--- a/spec-nylas/stores/contact-store-spec.coffee
+++ b/spec-nylas/stores/contact-store-spec.coffee
@@ -11,8 +11,6 @@ describe "ContactStore", ->
     ContactStore._contactCache = []
     ContactStore._fetchOffset = 0
     ContactStore._accountId = null
-    AccountStore._current =
-      id: "test_account_id"
 
   afterEach ->
     atom.testOrganizationUnit = null
@@ -36,9 +34,7 @@ describe "ContactStore", ->
       spyOn(ContactStore, "_refreshCache")
       ContactStore._contactCache = [1,2,3]
       ContactStore._fetchOffset = 3
-      ContactStore._accountId = "test_account_id"
-      AccountStore._current =
-        id: "test_account_id"
+      ContactStore._accountId = TEST_ACCOUNT_ID
       AccountStore.trigger()
       expect(ContactStore._contactCache).toEqual [1,2,3]
       expect(ContactStore._fetchOffset).toBe 3
diff --git a/spec-nylas/stores/database-setup-query-builder-spec.coffee b/spec-nylas/stores/database-setup-query-builder-spec.coffee
index f3e24ced8..da2adbe74 100644
--- a/spec-nylas/stores/database-setup-query-builder-spec.coffee
+++ b/spec-nylas/stores/database-setup-query-builder-spec.coffee
@@ -30,7 +30,7 @@ describe "DatabaseSetupQueryBuilder", ->
       TestModel.configureWithCollectionAttribute()
       queries = @builder.setupQueriesForTable(TestModel)
       expected = [
-        'CREATE TABLE IF NOT EXISTS `TestModel` (id TEXT PRIMARY KEY,data BLOB)',
+        'CREATE TABLE IF NOT EXISTS `TestModel` (id TEXT PRIMARY KEY,data BLOB,client_id TEXT,server_id TEXT)',
         'CREATE UNIQUE INDEX IF NOT EXISTS `TestModel_id` ON `TestModel` (`id`)',
         'CREATE TABLE IF NOT EXISTS `TestModel-Label` (id TEXT KEY, `value` TEXT)'
         'CREATE UNIQUE INDEX IF NOT EXISTS `TestModel_Label_id_val` ON `TestModel-Label` (`id`,`value`)',
diff --git a/spec-nylas/stores/database-store-spec.coffee b/spec-nylas/stores/database-store-spec.coffee
index c57c7544e..f9c420834 100644
--- a/spec-nylas/stores/database-store-spec.coffee
+++ b/spec-nylas/stores/database-store-spec.coffee
@@ -7,9 +7,9 @@ ModelQuery = require '../../src/flux/models/query'
 DatabaseStore = require '../../src/flux/stores/database-store'
 
 testMatchers = {'id': 'b'}
-testModelInstance = new TestModel(id: '1234')
-testModelInstanceA = new TestModel(id: 'AAA')
-testModelInstanceB = new TestModel(id: 'BBB')
+testModelInstance = new TestModel(id: "1234")
+testModelInstanceA = new TestModel(id: "AAA")
+testModelInstanceB = new TestModel(id: "BBB")
 
 describe "DatabaseStore", ->
   beforeEach ->
@@ -146,7 +146,7 @@ describe "DatabaseStore", ->
     it "should compose a REPLACE INTO query to save the model", ->
       TestModel.configureWithCollectionAttribute()
       DatabaseStore._writeModels([testModelInstance])
-      expect(@performed[0].query).toBe("REPLACE INTO `TestModel` (id,data) VALUES (?,?)")
+      expect(@performed[0].query).toBe("REPLACE INTO `TestModel` (id,data,client_id,server_id) VALUES (?,?,?,?)")
 
     it "should save the model JSON into the data column", ->
       DatabaseStore._writeModels([testModelInstance])
@@ -234,9 +234,9 @@ describe "DatabaseStore", ->
         TestModel.configureWithJoinedDataAttribute()
 
       it "should not include the value to the joined attribute in the JSON written to the main model table", ->
-        @m = new TestModel(id: 'local-6806434c-b0cd', body: 'hello world')
+        @m = new TestModel(clientId: 'local-6806434c-b0cd', serverId: 'server-1', body: 'hello world')
         DatabaseStore._writeModels([@m])
-        expect(@performed[0].values).toEqual(['local-6806434c-b0cd', '{"id":"local-6806434c-b0cd"}'])
+        expect(@performed[0].values).toEqual(['server-1', '{"client_id":"local-6806434c-b0cd","server_id":"server-1","id":"server-1"}', 'local-6806434c-b0cd', 'server-1'])
 
       it "should write the value to the joined table if it is defined", ->
         @m = new TestModel(id: 'local-6806434c-b0cd', body: 'hello world')
@@ -244,7 +244,7 @@ describe "DatabaseStore", ->
         expect(@performed[1].query).toBe('REPLACE INTO `TestModelBody` (`id`, `value`) VALUES (?, ?)')
         expect(@performed[1].values).toEqual([@m.id, @m.body])
 
-      it "should not write the valeu to the joined table if it undefined", ->
+      it "should not write the value to the joined table if it undefined", ->
         @m = new TestModel(id: 'local-6806434c-b0cd')
         DatabaseStore._writeModels([@m])
         expect(@performed.length).toBe(1)
diff --git a/spec-nylas/stores/draft-store-spec.coffee b/spec-nylas/stores/draft-store-spec.coffee
index 038966948..fd0b2d110 100644
--- a/spec-nylas/stores/draft-store-spec.coffee
+++ b/spec-nylas/stores/draft-store-spec.coffee
@@ -107,7 +107,6 @@ describe "DraftStore", ->
         return Promise.resolve(fakeMessage2) if query._klass is Message
         return Promise.reject(new Error('Not Stubbed'))
       spyOn(DatabaseStore, 'persistModel').andCallFake -> Promise.resolve()
-      spyOn(DatabaseStore, 'bindToLocalId')
 
     afterEach ->
       # Have to cleanup the DraftStoreProxy objects or we'll get a memory
@@ -277,21 +276,13 @@ describe "DraftStore", ->
         , (model) ->
           expect(model.constructor).toBe(Message)
 
-      it "should assign and save a local Id for the new message", ->
+      it "should setup a draft session for the draftClientId, so that a subsequent request for the session's draft resolves immediately.", ->
         @_callNewMessageWithContext {threadId: fakeThread.id}
         , (thread, message) ->
           {}
         , (model) ->
-          expect(DatabaseStore.bindToLocalId).toHaveBeenCalled()
-
-      it "should setup a draft session for the draftLocalId, so that a subsequent request for the session's draft resolves immediately.", ->
-        @_callNewMessageWithContext {threadId: fakeThread.id}
-        , (thread, message) ->
-          {}
-        , (model) ->
-          [draft, localId] = DatabaseStore.bindToLocalId.mostRecentCall.args
-          session = DraftStore.sessionForLocalId(localId).value()
-          expect(session.draft()).toBe(draft)
+          session = DraftStore.sessionForClientId(model.id).value()
+          expect(session.draft()).toBe(model)
 
       it "should set the subject of the new message automatically", ->
         @_callNewMessageWithContext {threadId: fakeThread.id}
@@ -559,7 +550,7 @@ describe "DraftStore", ->
         expect(DraftStore._onBeforeUnload()).toBe(true)
 
   describe "sending a draft", ->
-    draftLocalId = "local-123"
+    draftClientId = "local-123"
     beforeEach ->
       DraftStore._draftSessions = {}
       DraftStore._draftsSending = {}
@@ -569,7 +560,7 @@ describe "DraftStore", ->
         draft: -> {}
         changes:
           commit: -> Promise.resolve()
-      DraftStore._draftSessions[draftLocalId] = proxy
+      DraftStore._draftSessions[draftClientId] = proxy
       spyOn(DraftStore, "_doneWithSession").andCallThrough()
       spyOn(DraftStore, "trigger")
 
@@ -577,23 +568,23 @@ describe "DraftStore", ->
       spyOn(atom, "isMainWindow").andReturn true
       spyOn(Actions, "queueTask").andCallThrough()
       runs ->
-        DraftStore._onSendDraft(draftLocalId)
+        DraftStore._onSendDraft(draftClientId)
       waitsFor ->
         Actions.queueTask.calls.length > 0
       runs ->
-        expect(DraftStore.isSendingDraft(draftLocalId)).toBe true
+        expect(DraftStore.isSendingDraft(draftClientId)).toBe true
         expect(DraftStore.trigger).toHaveBeenCalled()
 
     it "returns false if the draft hasn't been seen", ->
       spyOn(atom, "isMainWindow").andReturn true
-      expect(DraftStore.isSendingDraft(draftLocalId)).toBe false
+      expect(DraftStore.isSendingDraft(draftClientId)).toBe false
 
     it "closes the window if it's a popout", ->
       spyOn(atom, "getWindowType").andReturn "composer"
       spyOn(atom, "isMainWindow").andReturn false
       spyOn(atom, "close")
       runs ->
-        DraftStore._onSendDraft(draftLocalId)
+        DraftStore._onSendDraft(draftClientId)
       waitsFor "Atom to close", ->
         atom.close.calls.length > 0
 
@@ -603,7 +594,7 @@ describe "DraftStore", ->
       spyOn(atom, "close")
       spyOn(DraftStore, "_isPopout").andCallThrough()
       runs ->
-        DraftStore._onSendDraft(draftLocalId)
+        DraftStore._onSendDraft(draftClientId)
       waitsFor ->
         DraftStore._isPopout.calls.length > 0
       runs ->
@@ -612,14 +603,14 @@ describe "DraftStore", ->
     it "queues a SendDraftTask", ->
       spyOn(Actions, "queueTask")
       runs ->
-        DraftStore._onSendDraft(draftLocalId)
+        DraftStore._onSendDraft(draftClientId)
       waitsFor ->
         DraftStore._doneWithSession.calls.length > 0
       runs ->
         expect(Actions.queueTask).toHaveBeenCalled()
         task = Actions.queueTask.calls[0].args[0]
         expect(task instanceof SendDraftTask).toBe true
-        expect(task.draftLocalId).toBe draftLocalId
+        expect(task.draftClientId).toBe draftClientId
         expect(task.fromPopout).toBe false
 
     it "queues a SendDraftTask with popout info", ->
@@ -628,7 +619,7 @@ describe "DraftStore", ->
       spyOn(atom, "close")
       spyOn(Actions, "queueTask")
       runs ->
-        DraftStore._onSendDraft(draftLocalId)
+        DraftStore._onSendDraft(draftClientId)
       waitsFor ->
         DraftStore._doneWithSession.calls.length > 0
       runs ->
@@ -640,7 +631,7 @@ describe "DraftStore", ->
     beforeEach ->
       @draftTeardown = jasmine.createSpy('draft teardown')
       @session =
-        draftLocalId: "abc"
+        draftClientId: "abc"
         draft: ->
           pristine: false
         changes:
@@ -675,7 +666,7 @@ describe "DraftStore", ->
       received = null
       spyOn(DraftStore, '_finalizeAndPersistNewMessage').andCallFake (draft) ->
         received = draft
-        Promise.resolve({draftLocalId: 123})
+        Promise.resolve({draftClientId: 123})
 
       expected = "EmailSubjectLOLOL"
       DraftStore._onHandleMailtoLink('mailto:asdf@asdf.com?subject=' + expected)
diff --git a/spec-nylas/stores/event-store-spec.coffee b/spec-nylas/stores/event-store-spec.coffee
index baec0ed9c..dc877994b 100644
--- a/spec-nylas/stores/event-store-spec.coffee
+++ b/spec-nylas/stores/event-store-spec.coffee
@@ -10,8 +10,6 @@ describe "EventStore", ->
     atom.testOrganizationUnit = "folder"
     EventStore._eventCache = {}
     EventStore._accountId = null
-    AccountStore._current =
-      id: "test_account_id"
 
   afterEach ->
     atom.testOrganizationUnit = null
@@ -36,29 +34,27 @@ describe "EventStore", ->
     it "does nothing", ->
       spyOn(EventStore, "_refreshCache")
       EventStore._eventCache = {1: '', 2: '', 3: ''}
-      EventStore._accountId = "test_account_id"
-      AccountStore._current =
-        id: "test_account_id"
+      EventStore._accountId = TEST_ACCOUNT_ID
       AccountStore.trigger()
       expect(EventStore._eventCache).toEqual {1: '', 2: '', 3: ''}
       expect(EventStore._refreshCache).not.toHaveBeenCalled()
 
   describe "getEvent", ->
     beforeEach ->
-      @e1 = new Event(id: 1, title:'Test1', start: '', end: '', location: '', participants: [{"name":"Guy", "email":"tester@nylas.com", "status":"noreply"}])
-      @e2 = new Event(id: 2, title:'Test2', start: '', end: '', location: '', participants: [{"name":"Guy", "email":"tester@nylas.com", "status":"noreply"}])
-      @e3 = new Event(id: 3, title:'Test3', start: '', end: '', location: '', participants: [{"name":"Guy", "email":"tester@nylas.com", "status":"noreply"}])
-      @e4 = new Event(id: 4, title:'Test4', start: '', end: '', location: '', participants: [{"name":"Guy", "email":"tester@nylas.com", "status":"noreply"}])
+      @e1 = new Event(id: 'a', title:'Test1', start: '', end: '', location: '', participants: [{"name":"Guy", "email":"tester@nylas.com", "status":"noreply"}])
+      @e2 = new Event(id: 'b', title:'Test2', start: '', end: '', location: '', participants: [{"name":"Guy", "email":"tester@nylas.com", "status":"noreply"}])
+      @e3 = new Event(id: 'c', title:'Test3', start: '', end: '', location: '', participants: [{"name":"Guy", "email":"tester@nylas.com", "status":"noreply"}])
+      @e4 = new Event(id: 'd', title:'Test4', start: '', end: '', location: '', participants: [{"name":"Guy", "email":"tester@nylas.com", "status":"noreply"}])
       EventStore._eventCache = {}
       for e in [@e1, @e2, @e3, @e4]
         EventStore._eventCache[e.id] = e
 
     it "returns event object based on id", ->
-      first = EventStore.getEvent(1)
+      first = EventStore.getEvent('a')
       expect(first.title).toBe 'Test1'
-      second = EventStore.getEvent(2)
+      second = EventStore.getEvent('b')
       expect(second.title).toBe 'Test2'
-      third = EventStore.getEvent(3)
+      third = EventStore.getEvent('c')
       expect(third.title).toBe 'Test3'
-      fourth = EventStore.getEvent(4)
+      fourth = EventStore.getEvent('d')
       expect(fourth.title).toBe 'Test4'
diff --git a/spec-nylas/stores/file-download-store-spec.coffee b/spec-nylas/stores/file-download-store-spec.coffee
index 148f17dea..115ce8cfc 100644
--- a/spec-nylas/stores/file-download-store-spec.coffee
+++ b/spec-nylas/stores/file-download-store-spec.coffee
@@ -41,7 +41,7 @@ describe "FileDownloadStore", ->
   beforeEach ->
     spyOn(shell, 'showItemInFolder')
     spyOn(shell, 'openItem')
-    @testfile = new File(filename: '123.png', contentType: 'image/png', id: 'id', size: 100)
+    @testfile = new File(filename: '123.png', contentType: 'image/png', id: "id", size: 100)
     FileDownloadStore._downloads = {}
     FileDownloadStore._downloadDirectory = "/Users/testuser/.nylas/downloads"
 
@@ -60,7 +60,7 @@ describe "FileDownloadStore", ->
 
   describe "_checkForDownloadedFile", ->
     it "should return true if the file exists at the path and is the right size", ->
-      f = new File(filename: '123.png', contentType: 'image/png', id: 'id', size: 100)
+      f = new File(filename: '123.png', contentType: 'image/png', id: "id", size: 100)
       spyOn(fs, 'stat').andCallFake (path, callback) ->
         callback(null, {size: 100})
       waitsForPromise ->
@@ -68,7 +68,7 @@ describe "FileDownloadStore", ->
           expect(downloaded).toBe(true)
 
     it "should return false if the file does not exist", ->
-      f = new File(filename: '123.png', contentType: 'image/png', id: 'id', size: 100)
+      f = new File(filename: '123.png', contentType: 'image/png', id: "id", size: 100)
       spyOn(fs, 'stat').andCallFake (path, callback) ->
         callback(new Error("File does not exist"))
       waitsForPromise ->
@@ -76,7 +76,7 @@ describe "FileDownloadStore", ->
           expect(downloaded).toBe(false)
 
     it "should return false if the file is too small", ->
-      f = new File(filename: '123.png', contentType: 'image/png', id: 'id', size: 100)
+      f = new File(filename: '123.png', contentType: 'image/png', id: "id", size: 100)
       spyOn(fs, 'stat').andCallFake (path, callback) ->
         callback(null, {size: 50})
       waitsForPromise ->
diff --git a/spec-nylas/stores/file-upload-store-spec.coffee b/spec-nylas/stores/file-upload-store-spec.coffee
index 195563ec6..b64ab83d3 100644
--- a/spec-nylas/stores/file-upload-store-spec.coffee
+++ b/spec-nylas/stores/file-upload-store-spec.coffee
@@ -14,7 +14,7 @@ describe 'FileUploadStore', ->
       size: 12345
     @uploadData =
       uploadTaskId: 123
-      messageLocalId: msgId
+      messageClientId: msgId
       filePath: fpath
       fileSize: 12345
 
@@ -29,11 +29,11 @@ describe 'FileUploadStore', ->
 
     it "throws if the message id is blank", ->
       spyOn(Actions, "attachFilePath")
-      Actions.attachFile messageLocalId: msgId
+      Actions.attachFile messageClientId: msgId
       expect(atom.showOpenDialog).toHaveBeenCalled()
       expect(Actions.attachFilePath).toHaveBeenCalled()
       args = Actions.attachFilePath.calls[0].args[0]
-      expect(args.messageLocalId).toBe msgId
+      expect(args.messageClientId).toBe msgId
       expect(args.path).toBe fpath
 
   describe 'attachFilePath', ->
@@ -44,19 +44,19 @@ describe 'FileUploadStore', ->
       spyOn(fs, 'stat').andCallFake (path, callback) ->
         callback(null, {isDirectory: -> false})
       Actions.attachFilePath
-        messageLocalId: msgId
+        messageClientId: msgId
         path: fpath
       expect(Actions.queueTask).toHaveBeenCalled()
       t = Actions.queueTask.calls[0].args[0]
       expect(t.filePath).toBe fpath
-      expect(t.messageLocalId).toBe msgId
+      expect(t.messageClientId).toBe msgId
 
     it 'displays an error if the file path given is a directory', ->
       spyOn(FileUploadStore, '_onAttachFileError')
       spyOn(fs, 'stat').andCallFake (path, callback) ->
         callback(null, {isDirectory: -> true})
       Actions.attachFilePath
-        messageLocalId: msgId
+        messageClientId: msgId
         path: fpath
       expect(Actions.queueTask).not.toHaveBeenCalled()
       expect(FileUploadStore._onAttachFileError).toHaveBeenCalled()
diff --git a/spec-nylas/stores/focused-category-store-spec.coffee b/spec-nylas/stores/focused-category-store-spec.coffee
index 7509fc95c..75e4ba31f 100644
--- a/spec-nylas/stores/focused-category-store-spec.coffee
+++ b/spec-nylas/stores/focused-category-store-spec.coffee
@@ -24,7 +24,7 @@ describe "FocusedCategoryStore", ->
 
       it "should set the current category to Inbox when the current category no longer exists in the CategoryStore", ->
         otherAccountInbox = @inboxCategory.clone()
-        otherAccountInbox.id = 'other-id'
+        otherAccountInbox.serverId = 'other-id'
         FocusedCategoryStore._category = otherAccountInbox
         FocusedCategoryStore._onCategoryStoreChanged()
         expect(FocusedCategoryStore.category().id).toEqual(@inboxCategory.id)
diff --git a/spec-nylas/stores/focused-contacts-store-spec.coffee b/spec-nylas/stores/focused-contacts-store-spec.coffee
index 7925ff5ac..b5715277c 100644
--- a/spec-nylas/stores/focused-contacts-store-spec.coffee
+++ b/spec-nylas/stores/focused-contacts-store-spec.coffee
@@ -1,17 +1,7 @@
 proxyquire = require 'proxyquire'
 Reflux = require 'reflux'
 
-MessageStoreStub = Reflux.createStore
-  items: -> []
-  extensions: -> []
-  threadId: -> null
-
-AccountStoreStub = Reflux.createStore
-  current: -> null
-
-FocusedContactsStore = proxyquire '../../src/flux/stores/focused-contacts-store',
-  "./message-store": MessageStoreStub
-  "./account-store": AccountStoreStub
+FocusedContactsStore = require '../../src/flux/stores/focused-contacts-store'
 
 describe "FocusedContactsStore", ->
   beforeEach ->
diff --git a/spec-nylas/stores/task-queue-spec.coffee b/spec-nylas/stores/task-queue-spec.coffee
index 31df1df96..bcb9c19e7 100644
--- a/spec-nylas/stores/task-queue-spec.coffee
+++ b/spec-nylas/stores/task-queue-spec.coffee
@@ -2,8 +2,6 @@ Actions = require '../../src/flux/actions'
 TaskQueue = require '../../src/flux/stores/task-queue'
 Task = require '../../src/flux/tasks/task'
 
-{isTempId} = require '../../src/flux/models/utils'
-
 {APIError,
  OfflineError,
  TimeoutError} = require '../../src/flux/errors'
diff --git a/spec-nylas/stores/unread-count-store-spec.coffee b/spec-nylas/stores/unread-count-store-spec.coffee
index 9686bbe23..28ae0ab4c 100644
--- a/spec-nylas/stores/unread-count-store-spec.coffee
+++ b/spec-nylas/stores/unread-count-store-spec.coffee
@@ -19,7 +19,7 @@ describe "UnreadCountStore", ->
       atom.testOrganizationUnit = 'folder'
       UnreadCountStore._fetchCount()
       advanceClock()
-      expect(DatabaseStore.findBy).toHaveBeenCalledWith(Folder, {name: 'inbox', accountId: 'test_account_id'})
+      expect(DatabaseStore.findBy).toHaveBeenCalledWith(Folder, {name: 'inbox', accountId: TEST_ACCOUNT_ID})
 
       [Model, Matchers] = DatabaseStore.count.calls[0].args
       expect(Model).toBe(Thread)
@@ -33,7 +33,7 @@ describe "UnreadCountStore", ->
       atom.testOrganizationUnit = 'label'
       UnreadCountStore._fetchCount()
       advanceClock()
-      expect(DatabaseStore.findBy).toHaveBeenCalledWith(Label, {name: 'inbox', accountId: 'test_account_id'})
+      expect(DatabaseStore.findBy).toHaveBeenCalledWith(Label, {name: 'inbox', accountId: TEST_ACCOUNT_ID})
 
       [Model, Matchers] = DatabaseStore.count.calls[0].args
       expect(Matchers[0].attr.modelKey).toBe('accountId')
diff --git a/spec-nylas/task-spec.coffee b/spec-nylas/task-spec.coffee
deleted file mode 100644
index 25da34441..000000000
--- a/spec-nylas/task-spec.coffee
+++ /dev/null
@@ -1,44 +0,0 @@
-Task = require '../src/task'
-
-xdescribe "Task", ->
-  describe "@once(taskPath, args..., callback)", ->
-    it "terminates the process after it completes", ->
-      handlerResult = null
-      task = Task.once require.resolve('./fixtures/task-spec-handler'), (result) ->
-        handlerResult = result
-
-      processErrored = false
-      childProcess = task.childProcess
-      spyOn(childProcess, 'kill').andCallThrough()
-      task.childProcess.on 'error', -> processErrored = true
-
-      waitsFor ->
-        handlerResult?
-
-      runs ->
-        expect(handlerResult).toBe 'hello'
-        expect(childProcess.kill).toHaveBeenCalled()
-        expect(processErrored).toBe false
-
-  it "calls listeners registered with ::on when events are emitted in the task", ->
-    task = new Task(require.resolve('./fixtures/task-spec-handler'))
-
-    eventSpy = jasmine.createSpy('eventSpy')
-    task.on("some-event", eventSpy)
-
-    waitsFor (done) -> task.start(done)
-
-    runs ->
-      expect(eventSpy).toHaveBeenCalledWith(1, 2, 3)
-
-  it "unregisters listeners when the Disposable returned by ::on is disposed", ->
-    task = new Task(require.resolve('./fixtures/task-spec-handler'))
-
-    eventSpy = jasmine.createSpy('eventSpy')
-    disposable = task.on("some-event", eventSpy)
-    disposable.dispose()
-
-    waitsFor (done) -> task.start(done)
-
-    runs ->
-      expect(eventSpy).not.toHaveBeenCalled()
diff --git a/spec-nylas/tasks/event-rsvp-spec.coffee b/spec-nylas/tasks/event-rsvp-spec.coffee
index 3a06bfe8c..9b5f49cc6 100644
--- a/spec-nylas/tasks/event-rsvp-spec.coffee
+++ b/spec-nylas/tasks/event-rsvp-spec.coffee
@@ -15,7 +15,7 @@ describe "EventRSVPTask", ->
     @myEmail = "tester@nylas.com"
     @event = new Event
       id: '12233AEDF5'
-      accountId: 'test_account_id'
+      accountId: TEST_ACCOUNT_ID
       title: 'Meeting with Ben Bitdiddle'
       description: ''
       location: ''
diff --git a/spec-nylas/tasks/file-upload-task-spec.coffee b/spec-nylas/tasks/file-upload-task-spec.coffee
index 0e032c50a..20fcbc874 100644
--- a/spec-nylas/tasks/file-upload-task-spec.coffee
+++ b/spec-nylas/tasks/file-upload-task-spec.coffee
@@ -26,7 +26,7 @@ test_file_paths = [
 
 noop = ->
 
-localId = "local-id_1234"
+messageClientId = "local-id_1234"
 
 fake_draft = new Message
   id: "draft-id_1234"
@@ -52,13 +52,13 @@ describe "FileUploadTask", ->
 
     @uploadData =
       startDate: DATE
-      messageLocalId: localId
+      messageClientId: messageClientId
       filePath: test_file_paths[0]
       fileSize: 1234
       fileName: "file.txt"
     bytesUploaded: 0
 
-    @task = new FileUploadTask(test_file_paths[0], localId)
+    @task = new FileUploadTask(test_file_paths[0], messageClientId)
 
     @req = jasmine.createSpyObj('req', ['abort'])
     @simulateRequestSuccessImmediately = false
@@ -82,13 +82,13 @@ describe "FileUploadTask", ->
       (new FileUploadTask).performLocal().catch (err) ->
         expect(err instanceof Error).toBe true
 
-  it "rejects if not initialized with a messageLocalId", ->
+  it "rejects if not initialized with a messageClientId", ->
     waitsForPromise ->
       (new FileUploadTask(test_file_paths[0])).performLocal().catch (err) ->
         expect(err instanceof Error).toBe true
 
   it 'initializes the upload start', ->
-    task = new FileUploadTask(test_file_paths[0], localId)
+    task = new FileUploadTask(test_file_paths[0], messageClientId)
     expect(task._startDate).toBe DATE
 
   it "notifies when the task locally starts", ->
@@ -159,7 +159,7 @@ describe "FileUploadTask", ->
       @simulateRequestSuccessImmediately = true
 
       spyOn(Actions, "uploadStateChanged")
-      spyOn(DraftStore, "sessionForLocalId").andCallFake =>
+      spyOn(DraftStore, "sessionForClientId").andCallFake =>
         Promise.resolve(
           draft: => files: @testFiles
           changes:
@@ -178,12 +178,14 @@ describe "FileUploadTask", ->
       waitsForPromise => @task.performRemote().then ->
         options = NylasAPI.makeRequest.mostRecentCall.args[0]
         expect(options.path).toBe("/files")
-        expect(options.accountId).toBe("test_account_id")
+        expect(options.accountId).toBe(TEST_ACCOUNT_ID)
         expect(options.method).toBe('POST')
         expect(options.formData.file.value).toBe("Read Stream")
 
     it "attaches the file to the draft", ->
       waitsForPromise => @task.performRemote().then =>
+        delete @changes[0].clientId
+        delete equivalentFile.clientId
         expect(@changes).toEqual [equivalentFile]
 
     describe "file upload notifications", ->
@@ -201,15 +203,17 @@ describe "FileUploadTask", ->
           bytesUploaded: 1000
 
         [{file, uploadData}] = Actions.fileUploaded.calls[0].args
+        delete file.clientId
+        delete equivalentFile.clientId
         expect(file).toEqual(equivalentFile)
         expect(_.isMatch(uploadData, uploadDataExpected)).toBe(true)
 
     describe "when attaching a lot of files", ->
       it "attaches them all to the draft", ->
-        t1 = new FileUploadTask("1.a", localId)
-        t2 = new FileUploadTask("2.b", localId)
-        t3 = new FileUploadTask("3.c", localId)
-        t4 = new FileUploadTask("4.d", localId)
+        t1 = new FileUploadTask("1.a", messageClientId)
+        t2 = new FileUploadTask("2.b", messageClientId)
+        t3 = new FileUploadTask("3.c", messageClientId)
+        t4 = new FileUploadTask("4.d", messageClientId)
 
         @simulateRequestSuccessImmediately = true
         waitsForPromise => Promise.all([
diff --git a/spec-nylas/tasks/send-draft-spec.coffee b/spec-nylas/tasks/send-draft-spec.coffee
index 31de624eb..180ab0e37 100644
--- a/spec-nylas/tasks/send-draft-spec.coffee
+++ b/spec-nylas/tasks/send-draft-spec.coffee
@@ -3,7 +3,6 @@ Actions = require '../../src/flux/actions'
 SyncbackDraftTask = require '../../src/flux/tasks/syncback-draft'
 SendDraftTask = require '../../src/flux/tasks/send-draft'
 DatabaseStore = require '../../src/flux/stores/database-store'
-{generateTempId} = require '../../src/flux/models/utils'
 {APIError} = require '../../src/flux/errors'
 Message = require '../../src/flux/models/message'
 TaskQueue = require '../../src/flux/stores/task-queue'
@@ -39,7 +38,7 @@ describe "SendDraftTask", ->
       expect(@sendA.shouldWaitForTask(@saveA)).toBe(true)
 
   describe "performLocal", ->
-    it "should throw an exception if the first parameter is not a localId", ->
+    it "should throw an exception if the first parameter is not a clientId", ->
       badTasks = [new SendDraftTask()]
       goodTasks = [new SendDraftTask('localid-a')]
       caught = []
@@ -60,8 +59,10 @@ describe "SendDraftTask", ->
 
   describe "performRemote", ->
     beforeEach ->
+      @draftClientId = "local-123"
       @draft = new Message
         version: '1'
+        clientId: @draftClientId
         id: '1233123AEDF1'
         accountId: 'A12ADE'
         subject: 'New Draft'
@@ -70,12 +71,13 @@ describe "SendDraftTask", ->
         to:
           name: 'Dummy'
           email: 'dummy@nylas.com'
-      @draftLocalId = "local-123"
-      @task = new SendDraftTask(@draftLocalId)
+      @task = new SendDraftTask(@draftClientId)
       spyOn(NylasAPI, 'makeRequest').andCallFake (options) =>
         options.success?(@draft.toJSON())
         Promise.resolve(@draft.toJSON())
-      spyOn(DatabaseStore, 'findByLocalId').andCallFake (klass, localId) =>
+      spyOn(DatabaseStore, 'findBy').andCallFake (klass, id) =>
+        Promise.resolve(@draft)
+      spyOn(DatabaseStore, 'find').andCallFake (klass, id) =>
         Promise.resolve(@draft)
       spyOn(DatabaseStore, 'unpersistModel').andCallFake (draft) ->
         Promise.resolve()
@@ -90,7 +92,7 @@ describe "SendDraftTask", ->
     it "should notify the draft was sent", ->
       waitsForPromise => @task.performRemote().then =>
         args = Actions.sendDraftSuccess.calls[0].args[0]
-        expect(args.draftLocalId).toBe @draftLocalId
+        expect(args.draftClientId).toBe @draftClientId
 
     it "get an object back on success", ->
       waitsForPromise => @task.performRemote().then =>
@@ -117,12 +119,12 @@ describe "SendDraftTask", ->
             expect(NylasAPI.makeRequest.calls.length).toBe(1)
             options = NylasAPI.makeRequest.mostRecentCall.args[0]
             expect(options.body.version).toBe(@draft.version)
-            expect(options.body.draft_id).toBe(@draft.id)
+            expect(options.body.draft_id).toBe(@draft.serverId)
 
     describe "when the draft has not been saved", ->
       beforeEach ->
         @draft = new Message
-          id: generateTempId()
+          id: "local-12345"
           accountId: 'A12ADE'
           subject: 'New Draft'
           draft: true
@@ -130,7 +132,7 @@ describe "SendDraftTask", ->
           to:
             name: 'Dummy'
             email: 'dummy@nylas.com'
-        @task = new SendDraftTask(@draftLocalId)
+        @task = new SendDraftTask(@draftClientId)
 
       it "should send the draft JSON", ->
         waitsForPromise =>
@@ -157,7 +159,7 @@ describe "SendDraftTask", ->
     beforeEach ->
       @draft = new Message
         version: '1'
-        id: '1233123AEDF1'
+        clientId: 'local-1234'
         accountId: 'A12ADE'
         threadId: 'threadId'
         replyToMessageId: 'replyToMessageId'
@@ -167,15 +169,15 @@ describe "SendDraftTask", ->
         to:
           name: 'Dummy'
           email: 'dummy@nylas.com'
-      @task = new SendDraftTask(@draft.id)
+      @task = new SendDraftTask("local-1234")
       spyOn(Actions, "dequeueTask")
       spyOn(DatabaseStore, 'unpersistModel').andCallFake (draft) ->
         Promise.resolve()
 
     describe "when the server responds with `Invalid message public ID`", ->
       it "should resend the draft without the reply_to_message_id key set", ->
-        @draft.id = generateTempId()
-        spyOn(DatabaseStore, 'findByLocalId').andCallFake => Promise.resolve(@draft)
+        spyOn(DatabaseStore, 'findBy').andCallFake =>
+          Promise.resolve(@draft)
         spyOn(NylasAPI, 'makeRequest').andCallFake ({body, success, error}) =>
           if body.reply_to_message_id
             err = new APIError(body: "Invalid message public id", statusCode: 400)
@@ -193,8 +195,7 @@ describe "SendDraftTask", ->
 
     describe "when the server responds with `Invalid thread ID`", ->
       it "should resend the draft without the thread_id or reply_to_message_id keys set", ->
-        @draft.id = generateTempId()
-        spyOn(DatabaseStore, 'findByLocalId').andCallFake => Promise.resolve(@draft)
+        spyOn(DatabaseStore, 'findBy').andCallFake => Promise.resolve(@draft)
         spyOn(NylasAPI, 'makeRequest').andCallFake ({body, success, error}) =>
           new Promise (resolve, reject) =>
             if body.thread_id
@@ -214,21 +215,21 @@ describe "SendDraftTask", ->
             console.log(err.trace)
 
     it "throws an error if the draft can't be found", ->
-      spyOn(DatabaseStore, 'findByLocalId').andCallFake (klass, localId) ->
+      spyOn(DatabaseStore, 'findBy').andCallFake (klass, clientId) ->
         Promise.resolve()
       waitsForPromise =>
         @task.performRemote().catch (error) ->
           expect(error.message).toBeDefined()
 
     it "throws an error if the draft isn't saved", ->
-      spyOn(DatabaseStore, 'findByLocalId').andCallFake (klass, localId) ->
-        Promise.resolve(isSaved: false)
+      spyOn(DatabaseStore, 'findBy').andCallFake (klass, clientId) ->
+        Promise.resolve(serverId: null)
       waitsForPromise =>
         @task.performRemote().catch (error) ->
           expect(error.message).toBeDefined()
 
     it "throws an error if the DB store has issues", ->
-      spyOn(DatabaseStore, 'findByLocalId').andCallFake (klass, localId) ->
+      spyOn(DatabaseStore, 'findBy').andCallFake (klass, clientId) ->
         Promise.reject("DB error")
       waitsForPromise =>
         @task.performRemote().catch (error) ->
diff --git a/spec-nylas/tasks/syncback-draft-spec.coffee b/spec-nylas/tasks/syncback-draft-spec.coffee
index f1c6e4a88..661c4c9b8 100644
--- a/spec-nylas/tasks/syncback-draft-spec.coffee
+++ b/spec-nylas/tasks/syncback-draft-spec.coffee
@@ -1,5 +1,4 @@
 _ = require 'underscore'
-{generateTempId, isTempId} = require '../../src/flux/models/utils'
 
 NylasAPI = require '../../src/flux/nylas-api'
 Task = require '../../src/flux/tasks/task'
@@ -33,30 +32,27 @@ testData =
   accountId: "abc123"
   body: '<body>123</body>'
 
-localDraft = new Message _.extend {}, testData, {id: "local-id"}
-remoteDraft = new Message _.extend {}, testData, {id: "remoteid1234"}
+localDraft = -> new Message _.extend {}, testData, {clientId: "local-id"}
+remoteDraft = -> new Message _.extend {}, testData, {clientId: "local-id", serverId: "remoteid1234"}
 
 describe "SyncbackDraftTask", ->
   beforeEach ->
-    spyOn(DatabaseStore, "findByLocalId").andCallFake (klass, localId) ->
-      if localId is "localDraftId" then Promise.resolve(localDraft)
-      else if localId is "remoteDraftId" then Promise.resolve(remoteDraft)
-      else if localId is "missingDraftId" then Promise.resolve()
-
-    spyOn(DatabaseStore, 'findBy').andCallFake (klass, matchers) ->
+    spyOn(DatabaseStore, "findBy").andCallFake (klass, {clientId}) ->
       if klass is Account
-        Promise.resolve(new Account(id: 'abc123'))
+        return Promise.resolve(new Account(clientId: 'local-abc123', serverId: 'abc123'))
+
+      if clientId is "localDraftId" then Promise.resolve(localDraft())
+      else if clientId is "remoteDraftId" then Promise.resolve(remoteDraft())
+      else if clientId is "missingDraftId" then Promise.resolve()
+      else return Promise.resolve()
 
     spyOn(DatabaseStore, "persistModel").andCallFake ->
       Promise.resolve()
 
-    spyOn(DatabaseStore, "swapModel").andCallFake ->
-      Promise.resolve()
-
   describe "performRemote", ->
     beforeEach ->
       spyOn(NylasAPI, 'makeRequest').andCallFake (opts) ->
-        Promise.resolve(remoteDraft.toJSON())
+        Promise.resolve(remoteDraft().toJSON())
 
     it "does nothing if no draft can be found in the db", ->
       task = new SyncbackDraftTask("missingDraftId")
@@ -101,33 +97,7 @@ describe "SyncbackDraftTask", ->
           options = NylasAPI.makeRequest.mostRecentCall.args[0]
           expect(options.returnsModel).toBe(false)
 
-    it "should swap the ids if we got a new one from the DB", ->
-      task = new SyncbackDraftTask("localDraftId")
-      waitsForPromise =>
-        task.performRemote().then ->
-          expect(DatabaseStore.swapModel).toHaveBeenCalled()
-          expect(DatabaseStore.persistModel).not.toHaveBeenCalled()
-
-    it "should not swap the ids if we're using a persisted one", ->
-      task = new SyncbackDraftTask("remoteDraftId")
-      waitsForPromise =>
-        task.performRemote().then ->
-          expect(DatabaseStore.swapModel).not.toHaveBeenCalled()
-          expect(DatabaseStore.persistModel).toHaveBeenCalled()
-
   describe "When the api throws a 404 error", ->
     beforeEach ->
       spyOn(NylasAPI, "makeRequest").andCallFake (opts) ->
         Promise.reject(testError(opts))
-
-    it "resets the id", ->
-      task = new SyncbackDraftTask("remoteDraftId")
-      taskStatus = null
-      task.performRemote().then (status) => taskStatus = status
-
-      waitsFor ->
-        DatabaseStore.swapModel.calls.length > 0
-      runs ->
-        newDraft = DatabaseStore.swapModel.mostRecentCall.args[0].newModel
-        expect(isTempId(newDraft.id)).toBe true
-        expect(taskStatus).toBe(Task.Status.Retry)
diff --git a/spec/fixtures/coffee.coffee b/spec/fixtures/coffee.coffee
index b8367ca59..81c78703f 100644
--- a/spec/fixtures/coffee.coffee
+++ b/spec/fixtures/coffee.coffee
@@ -1,4 +1,4 @@
-class quicksort
+class Quicksort
   sort: (items) ->
     return items if items.length <= 1
 
@@ -20,4 +20,4 @@ class quicksort
   noop: ->
     # just a noop
 
-exports.modules = quicksort
+exports.modules = Quicksort
diff --git a/spec/fixtures/sample-with-tabs-and-leading-comment.coffee b/spec/fixtures/sample-with-tabs-and-leading-comment.coffee
deleted file mode 100644
index 0f81f6fe8..000000000
--- a/spec/fixtures/sample-with-tabs-and-leading-comment.coffee
+++ /dev/null
@@ -1,4 +0,0 @@
-  # This is a comment
-if this.studyingEconomics
-	buy()	while supply > demand
-		sell() until supply > demand
diff --git a/spec/fixtures/sample-with-tabs.coffee b/spec/fixtures/sample-with-tabs.coffee
deleted file mode 100644
index 1b937ea33..000000000
--- a/spec/fixtures/sample-with-tabs.coffee
+++ /dev/null
@@ -1,4 +0,0 @@
-# Econ 101	
-if this.studyingEconomics
-	 buy()	while supply > demand
-		sell() until supply > demand
diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee
index 762ac92ce..167ce3538 100644
--- a/spec/spec-helper.coffee
+++ b/spec/spec-helper.coffee
@@ -19,6 +19,7 @@ ServiceHub = require 'service-hub'
 pathwatcher = require 'pathwatcher'
 clipboard = require 'clipboard'
 
+Account = require "../src/flux/models/account"
 AccountStore = require "../src/flux/stores/account-store"
 Contact = require '../src/flux/models/contact'
 {TaskQueue, ComponentRegistry} = require "nylas-exports"
@@ -104,6 +105,10 @@ Promise.setScheduler (fn) ->
   setTimeout(fn, 0)
   process.nextTick -> advanceClock(1)
 
+# So it passes the Utils.isTempId test
+window.TEST_ACCOUNT_CLIENT_ID = "local-test-account-client-id"
+window.TEST_ACCOUNT_ID = "test-account-server-id"
+
 beforeEach ->
   atom.testOrganizationUnit = null
   Grim.clearDeprecations() if isCoreSpec
@@ -146,9 +151,13 @@ beforeEach ->
   spyOn(atom.menu, 'sendToBrowserProcess')
 
   # Log in a fake user
-  spyOn(AccountStore, 'current').andCallFake ->
+  spyOn(AccountStore, 'current').andCallFake -> new Account
+    name: "Nylas Test"
+    provider: "gmail"
     emailAddress: 'tester@nylas.com'
-    id: 'test_account_id'
+    organizationUnit: atom.testOrganizationUnit
+    clientId: TEST_ACCOUNT_CLIENT_ID
+    serverId: TEST_ACCOUNT_ID
     usesLabels: -> atom.testOrganizationUnit is "label"
     usesFolders: -> atom.testOrganizationUnit is "folder"
     me: ->
diff --git a/src/browser/application.coffee b/src/browser/application.coffee
index 2ba511ecd..08c9f0d94 100644
--- a/src/browser/application.coffee
+++ b/src/browser/application.coffee
@@ -250,7 +250,7 @@ class Application
 
     @on 'application:send-feedback', => @windowManager.sendToMainWindow('send-feedback')
     @on 'application:open-preferences', => @windowManager.sendToMainWindow('open-preferences')
-    @on 'application:show-main-window', => @windowManager.ensurePrimaryWindowOnscreen()
+    @on 'application:show-main-window', => @windowManager.openWindowsForTokenState()
     @on 'application:show-work-window', => @windowManager.showWorkWindow()
     @on 'application:check-for-update', => @autoUpdateManager.check()
     @on 'application:install-update', =>
@@ -259,9 +259,9 @@ class Application
       @autoUpdateManager.install()
     @on 'application:open-dev', =>
       @devMode = true
-      @windowManager.closeMainWindow()
+      @windowManager.closeAllWindows()
       @windowManager.devMode = true
-      @windowManager.ensurePrimaryWindowOnscreen()
+      @windowManager.openWindowsForTokenState()
 
     @on 'application:toggle-theme', =>
       themes = @config.get('core.themes') ? []
@@ -326,7 +326,7 @@ class Application
       @windowManager.sendToMainWindow('from-react-remote-window', json)
 
     app.on 'activate-with-no-open-windows', (event) =>
-      @windowManager.ensurePrimaryWindowOnscreen()
+      @windowManager.openWindowsForTokenState()
       event.preventDefault()
 
     ipc.on 'update-application-menu', (event, template, keystrokesByCommand) =>
diff --git a/src/components/injected-component.cjsx b/src/components/injected-component.cjsx
index 8484c1e38..138314aec 100644
--- a/src/components/injected-component.cjsx
+++ b/src/components/injected-component.cjsx
@@ -13,11 +13,11 @@ components inside of your React render method. Rather than explicitly render
 a component, such as a `<Composer>`, you can use InjectedComponent:
 
 ```coffee
-<InjectedComponent matching={role:"Composer"} exposedProps={draftId:123} />
+<InjectedComponent matching={role:"Composer"} exposedProps={draftClientId:123} />
 ```
 
 InjectedComponent will look up the component registered with that role in the
-{ComponentRegistry} and render it, passing the exposedProps (`draftId={123}`) along.
+{ComponentRegistry} and render it, passing the exposedProps (`draftClientId={123}`) along.
 
 InjectedComponent monitors the ComponentRegistry for changes. If a new component
 is registered that matches the descriptor you provide, InjectedComponent will refresh.
diff --git a/src/flux/actions.coffee b/src/flux/actions.coffee
index a9ad69e33..5846c7339 100644
--- a/src/flux/actions.coffee
+++ b/src/flux/actions.coffee
@@ -58,13 +58,6 @@ Section: General
 ###
 class Actions
 
-  ###
-  Public: Fired when the {DatabaseStore} has changed the ID of a {Model}.
-
-  *Scope: Global*
-  ###
-  @didSwapModel: ActionScopeGlobal
-
   ###
   Public: Fired when the Nylas API Connector receives new data from the API.
 
@@ -427,7 +420,7 @@ class Actions
   ```
   Actions.removeFile
     file: fileObject
-    messageLocalId: draftLocalId
+    messageClientId: draftClientId
   ```
   ###
   @removeFile: ActionScopeWindow
diff --git a/src/flux/attributes.coffee b/src/flux/attributes.coffee
index f20974f8f..f790962be 100644
--- a/src/flux/attributes.coffee
+++ b/src/flux/attributes.coffee
@@ -8,6 +8,7 @@ AttributeBoolean = require './attributes/attribute-boolean'
 AttributeDateTime = require './attributes/attribute-datetime'
 AttributeCollection = require './attributes/attribute-collection'
 AttributeJoinedData = require './attributes/attribute-joined-data'
+AttributeServerId = require './attributes/attribute-serverid'
 
 module.exports =
   Matcher: Matcher
@@ -20,6 +21,7 @@ module.exports =
   DateTime: -> new AttributeDateTime(arguments...)
   Collection: -> new AttributeCollection(arguments...)
   JoinedData: -> new AttributeJoinedData(arguments...)
+  ServerId: -> new AttributeServerId(arguments...)
 
   AttributeNumber: AttributeNumber
   AttributeString: AttributeString
@@ -28,3 +30,4 @@ module.exports =
   AttributeDateTime: AttributeDateTime
   AttributeCollection: AttributeCollection
   AttributeJoinedData: AttributeJoinedData
+  AttributeServerId: AttributeServerId
diff --git a/src/flux/attributes/attribute-collection.coffee b/src/flux/attributes/attribute-collection.coffee
index 60a4df6db..72a7a68b7 100644
--- a/src/flux/attributes/attribute-collection.coffee
+++ b/src/flux/attributes/attribute-collection.coffee
@@ -10,8 +10,7 @@ For example, Threads in N1 have a collection of Labels or Folders.
 When Collection attributes are marked as `queryable`, the DatabaseStore
 automatically creates a join table and maintains it as you create, save,
 and delete models. When you call `persistModel`, entries are added to the
-join table associating the ID of the model with the IDs of models in the
-collection.
+join table associating the ID of the model with the IDs of models in the collection.
 
 Collection attributes have an additional clause builder, `contains`:
 
@@ -28,9 +27,7 @@ WHERE `M1`.`value` = 'inbox'
 ORDER BY `Thread`.`last_message_received_timestamp` DESC
 ```
 
-The value of this attribute is always an array of ff other model objects. To use
-a Collection attribute, the JSON for the parent object must contain the nested
-objects, complete with their `object` field.
+The value of this attribute is always an array of other model objects.
 
 Section: Database
 ###
@@ -58,10 +55,11 @@ class AttributeCollection extends Attribute
     objs = []
     for objJSON in json
       obj = new @itemClass(objJSON)
-      # Important: if no ids are in the JSON, don't make them up randomly.
-      # This causes an object to be "different" each time it's de-serialized
-      # even if it's actually the same, makes React components re-render!
-      obj.id = undefined
+      # Important: if no ids are in the JSON, don't make them up
+      # randomly.  This causes an object to be "different" each time it's
+      # de-serialized even if it's actually the same, makes React
+      # components re-render!
+      obj.clientId = undefined
       obj.fromJSON(objJSON) if obj.fromJSON?
       objs.push(obj)
     objs
diff --git a/src/flux/attributes/attribute-object.coffee b/src/flux/attributes/attribute-object.coffee
index 1d7095f6c..4a9bdb34e 100644
--- a/src/flux/attributes/attribute-object.coffee
+++ b/src/flux/attributes/attribute-object.coffee
@@ -23,7 +23,7 @@ class AttributeObject extends Attribute
       # Important: if no ids are in the JSON, don't make them up randomly.
       # This causes an object to be "different" each time it's de-serialized
       # even if it's actually the same, makes React components re-render!
-      obj.id = undefined
+      obj.clientId = undefined
       # Warning: typeof(null) is object
       if obj.fromJSON and val and typeof(val) is 'object'
         obj.fromJSON(val)
diff --git a/src/flux/attributes/attribute-serverid.coffee b/src/flux/attributes/attribute-serverid.coffee
new file mode 100644
index 000000000..586963405
--- /dev/null
+++ b/src/flux/attributes/attribute-serverid.coffee
@@ -0,0 +1,22 @@
+AttributeString = require './attribute-string'
+Matcher = require './matcher'
+
+###
+Public: The value of this attribute is always a string or `null`.
+
+String attributes can be queries using `equal`, `not`, and `startsWith`. Matching on
+`greaterThan` and `lessThan` is not supported.
+
+Section: Database
+###
+class AttributeServerId extends AttributeString
+  toJSON: (val) ->
+    if val and Utils.isTempId(val)
+      throw (new Error "AttributeServerId::toJSON (#{@modelKey}) #{val} does not look like a valid server id")
+
+  equal: (val) ->
+    if val and Utils.isTempId(val)
+      throw (new Error "AttributeServerId::equal (#{@modelKey}) #{val} is not a valid value for this field.")
+    super
+
+module.exports = AttributeString
diff --git a/src/flux/models/event.coffee b/src/flux/models/event.coffee
index db4bb54b5..74d24aef7 100644
--- a/src/flux/models/event.coffee
+++ b/src/flux/models/event.coffee
@@ -6,11 +6,6 @@ _ = require 'underscore'
 class Event extends Model
 
   @attributes: _.extend {}, Model.attributes,
-    'id': Attributes.String
-      queryable: true
-      modelKey: 'id'
-      jsonKey: 'id'
-
     'title': Attributes.String
       modelKey: 'title'
       jsonKey: 'title'
diff --git a/src/flux/models/local-link.coffee b/src/flux/models/local-link.coffee
deleted file mode 100644
index 9e8d87e99..000000000
--- a/src/flux/models/local-link.coffee
+++ /dev/null
@@ -1,18 +0,0 @@
-_ = require 'underscore'
-Model = require './model'
-Attributes = require '../attributes'
-
-class LocalLink extends Model
-  @attributes:
-    'id': Attributes.String
-      queryable: true
-      modelKey: 'id'
-
-    'objectId': Attributes.String
-      queryable: true
-      modelKey: 'objectId'
-  
-  constructor: ({@id, @objectId} = {}) ->
-    @
-
-module.exports = LocalLink
diff --git a/src/flux/models/message.coffee b/src/flux/models/message.coffee
index 1d3cc0f63..b8d22e755 100644
--- a/src/flux/models/message.coffee
+++ b/src/flux/models/message.coffee
@@ -118,7 +118,7 @@ class Message extends Model
     'snippet': Attributes.String
       modelKey: 'snippet'
 
-    'threadId': Attributes.String
+    'threadId': Attributes.ServerId
       queryable: true
       modelKey: 'threadId'
       jsonKey: 'thread_id'
@@ -140,7 +140,7 @@ class Message extends Model
       modelKey: 'version'
       queryable: true
 
-    'replyToMessageId': Attributes.String
+    'replyToMessageId': Attributes.ServerId
       modelKey: 'replyToMessageId'
       jsonKey: 'reply_to_message_id'
 
@@ -160,6 +160,7 @@ class Message extends Model
   @additionalSQLiteConfig:
     setup: ->
       ['CREATE INDEX IF NOT EXISTS MessageListIndex ON Message(account_id, thread_id, date ASC)',
+       'CREATE UNIQUE INDEX IF NOT EXISTS MessageDraftIndex ON Message(client_id)',
        'CREATE UNIQUE INDEX IF NOT EXISTS MessageBodyIndex ON MessageBody(id)']
 
   constructor: ->
diff --git a/src/flux/models/model.coffee b/src/flux/models/model.coffee
index fab0f18dc..cd0ac42a4 100644
--- a/src/flux/models/model.coffee
+++ b/src/flux/models/model.coffee
@@ -1,7 +1,6 @@
-Attributes = require '../attributes'
-ModelQuery = require './query'
-{isTempId, generateTempId} = require './utils'
 _ = require 'underscore'
+Utils = require './utils'
+Attributes = require '../attributes'
 
 ###
 Public: A base class for API objects that provides abstract support for
@@ -9,7 +8,19 @@ serialization and deserialization, matching by attributes, and ID-based equality
 
 ## Attributes
 
-`id`: {AttributeString} The ID of the model. Queryable.
+`id`: {AttributeString} The resolved canonical ID of the model used in the
+database and generally throughout the app. The id property is a custom
+getter that resolves to the serverId first, and then the clientId.
+
+`clientId`: {AttributeString} An ID created at object construction and
+persists throughout the lifetime of the object. This is extremely useful
+for optimistically creating objects (like drafts and categories) and
+having a constant reference to it. In all other cases, use the resolved
+`id` field.
+
+`serverId`: {AttributeServerId} The server ID of the model. In most cases,
+except optimistic creation, this will also be the canonical id of the
+object.
 
 `object`: {AttributeString} The model's type. This field is used by the JSON
  deserializer to create an instance of the correct class when inflating the object.
@@ -20,15 +31,31 @@ Section: Models
 ###
 class Model
 
+  Object.defineProperty @prototype, "id",
+    enumerable: false
+    get: -> @serverId ? @clientId
+    set: ->
+      throw new Error("You may not directly set the ID of an object. Set either the `clientId` or the `serverId` instead.")
+
   @attributes:
+    # Lookups will go through the custom getter.
     'id': Attributes.String
       queryable: true
       modelKey: 'id'
 
+    'clientId': Attributes.String
+      queryable: true
+      modelKey: 'clientId'
+      jsonKey: 'client_id'
+
+    'serverId': Attributes.ServerId
+      modelKey: 'serverId'
+      jsonKey: 'server_id'
+
     'object': Attributes.String
       modelKey: 'object'
 
-    'accountId': Attributes.String
+    'accountId': Attributes.ServerId
       queryable: true
       modelKey: 'accountId'
       jsonKey: 'account_id'
@@ -36,9 +63,13 @@ class Model
   @naturalSortOrder: -> null
 
   constructor: (values = {}) ->
+    if values["id"] and Utils.isTempId(values["id"])
+      values["clientId"] ?= values["id"]
+    else
+      values["serverId"] ?= values["id"]
     for key, definition of @attributes()
       @[key] = values[key] if values[key]?
-    @id ||= generateTempId()
+    @clientId ?= Utils.generateTempId()
     @
 
   clone: ->
@@ -47,12 +78,9 @@ class Model
   # Public: Returns an {Array} of {Attribute} objects defined on the Model's constructor
   #
   attributes: ->
-    @constructor.attributes
-
-  # Public Returns true if the object has a server-provided ID, false otherwise.
-  #
-  isSaved: ->
-    !isTempId(@id)
+    attrs = _.clone(@constructor.attributes)
+    delete attrs["id"]
+    return attrs
 
   ##
   # Public: Inflates the model object from JSON, using the defined attributes to
@@ -63,6 +91,8 @@ class Model
   # This method is chainable.
   #
   fromJSON: (json) ->
+    if json["id"] and not Utils.isTempId(json["id"])
+      @serverId = json["id"]
     for key, attr of @attributes()
       @[key] = attr.fromJSON(json[attr.jsonKey]) unless json[attr.jsonKey] is undefined
     @
@@ -82,6 +112,7 @@ class Model
       if attr instanceof Attributes.AttributeJoinedData and options.joined is false
         continue
       json[attr.jsonKey] = value
+    json["id"] = @id
     json
 
   toString: ->
diff --git a/src/flux/models/thread.coffee b/src/flux/models/thread.coffee
index 2ac5807f1..eaab94514 100644
--- a/src/flux/models/thread.coffee
+++ b/src/flux/models/thread.coffee
@@ -94,20 +94,6 @@ class Thread extends Model
     @lastMessageReceivedTimestamp ||= new Date(json['last_message_timestamp'] * 1000)
     @
 
-  # Public: Returns true if the thread has a {Category} with the given ID.
-  #
-  # * `id` A {String} {Category} ID
-  #
-  hasCategoryId: (id) ->
-    return false unless id
-    for folder in (@folders ? [])
-      return true if folder.id is id
-    for label in (@labels ? [])
-      return true if label.id is id
-    return false
-  hasLabelId: (id) -> @hasCategoryId(id)
-  hasFolderId: (id) -> @hasCategoryId(id)
-
   # Public: Returns true if the thread has a {Category} with the given
   # name. Note, only `CategoryStore::standardCategories` have valid
   # `names`
diff --git a/src/flux/models/utils.coffee b/src/flux/models/utils.coffee
index 1d309017f..8f343ad57 100644
--- a/src/flux/models/utils.coffee
+++ b/src/flux/models/utils.coffee
@@ -211,7 +211,7 @@ Utils =
 
   isEqualReact: (a, b, options={}) ->
     options.functionsAreEqual = true
-    options.ignoreKeys = (options.ignoreKeys ? []).push("localId")
+    options.ignoreKeys = (options.ignoreKeys ? []).push("clientId")
     Utils.isEqual(a, b, options)
 
   # Customized version of Underscore 1.8.2's isEqual function
diff --git a/src/flux/nylas-api.coffee b/src/flux/nylas-api.coffee
index 0299ef924..574303652 100644
--- a/src/flux/nylas-api.coffee
+++ b/src/flux/nylas-api.coffee
@@ -213,8 +213,9 @@ class NylasAPI
 
     return Promise.resolve()
 
-  # Returns a Promsie that resolves when any parsed out models (if any)
+  # Returns a Promise that resolves when any parsed out models (if any)
   # have been created and persisted to the database.
+  #
   _handleModelResponse: (jsons) ->
     if not jsons
       return Promise.reject(new Error("handleModelResponse with no JSON provided"))
@@ -223,26 +224,49 @@ class NylasAPI
     if jsons.length is 0
       return Promise.resolve([])
 
-    # Run a few assertions to make sure we're not going to run into problems
-    uniquedJSONs = _.uniq jsons, false, (model) -> model.id
-    if uniquedJSONs.length < jsons.length
-      console.warn("NylasAPI.handleModelResponse: called with non-unique object set. Maybe an API request returned the same object more than once?")
-
     type = jsons[0].object
     klass = @_apiObjectToClassMap[type]
     if not klass
       console.warn("NylasAPI::handleModelResponse: Received unknown API object type: #{type}")
       return Promise.resolve([])
 
-    accepted = Promise.resolve(uniquedJSONs)
-    if type is "thread" or type is "draft"
-      accepted = @_acceptableModelsInResponse(klass, uniquedJSONs)
+    # Step 1: Make sure the list of objects contains no duplicates, which cause
+    # problems downstream when we try to write to the database.
+    uniquedJSONs = _.uniq jsons, false, (model) -> model.id
+    if uniquedJSONs.length < jsons.length
+      console.warn("NylasAPI.handleModelResponse: called with non-unique object set. Maybe an API request returned the same object more than once?")
 
-    mapper = (json) -> (new klass).fromJSON(json)
+    # Step 2: Filter out any objects locked by the optimistic change tracker.
+    unlockedJSONs = _.filter uniquedJSONs, (json) =>
+      if @_optimisticChangeTracker.acceptRemoteChangesTo(klass, json.id) is false
+        json._delta?.ignoredBecause = "This model is locked by the optimistic change tracker"
+        return false
+      return true
 
-    accepted.map(mapper).then (objects) ->
-      DatabaseStore.persistModels(objects).then ->
-        return Promise.resolve(objects)
+    # Step 3: Retrieve any existing models from the database for the given IDs.
+    ids = _.pluck(unlockedJSONs, 'id')
+    DatabaseStore = require './stores/database-store'
+    DatabaseStore.findAll(klass).where(klass.attributes.id.in(ids)).then (models) ->
+      existingModels = {}
+      existingModels[model.id] = model for model in models
+
+      responseModels = []
+      changedModels = []
+
+      # Step 4: Merge the response data into the existing data for each model,
+      # skipping changes when we already have the given version
+      unlockedJSONs.forEach (json) =>
+        model = existingModels[json.id]
+        unless model and model.version? and json.version? and model.version is json.version
+          model ?= new klass()
+          model.fromJSON(json)
+          changedModels.push(model)
+        responseModels.push(model)
+
+      # Step 5: Save models that have changed, and then return all of the models
+      # that were in the response body.
+      DatabaseStore.persistModels(changedModels).then ->
+        return Promise.resolve(responseModels)
 
   _apiObjectToClassMap:
     "file": require('./models/file')
@@ -257,25 +281,6 @@ class NylasAPI
     "calendar": require('./models/calendar')
     "metadata": require('./models/metadata')
 
-  _acceptableModelsInResponse: (klass, jsons) ->
-    # Filter out models that are locked by pending optimistic changes
-    accepted = jsons.filter (json) =>
-      if @_optimisticChangeTracker.acceptRemoteChangesTo(klass, json.id) is false
-        json._delta?.ignoredBecause = "This model is locked by the optimistic change tracker"
-        return false
-      return true
-
-    # Filter out models that already have newer versions in the local cache
-    ids = _.pluck(accepted, 'id')
-    DatabaseStore = require './stores/database-store'
-    DatabaseStore.findVersions(klass, ids).then (versions) ->
-      accepted = accepted.filter (json) ->
-        if json.version and versions[json.id] >= json.version
-          json._delta?.ignoredBecause = "This version (#{json.version}) is not newer. Already have (#{versions[json.id]})"
-          return false
-        return true
-      Promise.resolve(accepted)
-
   getThreads: (accountId, params = {}, requestOptions = {}) ->
     requestSuccess = requestOptions.success
     requestOptions.success = (json) =>
diff --git a/src/flux/stores/analytics-store.coffee b/src/flux/stores/analytics-store.coffee
index 3b0aa4ea6..10acadb8b 100644
--- a/src/flux/stores/analytics-store.coffee
+++ b/src/flux/stores/analytics-store.coffee
@@ -52,10 +52,10 @@ AnalyticsStore = Reflux.createStore
     composeReply: ({threadId, messageId}) -> {threadId, messageId}
     composeForward: ({threadId, messageId}) -> {threadId, messageId}
     composeReplyAll: ({threadId, messageId}) -> {threadId, messageId}
-    composePopoutDraft: (draftLocalId) -> {draftLocalId: draftLocalId}
+    composePopoutDraft: (draftClientId) -> {draftClientId: draftClientId}
     composeNewBlankDraft: -> {}
-    sendDraft: (draftLocalId) -> {draftLocalId: draftLocalId}
-    destroyDraft: (draftLocalId) -> {draftLocalId: draftLocalId}
+    sendDraft: (draftClientId) -> {draftClientId}
+    destroyDraft: (draftClientId) -> {draftClientId}
     searchQueryCommitted: (query) -> {}
     fetchAndOpenFile: -> {}
     fetchAndSaveFile: -> {}
@@ -65,7 +65,7 @@ AnalyticsStore = Reflux.createStore
   coreGlobalActions: ->
     fileAborted: (uploadData={}) -> {fileSize: uploadData.fileSize}
     fileUploaded: (uploadData={}) -> {fileSize: uploadData.fileSize}
-    sendDraftSuccess: ({draftLocalId}) -> {draftLocalId: draftLocalId}
+    sendDraftSuccess: ({draftClientId}) -> {draftClientId}
 
   track: (action, data={}) ->
     _.defer =>
diff --git a/src/flux/stores/database-store.coffee b/src/flux/stores/database-store.coffee
index 5e1763af6..79e850e96 100644
--- a/src/flux/stores/database-store.coffee
+++ b/src/flux/stores/database-store.coffee
@@ -4,8 +4,8 @@ async = require 'async'
 path = require 'path'
 sqlite3 = require 'sqlite3'
 Model = require '../models/model'
+Utils = require '../models/utils'
 Actions = require '../actions'
-LocalLink = require '../models/local-link'
 ModelQuery = require '../models/query'
 NylasStore = require '../../../exports/nylas-store'
 DatabaseSetupQueryBuilder = require './database-setup-query-builder'
@@ -14,12 +14,10 @@ PriorityUICoordinator = require '../../priority-ui-coordinator'
 {AttributeCollection, AttributeJoinedData} = require '../attributes'
 
 {tableNameForJoin,
- generateTempId,
  serializeRegisteredObjects,
- deserializeRegisteredObjects,
- isTempId} = require '../models/utils'
+ deserializeRegisteredObjects} = require '../models/utils'
 
-DatabaseVersion = 59
+DatabaseVersion = 12
 
 DatabasePhase =
   Setup: 'setup'
@@ -81,7 +79,6 @@ class DatabaseStore extends NylasStore
 
   constructor: ->
     @_triggerPromise = null
-    @_localIdLookupCache = {}
     @_inflightTransactions = 0
     @_open = false
     @_waiting = []
@@ -221,10 +218,11 @@ class DatabaseStore extends NylasStore
             str = results.map((row) -> row.detail).join('\n') + " for " + query
             @_prettyConsoleLog(str) if str.indexOf("SCAN") isnt -1
 
-      # Important: once the user begins a transaction, queries need to run in serial.
-      # This ensures that the subsequent "COMMIT" call actually runs after the other
-      # queries in the transaction, and that no other code can execute "BEGIN TRANS."
-      # until the previously queued BEGIN/COMMIT have been processed.
+      # Important: once the user begins a transaction, queries need to run
+      # in serial.  This ensures that the subsequent "COMMIT" call
+      # actually runs after the other queries in the transaction, and that
+      # no other code can execute "BEGIN TRANS." until the previously
+      # queued BEGIN/COMMIT have been processed.
 
       # We don't exit serial execution mode until the last pending transaction has
       # finished executing.
@@ -316,8 +314,7 @@ class DatabaseStore extends NylasStore
     new ModelQuery(klass, @).where(predicates).count()
 
   # Public: Modelify converts the provided array of IDs or models (or a mix of
-  # IDs and models) into an array of models of the `klass` provided by querying
-  # for the missing items.
+  # IDs and models) into an array of models of the `klass` provided by querying for the missing items.
   #
   # Modelify is efficient and uses a single database query. It resolves Immediately
   # if no query is necessary.
@@ -353,106 +350,6 @@ class DatabaseStore extends NylasStore
 
       return Promise.resolve(arr)
 
-  ###
-  Support for Local IDs
-  ###
-
-  # Public: Retrieve a Model given a localId.
-  #
-  # - `class` The class of the {Model} you're trying to retrieve.
-  # - `localId` The {String} localId of the object.
-  #
-  # Returns a {Promise} that:
-  #   - resolves with the Model associated with the localId
-  #   - rejects if no matching object is found
-  #
-  # Note: When fetching an object by local Id, joined attributes
-  # (like body, stored in a separate table) are always included.
-  #
-  findByLocalId: (klass, localId) =>
-    return Promise.reject(new Error("DatabaseStore::findByLocalId - You must provide a class")) unless klass
-    return Promise.reject(new Error("DatabaseStore::findByLocalId - You must provide a localId")) unless localId
-
-    new Promise (resolve, reject) =>
-      @find(LocalLink, localId).then (link) =>
-        return reject(new Error("DatabaseStore::findByLocalId - no LocalLink found")) unless link
-        query = @find(klass, link.objectId).includeAll().then(resolve)
-
-  # Public: Give a Model a localId.
-  #
-  # - `model` A {Model} object to assign a localId.
-  # - `localId` (optional) The {String} localId. If you don't pass a LocalId, one
-  #    will be automatically assigned.
-  #
-  # Returns a {Promise} that:
-  #   - resolves with the localId assigned to the model
-  bindToLocalId: (model, localId = null) =>
-    return Promise.reject(new Error("DatabaseStore::bindToLocalId - You must provide a model")) unless model
-    return Promise.reject(new Error("DatabaseStore::bindToLocalId - Recieved a model with no ID")) unless model.id?
-
-    new Promise (resolve, reject) =>
-      unless localId
-        if isTempId(model.id)
-          localId = model.id
-        else
-          localId = generateTempId()
-
-      link = new LocalLink({id: localId, objectId: model.id})
-      @_localIdLookupCache[model.id] = localId
-
-      @persistModel(link).then ->
-        resolve(localId)
-      .catch(reject)
-
-  # Public: Look up the localId assigned to the model. If no localId has been
-  # assigned to the model yet, it assigns a new one and persists it to the database.
-  #
-  # - `model` A {Model} object to assign a localId.
-  #
-  # Returns a {Promise} that:
-  #   - resolves with the {String} localId.
-  localIdForModel: (model) =>
-    return Promise.reject(new Error("DatabaseStore::localIdForModel - You must provide a model")) unless model
-
-    new Promise (resolve, reject) =>
-      if @_localIdLookupCache[model.id]
-        return resolve(@_localIdLookupCache[model.id])
-
-      @findBy(LocalLink, {objectId: model.id}).then (link) =>
-        if link
-          @_localIdLookupCache[model.id] = link.id
-          resolve(link.id)
-        else
-          @bindToLocalId(model).then(resolve).catch(reject)
-
-  # Private: Returns an {Object} with id-version key value pairs for models in
-  # the ID set provided. Does not retrieve or inflate object JSON from the database.
-  #
-  # Using this method requires that the klass has declared the version field queryable.
-  #
-  findVersions: (klass, allIds) =>
-    return Promise.reject(new Error("DatabaseStore::findVersions - You must provide a class")) unless klass
-    return Promise.reject(new Error("DatabaseStore::findVersions - version field must be queryable")) unless klass.attributes.version.queryable
-
-    _findVersionsFor = (ids) =>
-      marks = new Array(ids.length)
-      marks[idx] = '?' for m, idx in marks
-      @_query("SELECT id, version FROM `#{klass.name}` WHERE id IN (#{marks.join(",")})", ids).then (results) ->
-        map = {}
-        map[id] = version  for {id, version} in results
-        Promise.resolve(map)
-
-    promises = []
-    while allIds.length > 0
-      promises.push(_findVersionsFor(allIds.splice(0, 100)))
-
-    # We can only use WHERE IN for up to ~250 items at a time. Run a query for
-    # every 100 items and then combine the results before returning.
-    Promise.all(promises).then (results) =>
-      all = {}
-      all = _.extend(all, result) for result in results
-      Promise.resolve(all)
-
   # Public: Executes a {ModelQuery} on the local database.
   #
   # - `modelQuery` A {ModelQuery} to execute.
@@ -530,34 +427,6 @@ class DatabaseStore extends NylasStore
     ]).then =>
       @_triggerSoon({objectClass: model.constructor.name, objects: [model], type: 'unpersist'})
 
-  # Public: Given an `oldModel` with a unique `localId`, it will swap the
-  # item out in the database.
-  #
-  # - `args` An arguments hash with:
-  #   - `oldModel` The old model
-  #   - `newModel` The new model
-  #   - `localId` The localId to reference
-  #
-  # Returns a {Promise} that
-  #   - resolves after the database queries are complete and any listening
-  #     database callbacks have finished
-  #   - rejects if any databse query fails or one of the triggering
-  #     callbacks failed
-  swapModel: ({oldModel, newModel, localId}) =>
-    queryPromise = Promise.all([
-      @_query(BEGIN_TRANSACTION)
-      @_deleteModel(oldModel)
-      @_writeModels([newModel])
-      @_writeModels([new LocalLink(id: localId, objectId: newModel.id)]) if localId
-      @_query(COMMIT)
-    ]).then =>
-      Actions.didSwapModel({
-        oldModel: oldModel,
-        newModel: newModel,
-        localId: localId
-      })
-      @_triggerSoon({objectClass: newModel.constructor.name, objects: [oldModel, newModel], type: 'swap'})
-
   persistJSONObject: (key, json) ->
     jsonString = serializeRegisteredObjects(json)
     @_query(BEGIN_TRANSACTION)
diff --git a/src/flux/stores/draft-store-proxy.coffee b/src/flux/stores/draft-store-proxy.coffee
index 48ffa363f..f3aebe154 100644
--- a/src/flux/stores/draft-store-proxy.coffee
+++ b/src/flux/stores/draft-store-proxy.coffee
@@ -22,7 +22,7 @@ DraftChangeSet associated with the store proxy. The DraftChangeSet does two thin
 Section: Drafts
 ###
 class DraftChangeSet
-  constructor: (@localId, @_onChange) ->
+  constructor: (@clientId, @_onChange) ->
     @_commitChain = Promise.resolve()
     @_pending = {}
     @_saving = {}
@@ -51,19 +51,19 @@ class DraftChangeSet
         return Promise.resolve(true)
 
       DatabaseStore = require './database-store'
-      return DatabaseStore.findByLocalId(Message, @localId).then (draft) =>
+      DatabaseStore.findBy(Message, clientId: @clientId).then (draft) =>
         if @_destroyed
           return Promise.resolve(true)
 
         if not draft
-          throw new Error("DraftChangeSet.commit: Assertion failure. Draft #{@localId} is not in the database.")
+          throw new Error("DraftChangeSet.commit: Assertion failure. Draft #{@clientId} is not in the database.")
 
         @_saving = @_pending
         @_pending = {}
         draft = @applyToModel(draft)
 
         return DatabaseStore.persistModel(draft).then =>
-          syncback = new SyncbackDraftTask(@localId)
+          syncback = new SyncbackDraftTask(@clientId)
           Actions.queueTask(syncback)
           @_saving = {}
 
@@ -94,17 +94,16 @@ class DraftStoreProxy
   @include Publisher
   @include Listener
 
-  constructor: (@draftLocalId, draft = null) ->
+  constructor: (@draftClientId, draft = null) ->
     DraftStore = require './draft-store'
 
     @listenTo DraftStore, @_onDraftChanged
-    @listenTo Actions.didSwapModel, @_onDraftSwapped
 
     @_draft = false
     @_draftPristineBody = null
     @_destroyed = false
 
-    @changes = new DraftChangeSet @draftLocalId, =>
+    @changes = new DraftChangeSet @draftClientId, =>
       return if @_destroyed
       if !@_draft
         throw new Error("DraftChangeSet was modified before the draft was prepared.")
@@ -115,7 +114,7 @@ class DraftStoreProxy
       @_draftPromise = Promise.resolve(@)
 
     @prepare()
-    
+
   # Public: Returns the draft object with the latest changes applied.
   #
   draft: ->
@@ -131,9 +130,9 @@ class DraftStoreProxy
 
   prepare: ->
     DatabaseStore = require './database-store'
-    @_draftPromise ?= DatabaseStore.findByLocalId(Message, @draftLocalId).then (draft) =>
+    @_draftPromise ?= DatabaseStore.findBy(Message, clientId: @draftClientId).then (draft) =>
       return Promise.reject(new Error("Draft has been destroyed.")) if @_destroyed
-      return Promise.reject(new Error("Assertion Failure: Draft #{@draftLocalId} not found.")) if not draft
+      return Promise.reject(new Error("Assertion Failure: Draft #{@draftClientId} not found.")) if not draft
       @_setDraft(draft)
       Promise.resolve(@)
     @_draftPromise
@@ -167,12 +166,4 @@ class DraftStoreProxy
       @_draft = _.extend @_draft, _.last(myDrafts)
       @trigger()
 
-  _onDraftSwapped: (change) ->
-    # A draft was saved with a new ID. Since we use the draft ID to
-    # watch for changes to our draft, we need to pull again using our
-    # localId.
-    if change.oldModel.id is @_draft.id
-      @_setDraft(change.newModel)
-
-
 module.exports = DraftStoreProxy
diff --git a/src/flux/stores/draft-store.coffee b/src/flux/stores/draft-store.coffee
index 59bb5ff34..5058a997b 100644
--- a/src/flux/stores/draft-store.coffee
+++ b/src/flux/stores/draft-store.coffee
@@ -18,7 +18,7 @@ Actions = require '../actions'
 
 TaskQueue = require './task-queue'
 
-{subjectWithPrefix, generateTempId} = require '../models/utils'
+{subjectWithPrefix} = require '../models/utils'
 {Listener, Publisher} = require '../modules/reflux-coffee'
 CoffeeHelpers = require '../coffee-helpers'
 DOMUtils = require '../../dom-utils'
@@ -46,7 +46,7 @@ class DraftStore
     @listenTo Actions.composeReply, @_onComposeReply
     @listenTo Actions.composeForward, @_onComposeForward
     @listenTo Actions.composeReplyAll, @_onComposeReplyAll
-    @listenTo Actions.composePopoutDraft, @_onPopoutDraftLocalId
+    @listenTo Actions.composePopoutDraft, @_onPopoutDraftClientId
     @listenTo Actions.composeNewBlankDraft, @_onPopoutBlankDraft
 
     atom.commands.add 'body',
@@ -89,30 +89,30 @@ class DraftStore
   ######### PUBLIC #######################################################
 
   # Public: Fetch a {DraftStoreProxy} for displaying and/or editing the
-  # draft with `localId`.
+  # draft with `clientId`.
   #
   # Example:
   #
   # ```coffee
-  # session = DraftStore.sessionForLocalId(localId)
+  # session = DraftStore.sessionForClientId(clientId)
   # session.prepare().then ->
   #    # session.draft() is now ready
   # ```
   #
-  # - `localId` The {String} local ID of the draft.
+  # - `clientId` The {String} clientId of the draft.
   #
   # Returns a {Promise} that resolves to an {DraftStoreProxy} for the
   # draft once it has been prepared:
-  sessionForLocalId: (localId) =>
-    if not localId
-      throw new Error("DraftStore::sessionForLocalId requires a localId")
-    @_draftSessions[localId] ?= new DraftStoreProxy(localId)
-    @_draftSessions[localId].prepare()
+  sessionForClientId: (clientId) =>
+    if not clientId
+      throw new Error("DraftStore::sessionForClientId requires a clientId")
+    @_draftSessions[clientId] ?= new DraftStoreProxy(clientId)
+    @_draftSessions[clientId].prepare()
 
-  # Public: Look up the sending state of the given draft Id.
+  # Public: Look up the sending state of the given draftClientId.
   # In popout windows the existance of the window is the sending state.
-  isSendingDraft: (draftLocalId) ->
-    return @_draftsSending[draftLocalId]?
+  isSendingDraft: (draftClientId) ->
+    return @_draftsSending[draftClientId]?
 
   ###
   Composer Extensions
@@ -142,7 +142,7 @@ class DraftStore
 
   _doneWithSession: (session) ->
     session.teardown()
-    delete @_draftSessions[session.draftLocalId]
+    delete @_draftSessions[session.draftClientId]
 
   _onBeforeUnload: =>
     promises = []
@@ -153,7 +153,7 @@ class DraftStore
     # window.close() within on onbeforeunload could do weird things.
     for key, session of @_draftSessions
       if session.draft()?.pristine
-        Actions.queueTask(new DestroyDraftTask(draftLocalId: session.draftLocalId))
+        Actions.queueTask(new DestroyDraftTask(draftClientId: session.draftClientId))
       else
         promises.push(session.changes.commit())
 
@@ -204,19 +204,12 @@ class DraftStore
       continue unless extension.prepareNewDraft
       extension.prepareNewDraft(draft)
 
-    # Normally we'd allow the DatabaseStore to create a localId, wait for it to
-    # commit a LocalLink and resolve, etc. but it's faster to create one now.
-    draftLocalId = generateTempId()
-
     # Optimistically create a draft session and hand it the draft so that it
     # doesn't need to do a query for it a second from now when the composer wants it.
-    @_draftSessions[draftLocalId] = new DraftStoreProxy(draftLocalId, draft)
+    @_draftSessions[draft.clientId] = new DraftStoreProxy(draft.clientId, draft)
 
-    Promise.all([
-      DatabaseStore.bindToLocalId(draft, draftLocalId)
-      DatabaseStore.persistModel(draft)
-    ]).then =>
-      return Promise.resolve({draftLocalId})
+    DatabaseStore.persistModel(draft).then =>
+      Promise.resolve(draftClientId: draft.clientId)
 
   _newMessageWithContext: ({thread, threadId, message, messageId, popout}, attributesCallback) =>
     return unless AccountStore.current()
@@ -254,34 +247,34 @@ class DraftStore
         DOMUtils.escapeHTMLCharacters(_.invoke(cs, "toString").join(", "))
 
       if attributes.replyToMessage
-        msg = attributes.replyToMessage
+        replyToMessage = attributes.replyToMessage
 
-        attributes.subject = subjectWithPrefix(msg.subject, 'Re:')
-        attributes.replyToMessageId = msg.id
+        attributes.subject = subjectWithPrefix(replyToMessage.subject, 'Re:')
+        attributes.replyToMessageId = replyToMessage.id
         attributes.body = """
           <br><br><blockquote class="gmail_quote"
             style="margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex;">
-            #{DOMUtils.escapeHTMLCharacters(msg.replyAttributionLine())}
+            #{DOMUtils.escapeHTMLCharacters(replyToMessage.replyAttributionLine())}
             <br>
-            #{@_formatBodyForQuoting(msg.body)}
+            #{@_formatBodyForQuoting(replyToMessage.body)}
           </blockquote>"""
         delete attributes.quotedMessage
 
       if attributes.forwardMessage
-        msg = attributes.forwardMessage
+        forwardMessage = attributes.forwardMessage
         fields = []
-        fields.push("From: #{contactsAsHtml(msg.from)}") if msg.from.length > 0
-        fields.push("Subject: #{msg.subject}")
-        fields.push("Date: #{msg.formattedDate()}")
-        fields.push("To: #{contactsAsHtml(msg.to)}") if msg.to.length > 0
-        fields.push("CC: #{contactsAsHtml(msg.cc)}") if msg.cc.length > 0
-        fields.push("BCC: #{contactsAsHtml(msg.bcc)}") if msg.bcc.length > 0
+        fields.push("From: #{contactsAsHtml(forwardMessage.from)}") if forwardMessage.from.length > 0
+        fields.push("Subject: #{forwardMessage.subject}")
+        fields.push("Date: #{forwardMessage.formattedDate()}")
+        fields.push("To: #{contactsAsHtml(forwardMessage.to)}") if forwardMessage.to.length > 0
+        fields.push("CC: #{contactsAsHtml(forwardMessage.cc)}") if forwardMessage.cc.length > 0
+        fields.push("BCC: #{contactsAsHtml(forwardMessage.bcc)}") if forwardMessage.bcc.length > 0
 
-        if msg.files?.length > 0
+        if forwardMessage.files?.length > 0
           attributes.files ?= []
-          attributes.files = attributes.files.concat(msg.files)
+          attributes.files = attributes.files.concat(forwardMessage.files)
 
-        attributes.subject = subjectWithPrefix(msg.subject, 'Fwd:')
+        attributes.subject = subjectWithPrefix(forwardMessage.subject, 'Fwd:')
         attributes.body = """
           <br><br><blockquote class="gmail_quote"
             style="margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex;">
@@ -289,7 +282,7 @@ class DraftStore
             <br><br>
             #{fields.join('<br>')}
             <br><br>
-            #{@_formatBodyForQuoting(msg.body)}
+            #{@_formatBodyForQuoting(forwardMessage.body)}
           </blockquote>"""
         delete attributes.forwardedMessage
 
@@ -301,8 +294,8 @@ class DraftStore
         threadId: thread.id
         accountId: thread.accountId
 
-      @_finalizeAndPersistNewMessage(draft).then ({draftLocalId}) =>
-        Actions.composePopoutDraft(draftLocalId) if popout
+      @_finalizeAndPersistNewMessage(draft).then ({draftClientId}) =>
+        Actions.composePopoutDraft(draftClientId) if popout
 
 
   # Eventually we'll want a nicer solution for inline attachments
@@ -325,18 +318,18 @@ class DraftStore
       pristine: true
       accountId: account.id
 
-    @_finalizeAndPersistNewMessage(draft).then ({draftLocalId}) =>
-      @_onPopoutDraftLocalId(draftLocalId, {newDraft: true})
+    @_finalizeAndPersistNewMessage(draft).then ({draftClientId}) =>
+      @_onPopoutDraftClientId(draftClientId, {newDraft: true})
 
-  _onPopoutDraftLocalId: (draftLocalId, options = {}) =>
+  _onPopoutDraftClientId: (draftClientId, options = {}) =>
     return unless AccountStore.current()
 
-    if not draftLocalId?
-      throw new Error("DraftStore::onPopoutDraftLocalId - You must provide a draftLocalId")
+    if not draftClientId?
+      throw new Error("DraftStore::onPopoutDraftId - You must provide a draftClientId")
 
     save = Promise.resolve()
-    if @_draftSessions[draftLocalId]
-      save = @_draftSessions[draftLocalId].changes.commit()
+    if @_draftSessions[draftClientId]
+      save = @_draftSessions[draftClientId].changes.commit()
 
     title = if options.newDraft then "New Message" else "Message"
 
@@ -344,7 +337,7 @@ class DraftStore
       atom.newWindow
         title: title
         windowType: "composer"
-        windowProps: _.extend(options, {draftLocalId})
+        windowProps: _.extend(options, {draftClientId})
 
   _onHandleMailtoLink: (urlString) =>
     account = AccountStore.current()
@@ -374,32 +367,32 @@ class DraftStore
       if query[attr]
         draft[attr] = ContactStore.parseContactsInString(query[attr])
 
-    @_finalizeAndPersistNewMessage(draft).then ({draftLocalId}) =>
-      @_onPopoutDraftLocalId(draftLocalId)
+    @_finalizeAndPersistNewMessage(draft).then ({draftClientId}) =>
+      @_onPopoutDraftClientId({draftClientId})
 
-  _onDestroyDraft: (draftLocalId) =>
-    session = @_draftSessions[draftLocalId]
+  _onDestroyDraft: (draftClientId) =>
+    session = @_draftSessions[draftClientId]
 
     # Immediately reset any pending changes so no saves occur
     if session
       @_doneWithSession(session)
 
     # Queue the task to destroy the draft
-    Actions.queueTask(new DestroyDraftTask(draftLocalId: draftLocalId))
+    Actions.queueTask(new DestroyDraftTask(draftClientId: draftClientId))
 
     atom.close() if @_isPopout()
 
   # The user request to send the draft
-  _onSendDraft: (draftLocalId) =>
-    @_draftsSending[draftLocalId] = true
-    @trigger(draftLocalId)
+  _onSendDraft: (draftClientId) =>
+    @_draftsSending[draftClientId] = true
+    @trigger(draftClientId)
 
-    @sessionForLocalId(draftLocalId).then (session) =>
+    @sessionForClientId(draftClientId).then (session) =>
       @_runExtensionsBeforeSend(session)
 
       # Immediately save any pending changes so we don't save after sending
       session.changes.commit().then =>
-        task = new SendDraftTask(draftLocalId, {fromPopout: @_isPopout()})
+        task = new SendDraftTask(draftClientId, {fromPopout: @_isPopout()})
         Actions.queueTask(task)
         @_doneWithSession(session)
         atom.close() if @_isPopout()
@@ -413,8 +406,8 @@ class DraftStore
       continue unless extension.finalizeSessionBeforeSending
       extension.finalizeSessionBeforeSending(session)
 
-  _onRemoveFile: ({file, messageLocalId}) =>
-    @sessionForLocalId(messageLocalId).then (session) ->
+  _onRemoveFile: ({file, messageClientId}) =>
+    @sessionForClientId(messageClientId).then (session) ->
       files = _.clone(session.draft().files) ? []
       files = _.reject files, (f) -> f.id is file.id
       session.changes.add({files}, immediate: true)
diff --git a/src/flux/stores/file-download-store.coffee b/src/flux/stores/file-download-store.coffee
index 3024b75dd..cda4d0657 100644
--- a/src/flux/stores/file-download-store.coffee
+++ b/src/flux/stores/file-download-store.coffee
@@ -59,7 +59,7 @@ class Download
     return @promise if @promise
 
     @promise = new Promise (resolve, reject) =>
-      account = AccountStore.current()?.id
+      accountId = AccountStore.current()?.id
       stream = fs.createWriteStream(@targetPath)
       finished = false
       finishedAction = null
@@ -82,7 +82,7 @@ class Download
       NylasAPI.makeRequest
         json: false
         path: "/files/#{@fileId}/download"
-        accountId: account
+        accountId: accountId
         encoding: null # Tell `request` not to parse the response data
         started: (req) =>
           @request = req
diff --git a/src/flux/stores/file-upload-store.coffee b/src/flux/stores/file-upload-store.coffee
index 9bd3df920..850ef59b4 100644
--- a/src/flux/stores/file-upload-store.coffee
+++ b/src/flux/stores/file-upload-store.coffee
@@ -20,7 +20,7 @@ FileUploadStore = Reflux.createStore
     @listenTo Actions.fileAborted, @_onFileAborted
 
     # We don't save uploads to the DB, we keep it in memory in the store.
-    # The key is the messageLocalId. The value is a hash of paths and
+    # The key is the messageClientId. The value is a hash of paths and
     # corresponding upload data.
     @_fileUploads = {}
     @_linkedFiles = {}
@@ -28,18 +28,18 @@ FileUploadStore = Reflux.createStore
 
   ######### PUBLIC #######################################################
 
-  uploadsForMessage: (messageLocalId) ->
-    if not messageLocalId? then return []
+  uploadsForMessage: (messageClientId) ->
+    if not messageClientId? then return []
     _.filter @_fileUploads, (uploadData, uploadKey) ->
-      uploadData.messageLocalId is messageLocalId
+      uploadData.messageClientId is messageClientId
 
   linkedUpload: (file) -> @_linkedFiles[file.id]
 
 
   ########### PRIVATE ####################################################
 
-  _onAttachFile: ({messageLocalId}) ->
-    @_verifyId(messageLocalId)
+  _onAttachFile: ({messageClientId}) ->
+    @_verifyId(messageClientId)
 
     # When the dialog closes, it triggers `Actions.pathsToOpen`
     atom.showOpenDialog {properties: ['openFile', 'multiSelections']}, (pathsToOpen) ->
@@ -47,7 +47,7 @@ FileUploadStore = Reflux.createStore
       pathsToOpen = [pathsToOpen] if _.isString(pathsToOpen)
 
       pathsToOpen.forEach (path) ->
-        Actions.attachFilePath({messageLocalId, path})
+        Actions.attachFilePath({messageClientId, path})
 
   _onAttachFileError: (message) ->
     remote = require('remote')
@@ -58,8 +58,8 @@ FileUploadStore = Reflux.createStore
       message: 'Cannot Attach File',
       detail: message
 
-  _onAttachFilePath: ({messageLocalId, path}) ->
-    @_verifyId(messageLocalId)
+  _onAttachFilePath: ({messageClientId, path}) ->
+    @_verifyId(messageClientId)
     fs.stat path, (err, stats) =>
       filename = require('path').basename(path)
       if err
@@ -67,12 +67,12 @@ FileUploadStore = Reflux.createStore
       else if stats.isDirectory()
         @_onAttachFileError("#{filename} is a directory. Try compressing it and attaching it again.")
       else
-        Actions.queueTask(new FileUploadTask(path, messageLocalId))
+        Actions.queueTask(new FileUploadTask(path, messageClientId))
 
   # Receives:
   #   uploadData:
   #     uploadTaskId - A unique id
-  #     messageLocalId - The localId of the message (draft) we're uploading to
+  #     messageClientId - The clientId of the message (draft) we're uploading to
   #     filePath - The full absolute local system file path
   #     fileSize - The size in bytes
   #     fileName - The basename of the file
@@ -107,6 +107,6 @@ FileUploadStore = Reflux.createStore
     delete @_fileUploads[uploadData.uploadTaskId]
     @trigger()
 
-  _verifyId: (messageLocalId) ->
-    if messageLocalId.blank?
+  _verifyId: (messageClientId) ->
+    if messageClientId.blank?
       throw new Error "You need to pass the ID of the message (draft) this Action refers to"
diff --git a/src/flux/stores/focused-contacts-store.coffee b/src/flux/stores/focused-contacts-store.coffee
index 44d0d1c3f..0e6900736 100644
--- a/src/flux/stores/focused-contacts-store.coffee
+++ b/src/flux/stores/focused-contacts-store.coffee
@@ -1,19 +1,18 @@
 _ = require 'underscore'
-Reflux = require 'reflux'
 
 Utils = require '../models/utils'
 Actions = require '../actions'
+NylasStore = require 'nylas-store'
 MessageStore = require './message-store'
 AccountStore = require './account-store'
 FocusedContentStore = require './focused-content-store'
 
 # A store that handles the focuses collections of and individual contacts
-module.exports =
-FocusedContactsStore = Reflux.createStore
-  init: ->
+class FocusedContactsStore extends NylasStore
+  constructor: ->
     @listenTo Actions.focusContact, @_focusContact
-    @listenTo MessageStore, => @_onMessageStoreChanged()
-    @listenTo AccountStore, => @_onAccountChanged()
+    @listenTo MessageStore, @_onMessageStoreChanged
+    @listenTo AccountStore, @_onAccountChanged
     @listenTo FocusedContentStore, @_onFocusChanged
 
     @_currentThread = null
@@ -31,7 +30,7 @@ FocusedContactsStore = Reflux.createStore
     @_currentFocusedContact = null
     @trigger() unless silent
 
-  _onFocusChanged: (change) ->
+  _onFocusChanged: (change) =>
     return unless change.impactsCollection('thread')
     item = FocusedContentStore.focused('thread')
     return if @_currentThread?.id is item?.id
@@ -42,13 +41,13 @@ FocusedContactsStore = Reflux.createStore
     # We need to wait now for the MessageStore to grab all of the
     # appropriate messages for the given thread.
 
-  _onMessageStoreChanged: -> _.defer =>
+  _onMessageStoreChanged: =>
     if MessageStore.threadId() is @_currentThread?.id
       @_setCurrentParticipants()
     else
       @_clearCurrentParticipants()
 
-  _onAccountChanged: ->
+  _onAccountChanged: =>
     @_myEmail = (AccountStore.current()?.me().email ? "").toLowerCase().trim()
 
   # For now we take the last message
@@ -59,7 +58,7 @@ FocusedContactsStore = Reflux.createStore
     @_focusContact(@_currentContacts[0], silent: true)
     @trigger()
 
-  _focusContact: (contact, {silent}={}) ->
+  _focusContact: (contact, {silent}={}) =>
     return unless contact
     @_currentFocusedContact = contact
     @trigger() unless silent
@@ -113,3 +112,4 @@ FocusedContactsStore = Reflux.createStore
     theirDomain = _.last(email.split("@"))
     return myDomain.length > 0 and theirDomain.length > 0 and myDomain is theirDomain
 
+module.exports = new FocusedContactsStore
diff --git a/src/flux/stores/message-store.coffee b/src/flux/stores/message-store.coffee
index 0a593c652..2f3a1f480 100644
--- a/src/flux/stores/message-store.coffee
+++ b/src/flux/stores/message-store.coffee
@@ -31,8 +31,8 @@ class MessageStore extends NylasStore
     # this.state == nextState is always true if we modify objects in place.
     _.clone @_itemsExpanded
 
-  itemLocalIds: =>
-    _.clone @_itemsLocalIds
+  itemClientIds: ->
+    _.pluck(@_items, "clientId")
 
   itemsLoading: ->
     @_itemsLoading
@@ -71,7 +71,6 @@ class MessageStore extends NylasStore
   _setStoreDefaults: =>
     @_items = []
     @_itemsExpanded = {}
-    @_itemsLocalIds = {}
     @_itemsLoading = false
     @_thread = null
     @_extensions = []
@@ -89,20 +88,13 @@ class MessageStore extends NylasStore
       inDisplayedThread = _.some change.objects, (obj) => obj.threadId is @_thread.id
       if inDisplayedThread
 
-        # Are we most likely adding a new draft? If the item is a draft and we don't
-        # have it's local Id, optimistically add it to the set, resort, and trigger.
-        # Note: this can avoid 100msec+ of delay from "Reply" => composer onscreen,
         item = change.objects[0]
         itemAlreadyExists = _.some @_items, (msg) -> msg.id is item.id
         if change.objects.length is 1 and item.draft is true and not itemAlreadyExists
-          DatabaseStore.localIdForModel(item).then (localId) =>
-            @_itemsLocalIds[item.id] = localId
-            # We need to create a new copy of the items array so that the message-list
-            # can compare new state to previous state.
-            @_items = [].concat(@_items, [item])
-            @_items = @_sortItemsForDisplay(@_items)
-            @_expandItemsToDefault()
-            @trigger()
+          @_items = [].concat(@_items, [item])
+          @_items = @_sortItemsForDisplay(@_items)
+          @_expandItemsToDefault()
+          @trigger()
         else
           @_fetchFromCache()
 
@@ -144,61 +136,53 @@ class MessageStore extends NylasStore
     query.where(threadId: loadedThreadId, accountId: @_thread.accountId)
     query.include(Message.attributes.body)
     query.then (items) =>
-      localIds = {}
-      async.each items, (item, callback) ->
-        return callback() unless item.draft
-        DatabaseStore.localIdForModel(item).then (localId) ->
-          localIds[item.id] = localId
-          callback()
-      , =>
-        # Check to make sure that our thread is still the thread we were
-        # loading items for. Necessary because this takes a while.
-        return unless loadedThreadId is @_thread?.id
+      # Check to make sure that our thread is still the thread we were
+      # loading items for. Necessary because this takes a while.
+      return unless loadedThreadId is @_thread?.id
 
-        loaded = true
+      loaded = true
 
-        @_items = @_sortItemsForDisplay(items)
-        @_itemsLocalIds = localIds
+      @_items = @_sortItemsForDisplay(items)
 
-        # If no items were returned, attempt to load messages via the API. If items
-        # are returned, this will trigger a refresh here.
-        if @_items.length is 0
-          @_fetchMessages()
-          loaded = false
+      # If no items were returned, attempt to load messages via the API. If items
+      # are returned, this will trigger a refresh here.
+      if @_items.length is 0
+        @_fetchMessages()
+        loaded = false
 
-        @_expandItemsToDefault()
+      @_expandItemsToDefault()
 
-        # Download the attachments on expanded messages.
-        @_fetchExpandedAttachments(@_items)
+      # Download the attachments on expanded messages.
+      @_fetchExpandedAttachments(@_items)
 
-        # Check that expanded messages have bodies. We won't mark ourselves
-        # as loaded until they're all available. Note that items can be manually
-        # expanded so this logic must be separate from above.
-        if @_fetchExpandedBodies(@_items)
-          loaded = false
+      # Check that expanded messages have bodies. We won't mark ourselves
+      # as loaded until they're all available. Note that items can be manually
+      # expanded so this logic must be separate from above.
+      if @_fetchExpandedBodies(@_items)
+        loaded = false
 
-        # Normally, we would trigger often and let the view's
-        # shouldComponentUpdate decide whether to re-render, but if we
-        # know we're not ready, don't even bother.  Trigger once at start
-        # and once when ready. Many third-party stores will observe
-        # MessageStore and they'll be stupid and re-render constantly.
-        if loaded
-          # Mark the thread as read if necessary. Make sure it's still the
-          # current thread after the timeout.
+      # Normally, we would trigger often and let the view's
+      # shouldComponentUpdate decide whether to re-render, but if we
+      # know we're not ready, don't even bother.  Trigger once at start
+      # and once when ready. Many third-party stores will observe
+      # MessageStore and they'll be stupid and re-render constantly.
+      if loaded
+        # Mark the thread as read if necessary. Make sure it's still the
+        # current thread after the timeout.
 
-          # Override canBeUndone to return false so that we don't see undo prompts
-          # (since this is a passive action vs. a user-triggered action.)
-          if @_thread.unread
-            markAsReadDelay = atom.config.get('core.reading.markAsReadDelay')
-            setTimeout =>
-              return unless loadedThreadId is @_thread?.id
-              t = new ChangeUnreadTask(thread: @_thread, unread: false)
-              t.canBeUndone = => false
-              Actions.queueTask(t)
-            , markAsReadDelay
+        # Override canBeUndone to return false so that we don't see undo prompts
+        # (since this is a passive action vs. a user-triggered action.)
+        if @_thread.unread
+          markAsReadDelay = atom.config.get('core.reading.markAsReadDelay')
+          setTimeout =>
+            return unless loadedThreadId is @_thread?.id
+            t = new ChangeUnreadTask(thread: @_thread, unread: false)
+            t.canBeUndone = => false
+            Actions.queueTask(t)
+          , markAsReadDelay
 
-          @_itemsLoading = false
-          @trigger(@)
+        @_itemsLoading = false
+        @trigger(@)
 
   _fetchExpandedBodies: (items) ->
     startedAFetch = false
diff --git a/src/flux/stores/model-view.coffee b/src/flux/stores/model-view.coffee
index d7a61b0b8..c1716b9c1 100644
--- a/src/flux/stores/model-view.coffee
+++ b/src/flux/stores/model-view.coffee
@@ -92,7 +92,7 @@ class ModelView
     # "Total Refresh" - in a subclass, do something smarter
     @invalidateRetainedRange()
 
-  invalidateMetadataFor: (ids = []) ->
+  invalidateMetadataFor: ->
     # "Total Refresh" - in a subclass, do something smarter
     @invalidateRetainedRange()
 
diff --git a/src/flux/stores/task-queue.coffee b/src/flux/stores/task-queue.coffee
index 35c123b7a..e1455624b 100644
--- a/src/flux/stores/task-queue.coffee
+++ b/src/flux/stores/task-queue.coffee
@@ -1,7 +1,6 @@
 _ = require 'underscore'
 fs = require 'fs-plus'
 path = require 'path'
-{generateTempId} = require '../models/utils'
 
 {Listener, Publisher} = require '../modules/reflux-coffee'
 CoffeeHelpers = require '../coffee-helpers'
@@ -100,7 +99,7 @@ class TaskQueue
     {SaveDraftTask} or 'SaveDraftTask')
 
   - `matching`: Optional An {Object} with criteria to pass to _.isMatch. For a
-     SaveDraftTask, this could be {draftLocalId: "123123"}
+     SaveDraftTask, this could be {draftClientId: "123123"}
 
   Returns a matching {Task}, or null.
   ###
diff --git a/src/flux/tasks/change-folder-task.coffee b/src/flux/tasks/change-folder-task.coffee
index 39bd6ffed..509980fc6 100644
--- a/src/flux/tasks/change-folder-task.coffee
+++ b/src/flux/tasks/change-folder-task.coffee
@@ -9,7 +9,7 @@ ChangeMailTask = require './change-mail-task'
 # Public: Create a new task to apply labels to a message or thread.
 #
 # Takes an options array of the form:
-#   - `folder` The {Folder} or {Folder} id to move to
+#   - `folder` The {Folder} or {Folder} IDs to move to
 #   - `threads` An array of {Thread}s or {Thread} IDs
 #   - `threads` An array of {Message}s or {Message} IDs
 #   - `undoData` Since changing the folder is a destructive action,
diff --git a/src/flux/tasks/change-mail-task.coffee b/src/flux/tasks/change-mail-task.coffee
index e856994c8..5a11a569f 100644
--- a/src/flux/tasks/change-mail-task.coffee
+++ b/src/flux/tasks/change-mail-task.coffee
@@ -60,7 +60,7 @@ class ChangeMailTask extends Task
   # prepared the data they need and verified that requirements are met.
   #
   # Note: Currently, *ALL* subclasses must use `DatabaseStore.modelify`
-  # to convert `threads` and `messages` from models/ids to models.
+  # to convert `threads` and `messages` from models or ids to models.
   #
   performLocal: ->
     if @_isUndoTask and not @_restoreValues
diff --git a/src/flux/tasks/destroy-draft.coffee b/src/flux/tasks/destroy-draft.coffee
index b121815d5..e92ea0435 100644
--- a/src/flux/tasks/destroy-draft.coffee
+++ b/src/flux/tasks/destroy-draft.coffee
@@ -11,29 +11,29 @@ FileUploadTask = require './file-upload-task'
 
 module.exports =
 class DestroyDraftTask extends Task
-  constructor: ({@draftLocalId, @draftId} = {}) -> super
+  constructor: ({@draftClientId, @draftId} = {}) -> super
 
   shouldDequeueOtherTask: (other) ->
-    if @draftLocalId
-      (other instanceof DestroyDraftTask and other.draftLocalId is @draftLocalId) or
-      (other instanceof SyncbackDraftTask and other.draftLocalId is @draftLocalId) or
-      (other instanceof SendDraftTask and other.draftLocalId is @draftLocalId) or
-      (other instanceof FileUploadTask and other.messageLocalId is @draftLocalId)
+    if @draftClientId
+      (other instanceof DestroyDraftTask and other.draftClientId is @draftClientId) or
+      (other instanceof SyncbackDraftTask and other.draftClientId is @draftClientId) or
+      (other instanceof SendDraftTask and other.draftClientId is @draftClientId) or
+      (other instanceof FileUploadTask and other.messageClientId is @draftClientId)
     else if @draftId
-      (other instanceof DestroyDraftTask and other.draftLocalId is @draftLocalId)
+      (other instanceof DestroyDraftTask and other.draftClientId is @draftClientId)
     else
       false
 
   shouldWaitForTask: (other) ->
-    (other instanceof SyncbackDraftTask and other.draftLocalId is @draftLocalId)
+    (other instanceof SyncbackDraftTask and other.draftClientId is @draftClientId)
 
   performLocal: ->
-    if @draftLocalId
-      find = DatabaseStore.findByLocalId(Message, @draftLocalId)
+    if @draftClientId
+      find = DatabaseStore.findBy(Message, clientId: @draftClientId)
     else if @draftId
       find = DatabaseStore.find(Message, @draftId)
     else
-      return Promise.reject(new Error("Attempt to call DestroyDraftTask.performLocal without draftLocalId or draftId"))
+      return Promise.reject(new Error("Attempt to call DestroyDraftTask.performLocal without draftClientId"))
 
     find.then (draft) =>
       return Promise.resolve() unless draft
@@ -45,10 +45,10 @@ class DestroyDraftTask extends Task
     # when we performed locally, or if the draft has never been synced to
     # the server (id is still self-assigned)
     return Promise.resolve(Task.Status.Finished) unless @draft
-    return Promise.resolve(Task.Status.Finished) unless @draft.isSaved() and @draft.version?
+    return Promise.resolve(Task.Status.Finished) unless @draft.serverId and @draft.version?
 
     NylasAPI.makeRequest
-      path: "/drafts/#{@draft.id}"
+      path: "/drafts/#{@draft.serverId}"
       accountId: @draft.accountId
       method: "DELETE"
       body:
diff --git a/src/flux/tasks/file-upload-task.coffee b/src/flux/tasks/file-upload-task.coffee
index 9bf42edc9..5bc8e27bc 100644
--- a/src/flux/tasks/file-upload-task.coffee
+++ b/src/flux/tasks/file-upload-task.coffee
@@ -17,7 +17,7 @@ UploadCounter = 0
 
 class FileUploadTask extends Task
 
-  constructor: (@filePath, @messageLocalId) ->
+  constructor: (@filePath, @messageClientId) ->
     super
     @_startDate = Date.now()
     @_startId = UploadCounter
@@ -27,7 +27,7 @@ class FileUploadTask extends Task
 
   performLocal: ->
     return Promise.reject(new Error("Must pass an absolute path to upload")) unless @filePath?.length
-    return Promise.reject(new Error("Must be attached to a messageLocalId")) unless isTempId(@messageLocalId)
+    return Promise.reject(new Error("Must be attached to a messageClientId")) unless isTempId(@messageClientId)
     Actions.uploadStateChanged @_uploadData("pending")
     Promise.resolve()
 
@@ -97,7 +97,7 @@ class FileUploadTask extends Task
     Actions.linkFileToUpload(file: file, uploadData: @_uploadData("completed"))
 
     DraftStore = require '../stores/draft-store'
-    DraftStore.sessionForLocalId(@messageLocalId).then (session) =>
+    DraftStore.sessionForClientId(@messageClientId).then (session) =>
       files = _.clone(session.draft().files) ? []
       files.push(file)
       session.changes.add({files})
@@ -121,7 +121,7 @@ class FileUploadTask extends Task
         filename: @_uploadData().fileName
 
   # returns:
-  #   messageLocalId - The localId of the message (draft) we're uploading to
+  #   messageClientId - The clientId of the message (draft) we're uploading to
   #   filePath - The full absolute local system file path
   #   fileSize - The size in bytes
   #   fileName - The basename of the file
@@ -132,7 +132,7 @@ class FileUploadTask extends Task
       uploadTaskId: @id
       startDate: @_startDate
       startId: @_startId
-      messageLocalId: @messageLocalId
+      messageClientId: @messageClientId
       filePath: @filePath
       fileSize: @_getFileSize(@filePath)
       fileName: pathUtils.basename(@filePath)
diff --git a/src/flux/tasks/send-draft.coffee b/src/flux/tasks/send-draft.coffee
index de14d9f65..867cbf430 100644
--- a/src/flux/tasks/send-draft.coffee
+++ b/src/flux/tasks/send-draft.coffee
@@ -1,5 +1,3 @@
-{isTempId} = require '../models/utils'
-
 Actions = require '../actions'
 DatabaseStore = require '../stores/database-store'
 Message = require '../models/message'
@@ -13,40 +11,39 @@ NylasAPI = require '../nylas-api'
 module.exports =
 class SendDraftTask extends Task
 
-  constructor: (@draftLocalId, {@fromPopout}={}) ->
+  constructor: (@draftClientId, {@fromPopout}={}) ->
     super
 
   label: ->
     "Sending draft..."
 
   shouldDequeueOtherTask: (other) ->
-    other instanceof SendDraftTask and other.draftLocalId is @draftLocalId
+    other instanceof SendDraftTask and other.draftClientId is @draftClientId
 
   shouldWaitForTask: (other) ->
-    (other instanceof SyncbackDraftTask and other.draftLocalId is @draftLocalId) or
-    (other instanceof FileUploadTask and other.messageLocalId is @draftLocalId)
+    (other instanceof SyncbackDraftTask and other.draftClientId is @draftClientId) or
+    (other instanceof FileUploadTask and other.messageClientId is @draftClientId)
 
   performLocal: ->
     # When we send drafts, we don't update anything in the app until
     # it actually succeeds. We don't want users to think messages have
     # already sent when they haven't!
-    if not @draftLocalId
-      return Promise.reject(new Error("Attempt to call SendDraftTask.performLocal without @draftLocalId."))
+    if not @draftClientId
+      return Promise.reject(new Error("Attempt to call SendDraftTask.performLocal without @draftClientId."))
     Promise.resolve()
 
   performRemote: ->
     # Fetch the latest draft data to make sure we make the request with the most
     # recent draft version
-    DatabaseStore.findByLocalId(Message, @draftLocalId).then (draft) =>
+    DatabaseStore.findBy(Message, clientId: @draftClientId).then (draft) =>
       # The draft may have been deleted by another task. Nothing we can do.
-      NylasAPI.incrementOptimisticChangeCount(Message, draft.id)
       @draft = draft
       if not draft
         return Promise.reject(new Error("We couldn't find the saved draft."))
 
-      if draft.isSaved()
+      if draft.serverId
         body =
-          draft_id: draft.id
+          draft_id: draft.serverId
           version: draft.version
       else
         body = draft.toJSON()
@@ -68,13 +65,14 @@ class SendDraftTask extends Task
       message = (new Message).fromJSON(json)
       atom.playSound('mail_sent.ogg')
       Actions.sendDraftSuccess
-        draftLocalId: @draftLocalId
+        draftClientId: @draftClientId
         newMessage: message
-      DatabaseStore.unpersistModel(@draft).then =>
-        return Promise.resolve(Task.Status.Finished)
+      DestroyDraftTask = require './destroy-draft'
+      task = new DestroyDraftTask(draftClientId: @draftClientId)
+      Actions.queueTask(task)
+      return Promise.resolve(Task.Status.Finished)
 
     .catch APIError, (err) =>
-      NylasAPI.decrementOptimisticChangeCount(Message, @draft.id)
       if err.message?.indexOf('Invalid message public id') is 0
         body.reply_to_message_id = null
         return @_send(body)
@@ -84,7 +82,7 @@ class SendDraftTask extends Task
         return @_send(body)
       else if err.statusCode in NylasAPI.PermanentErrorCodes
         msg = err.message ? "Your draft could not be sent."
-        Actions.composePopoutDraft(@draftLocalId, {errorMessage: msg})
+        Actions.composePopoutDraft(@draftClientId, {errorMessage: msg})
         return Promise.resolve(Task.Status.Finished)
       else
         return Promise.resolve(Task.Status.Retry)
diff --git a/src/flux/tasks/syncback-draft.coffee b/src/flux/tasks/syncback-draft.coffee
index 4f7bd8587..c286bf121 100644
--- a/src/flux/tasks/syncback-draft.coffee
+++ b/src/flux/tasks/syncback-draft.coffee
@@ -1,5 +1,4 @@
 _ = require 'underscore'
-{isTempId, generateTempId} = require '../models/utils'
 
 Actions = require '../actions'
 DatabaseStore = require '../stores/database-store'
@@ -18,21 +17,21 @@ FileUploadTask = require './file-upload-task'
 module.exports =
 class SyncbackDraftTask extends Task
 
-  constructor: (@draftLocalId) ->
+  constructor: (@draftClientId) ->
     super
 
   shouldDequeueOtherTask: (other) ->
-    other instanceof SyncbackDraftTask and other.draftLocalId is @draftLocalId and other.creationDate < @creationDate
+    other instanceof SyncbackDraftTask and other.draftClientId is @draftClientId and other.creationDate < @creationDate
 
   shouldWaitForTask: (other) ->
-    other instanceof SyncbackDraftTask and other.draftLocalId is @draftLocalId and other.creationDate < @creationDate
+    other instanceof SyncbackDraftTask and other.draftClientId is @draftClientId and other.creationDate < @creationDate
 
   performLocal: ->
     # SyncbackDraftTask does not do anything locally. You should persist your changes
     # to the local database directly or using a DraftStoreProxy, and then queue a
     # SyncbackDraftTask to send those changes to the server.
-    if not @draftLocalId
-      errMsg = "Attempt to call SyncbackDraftTask.performLocal without @draftLocalId"
+    if not @draftClientId
+      errMsg = "Attempt to call SyncbackDraftTask.performLocal without @draftClientId"
       return Promise.reject(new Error(errMsg))
     Promise.resolve()
 
@@ -42,8 +41,8 @@ class SyncbackDraftTask extends Task
       return Promise.resolve() unless draft
       @checkDraftFromMatchesAccount(draft).then (draft) =>
 
-        if draft.isSaved()
-          path = "/drafts/#{draft.id}"
+        if draft.serverId
+          path = "/drafts/#{draft.serverId}"
           method = 'PUT'
         else
           path = "/drafts"
@@ -71,38 +70,31 @@ class SyncbackDraftTask extends Task
           # below. We currently have no way of locking between processes. Maybe a
           # log-style data structure would be better suited for drafts.
           #
-          @getLatestLocalDraft().then (draft) =>
-            updatedDraft = draft.clone()
-            updatedDraft.version = json.version
-            updatedDraft.id = json.id
-
-            if updatedDraft.id != draft.id
-              DatabaseStore.swapModel(oldModel: draft, newModel: updatedDraft, localId: @draftLocalId)
-            else
-              DatabaseStore.persistModel(updatedDraft)
+          @getLatestLocalDraft().then (draft) ->
+            draft.version = json.version
+            draft.serverId = json.id
+            DatabaseStore.persistModel(draft)
 
         .then =>
           return Promise.resolve(Task.Status.Finished)
 
         .catch APIError, (err) =>
           if err.statusCode in [400, 404, 409] and err.requestOptions.method is 'PUT'
-            return @getLatestLocalDraft().then (draft) =>
-              @detatchFromRemoteID(draft).then =>
-                Promise.resolve(Task.Status.Retry)
+            return Promise.resolve(Task.Status.Retry)
 
           if err.statusCode in NylasAPI.PermanentErrorCodes
             return Promise.resolve(Task.Status.Finished)
 
           return Promise.resolve(Task.Status.Retry)
 
-  getLatestLocalDraft: ->
-    DatabaseStore.findByLocalId(Message, @draftLocalId)
+  getLatestLocalDraft: =>
+    DatabaseStore.findBy(Message, clientId: @draftClientId)
 
-  checkDraftFromMatchesAccount: (existingAccountDraft) ->
-    DatabaseStore.findBy(Account, [Account.attributes.emailAddress.equal(existingAccountDraft.from[0].email)]).then (acct) =>
-      promise = Promise.resolve(existingAccountDraft)
+  checkDraftFromMatchesAccount: (draft) ->
+    DatabaseStore.findBy(Account, [Account.attributes.emailAddress.equal(draft.from[0].email)]).then (account) =>
+      promise = Promise.resolve(draft)
 
-      if existingAccountDraft.accountId isnt acct.id
+      if draft.accountId isnt account.id
         DestroyDraftTask = require './destroy-draft'
         destroy = new DestroyDraftTask(draftId: existingAccountDraft.id)
         promise = TaskQueueStatusStore.waitForPerformLocal(destroy).then =>
@@ -115,11 +107,11 @@ class SyncbackDraftTask extends Task
   detatchFromRemoteID: (draft, newAccountId = null) ->
     return Promise.resolve() unless draft
     newDraft = new Message(draft)
-    newDraft.id = generateTempId()
     newDraft.accountId = newAccountId if newAccountId
 
+    delete newDraft.serverId
+    delete newDraft.version
     delete newDraft.threadId
     delete newDraft.replyToMessageId
 
-    DatabaseStore.swapModel(oldModel: draft, newModel: newDraft, localId: @draftLocalId).then =>
-      Promise.resolve(newDraft)
+    DatabaseStore.persistModel(newDraft)
diff --git a/static/package-template/lib/my-composer-button.cjsx b/static/package-template/lib/my-composer-button.cjsx
index 573748cf6..3392af7a1 100644
--- a/static/package-template/lib/my-composer-button.cjsx
+++ b/static/package-template/lib/my-composer-button.cjsx
@@ -11,7 +11,7 @@ class MyComposerButton extends React.Component
   # reference to the draft, and you can look it up to perform
   # actions and retrieve data.
   @propTypes:
-    draftLocalId: React.PropTypes.string.isRequired
+    draftClientId: React.PropTypes.string.isRequired
 
   render: =>
     <div className="my-package">
@@ -24,7 +24,7 @@ class MyComposerButton extends React.Component
     # To retrieve information about the draft, we fetch the current editing
     # session from the draft store. We can access attributes of the draft
     # and add changes to the session which will be appear immediately.
-    DraftStore.sessionForLocalId(@props.draftLocalId).then (session) =>
+    DraftStore.sessionForClientId(@props.draftClientId).then (session) =>
       newSubject = "#{session.draft().subject} - It Worked!"
 
       dialog = @_getDialog()
@@ -40,4 +40,4 @@ class MyComposerButton extends React.Component
     require('remote').require('dialog')
 
 
-module.exports = MyComposerButton
\ No newline at end of file
+module.exports = MyComposerButton
diff --git a/static/package-template/spec/my-composer-button-spec.cjsx b/static/package-template/spec/my-composer-button-spec.cjsx
index 76f4f68ac..a02cfc370 100644
--- a/static/package-template/spec/my-composer-button-spec.cjsx
+++ b/static/package-template/spec/my-composer-button-spec.cjsx
@@ -9,7 +9,7 @@ dialogStub =
 describe "MyComposerButton", ->
   beforeEach ->
     @component = ReactTestUtils.renderIntoDocument(
-      <MyComposerButton draftLocalId="test" />
+      <MyComposerButton draftClientId="test" />
     )
 
   it "should render into the page", ->
@@ -22,4 +22,4 @@ describe "MyComposerButton", ->
     spyOn(@component, '_onClick')
     buttonNode = React.findDOMNode(@component.refs.button)
     ReactTestUtils.Simulate.click(buttonNode)
-    expect(@component._onClick).toHaveBeenCalled()
\ No newline at end of file
+    expect(@component._onClick).toHaveBeenCalled()