diff --git a/app/assets/javascripts/discourse/components/mount-widget.js.es6 b/app/assets/javascripts/discourse/components/mount-widget.js.es6
index dbdf7d84177..3b0c22f891b 100644
--- a/app/assets/javascripts/discourse/components/mount-widget.js.es6
+++ b/app/assets/javascripts/discourse/components/mount-widget.js.es6
@@ -2,6 +2,12 @@ import { diff, patch } from 'virtual-dom';
 import { WidgetClickHook } from 'discourse/widgets/click-hook';
 import { renderedKey } from 'discourse/widgets/widget';
 
+const _cleanCallbacks = {};
+export function addWidgetCleanCallback(widgetName, fn) {
+  _cleanCallbacks[widgetName] = _cleanCallbacks[widgetName] || [];
+  _cleanCallbacks[widgetName].push(fn);
+}
+
 export default Ember.Component.extend({
   _tree: null,
   _rootNode: null,
@@ -22,6 +28,13 @@ export default Ember.Component.extend({
     this._timeout = Ember.run.scheduleOnce('render', this, this.rerenderWidget);
   },
 
+  willClearRender() {
+    const callbacks = _cleanCallbacks[this.get('widget')];
+    if (callbacks) {
+      callbacks.forEach(cb => cb());
+    }
+  },
+
   willDestroyElement() {
     Ember.run.cancel(this._timeout);
   },
diff --git a/app/assets/javascripts/discourse/lib/plugin-api.js.es6 b/app/assets/javascripts/discourse/lib/plugin-api.js.es6
index 5995bd2b06f..073a078239d 100644
--- a/app/assets/javascripts/discourse/lib/plugin-api.js.es6
+++ b/app/assets/javascripts/discourse/lib/plugin-api.js.es6
@@ -4,6 +4,7 @@ import { addPosterIcon } from 'discourse/widgets/poster-name';
 import { addButton } from 'discourse/widgets/post-menu';
 import { includeAttributes } from 'discourse/lib/transform-post';
 import { addToolbarCallback } from 'discourse/components/d-editor';
+import { addWidgetCleanCallback } from 'discourse/components/mount-widget';
 
 let _decorateId = 0;
 function decorate(klass, evt, cb) {
@@ -29,23 +30,31 @@ class PluginApi {
   }
 
   /**
-   * decorateCooked(callback)
+   * decorateCooked(callback, options)
    *
    * Used for decorating the `cooked` content of a post after it is rendered using
    * jQuery.
    *
    * `callback` will be called when it is time to decorate with a jQuery selector.
    *
+   * Use `options.onlyStream` if you only want to decorate posts within a topic,
+   * and not in other places like the user stream.
+   *
    * For example, to add a yellow background to all posts you could do this:
    *
    * ```
    * api.decorateCooked($elem => $elem.css({ backgroundColor: 'yellow' }));
    * ```
    **/
-  decorateCooked(cb) {
-    addDecorator(cb);
-    decorate(ComposerEditor, 'previewRefreshed', cb);
-    decorate(this.container.lookupFactory('view:user-stream'), 'didInsertElement', cb);
+  decorateCooked(callback, opts) {
+    opts = opts || {};
+
+    addDecorator(callback);
+
+    if (!opts.onlyStream) {
+      decorate(ComposerEditor, 'previewRefreshed', callback);
+      decorate(this.container.lookupFactory('view:user-stream'), 'didInsertElement', callback);
+    }
   }
 
   /**
@@ -91,6 +100,10 @@ class PluginApi {
     addToolbarCallback(callback);
   }
 
+  cleanupStream(fn) {
+    addWidgetCleanCallback('post-stream', fn);
+  }
+
 }
 
 let _pluginv01;
diff --git a/app/assets/javascripts/discourse/widgets/post-cooked.js.es6 b/app/assets/javascripts/discourse/widgets/post-cooked.js.es6
index 34f8b088995..6aebe200e5c 100644
--- a/app/assets/javascripts/discourse/widgets/post-cooked.js.es6
+++ b/app/assets/javascripts/discourse/widgets/post-cooked.js.es6
@@ -10,10 +10,11 @@ export function addDecorator(cb) {
 
 export default class PostCooked {
 
-  constructor(attrs) {
+  constructor(attrs, getModel) {
     this.attrs = attrs;
     this.expanding = false;
     this._highlighted = false;
+    this.getModel = getModel;
   }
 
   update(prev) {
@@ -29,7 +30,7 @@ export default class PostCooked {
     this._fixImageSizes($html);
     this._applySearchHighlight($html);
 
-    _decorators.forEach(cb => cb($html));
+    _decorators.forEach(cb => cb($html, this.getModel));
     return $html[0];
   }
 
diff --git a/app/assets/javascripts/discourse/widgets/post.js.es6 b/app/assets/javascripts/discourse/widgets/post.js.es6
index f17c9904de6..ec36fe6f5b0 100644
--- a/app/assets/javascripts/discourse/widgets/post.js.es6
+++ b/app/assets/javascripts/discourse/widgets/post.js.es6
@@ -221,6 +221,17 @@ createWidget('expand-post-button', {
   }
 });
 
+class DecoratorHelper {
+  constructor(widget) {
+    this.container = widget.container;
+    this._widget = widget;
+  }
+
+  getModel() {
+    return this._widget.findAncestorModel();
+  }
+}
+
 createWidget('post-contents', {
   buildKey: attrs => `post-contents-${attrs.id}`,
 
@@ -240,7 +251,7 @@ createWidget('post-contents', {
   },
 
   html(attrs, state) {
-    const result = [new PostCooked(attrs)];
+    const result = [new PostCooked(attrs, new DecoratorHelper(this))];
 
     if (attrs.cooked_hidden) {
       result.push(this.attach('expand-hidden', attrs));
diff --git a/plugins/poll/assets/javascripts/initializers/extend-for-poll.js.es6 b/plugins/poll/assets/javascripts/initializers/extend-for-poll.js.es6
index f96da54dd7c..c27957849d9 100644
--- a/plugins/poll/assets/javascripts/initializers/extend-for-poll.js.es6
+++ b/plugins/poll/assets/javascripts/initializers/extend-for-poll.js.es6
@@ -1,12 +1,8 @@
-import PostView from "discourse/views/post";
-import TopicController from "discourse/controllers/topic";
-import Post from "discourse/models/post";
-
-import { on } from "ember-addons/ember-computed-decorators";
+import { withPluginApi } from 'discourse/lib/plugin-api';
 
 function createPollView(container, post, poll, vote) {
-  const controller = container.lookup("controller:poll", { singleton: false }),
-        view = container.lookup("view:poll");
+  const controller = container.lookup("controller:poll", { singleton: false });
+  const view = container.lookup("view:poll");
 
   controller.set("vote", vote);
   controller.setProperties({ model: poll, post });
@@ -15,88 +11,95 @@ function createPollView(container, post, poll, vote) {
   return view;
 }
 
+let _pollViews;
+
+function initializePolls(api) {
+
+  const TopicController = api.container.lookupFactory('controller:topic');
+  TopicController.reopen({
+    subscribe(){
+      this._super();
+      this.messageBus.subscribe("/polls/" + this.get("model.id"), msg => {
+        const post = this.get('model.postStream').findLoadedPost(msg.post_id);
+        if (post) {
+          post.set('polls', msg.polls);
+        }
+      });
+    },
+    unsubscribe(){
+      this.messageBus.unsubscribe('/polls/*');
+      this._super();
+    }
+  });
+
+  const Post = api.container.lookupFactory('model:post');
+  Post.reopen({
+    _polls: null,
+    pollsObject: null,
+
+    // we need a proper ember object so it is bindable
+    pollsChanged: function(){
+      const polls = this.get("polls");
+      if (polls) {
+        this._polls = this._polls || {};
+        _.map(polls, (v,k) => {
+          const existing = this._polls[k];
+          if (existing) {
+            this._polls[k].setProperties(v);
+          } else {
+            this._polls[k] = Em.Object.create(v);
+          }
+        });
+        this.set("pollsObject", this._polls);
+      }
+    }.observes("polls")
+  });
+
+  function cleanUpPollViews() {
+    if (_pollViews) {
+      Object.keys(_pollViews).forEach(pollName => _pollViews[pollName].destroy());
+    }
+    _pollViews = null;
+  }
+
+  function createPollViews($elem, helper) {
+    const $polls = $('.poll', $elem);
+    if (!$polls.length) { return; }
+
+    const post = helper.getModel();
+    const votes = post.get('polls_votes') || {};
+
+    post.pollsChanged();
+
+    const polls = post.get("pollsObject");
+    if (!polls) { return; }
+
+    cleanUpPollViews();
+    const postPollViews = {};
+
+    $polls.each((idx, pollElem) => {
+      const $div = $("<div>");
+      const $poll = $(pollElem);
+
+      const pollName = $poll.data("poll-name");
+      const pollView = createPollView(helper.container, post, polls[pollName], votes[pollName]);
+
+      $poll.replaceWith($div);
+      Em.run.next(() => pollView.renderer.replaceIn(pollView, $div[0]));
+      postPollViews[pollName] = pollView;
+    });
+
+    _pollViews = postPollViews;
+  }
+
+  api.decorateCooked(createPollViews, { onlyStream: true });
+  api.cleanupStream(cleanUpPollViews);
+}
+
 export default {
   name: "extend-for-poll",
 
-  initialize(container) {
-
-    Post.reopen({
-      // we need a proper ember object so it is bindable
-      pollsChanged: function(){
-        const polls  = this.get("polls");
-        if (polls) {
-          this._polls = this._polls || {};
-          _.map(polls, (v,k) => {
-            const existing = this._polls[k];
-            if (existing) {
-              this._polls[k].setProperties(v);
-            } else {
-              this._polls[k] = Em.Object.create(v);
-            }
-          });
-          this.set("pollsObject", this._polls);
-        }
-      }.observes("polls")
-    });
-
-    TopicController.reopen({
-      subscribe(){
-          this._super();
-          this.messageBus.subscribe("/polls/" + this.get("model.id"), msg => {
-            const post = this.get('model.postStream').findLoadedPost(msg.post_id);
-            if (post) {
-              post.set('polls', msg.polls);
-            }
-        });
-      },
-      unsubscribe(){
-        this.messageBus.unsubscribe('/polls/*');
-        this._super();
-      }
-    });
-
-    // overwrite polls
-    PostView.reopen({
-
-      @on("postViewInserted", "postViewUpdated")
-      _createPollViews($post) {
-        const post = this.get("post"),
-              votes = post.get("polls_votes") || {};
-
-        post.pollsChanged();
-        const polls = post.get("pollsObject");
-
-        // don't even bother when there's no poll
-        if (!polls) { return; }
-
-        // TODO inject cleanly into 
-
-        // clean-up if needed
-        this._cleanUpPollViews();
-
-        const pollViews = {};
-
-        // iterate over all polls
-        $(".poll", $post).each(function() {
-          const $div = $("<div>"),
-                $poll = $(this),
-                pollName = $poll.data("poll-name"),
-                pollView = createPollView(container, post, polls[pollName], votes[pollName]);
-
-          $poll.replaceWith($div);
-          Em.run.next(() => pollView.renderer.replaceIn(pollView, $div[0]));
-          pollViews[pollName] = pollView;
-        });
-
-        this.set("pollViews", pollViews);
-      },
-
-      @on("willClearRender")
-      _cleanUpPollViews() {
-        if (this.get("pollViews")) {
-          _.forEach(this.get("pollViews"), v => v.destroy());
-        }
-      }
-    });
+  initialize() {
+    withPluginApi('0.1', initializePolls);
   }
 };