diff --git a/papersurfer/mattermost.py b/papersurfer/mattermost.py
index a549ce6687d18e8e51b882c3a83887f7ab0cd71d..140ffc0f67f011bff75cf5d923d4830d9697f82f 100644
--- a/papersurfer/mattermost.py
+++ b/papersurfer/mattermost.py
@@ -9,16 +9,22 @@ from .doi import Doi
 class Mattermost:
     """Provide a simplified interaction w/ mattermost api."""
     def __init__(self, url, channelname, username, password):
-        self.msgs = []
-        self.mattermost = mattermostdriver.Driver({
+        self.posts = []
+        self._mattermost = mattermostdriver.Driver({
             'url': url,
             'login_id': username,
             'password': password,
             'port': 443
         })
 
+        self._loggedin = False
+        self._reporters = {}
+        self._channelname = channelname
+        self._channel = None
+
+    def _login(self):
         try:
-            self.mattermost.login()
+            self._mattermost.login()
         except (mattermostdriver.exceptions.NoAccessTokenProvided,
                 requests.exceptions.InvalidURL,
                 requests.exceptions.HTTPError):
@@ -26,16 +32,17 @@ class Mattermost:
             raise ConfigError
 
         try:
-            self.channel = self.get_channel(channelname)
+            self._channel = self._get_channel(self._channelname)
         except ConfigError:
             print("Couldn't find Mattermost channel.")
             raise ConfigError
-        self.reporters = {}
 
-    def get_channel(self, channelname):
+        self._loggedin = True
+
+    def _get_channel(self, channelname):
         """Try to find the paper channel by display name."""
-        teamapi = self.mattermost.teams
-        channelapi = self.mattermost.channels
+        teamapi = self._mattermost.teams
+        channelapi = self._mattermost.channels
         teams = [team["id"] for team in teamapi.get_user_teams("me")]
         channels = []
         for team in teams:
@@ -50,46 +57,42 @@ class Mattermost:
             raise ConfigError
         return channels[0]["id"]
 
-    def get_reporter(self, userid):
+    def _get_reporter(self, userid):
         """Load user from mattermost api and cache."""
-        userapi = self.mattermost.users
-        if userid not in self.reporters:
-            self.reporters[userid] = userapi.get_user(userid)["username"]
+        userapi = self._mattermost.users
+        if userid not in self._reporters:
+            self._reporters[userid] = userapi.get_user(userid)["username"]
 
-        return self.reporters[userid]
+        return self._reporters[userid]
 
-    def retrieve_all_messages(self):
-        """Retrieve all messages from mattermost, unfiltered for papers."""
-        posts = self.mattermost.posts.get_posts_for_channel(self.channel)
+    def _retrieve_all_posts(self):
+        """Retrieve all posts from mattermost, unfiltered for papers."""
+        posts = self._mattermost.posts.get_posts_for_channel(self._channel)
         return [PostDTO(id=m['id'], message=m['message'],
-                        reporter=self.get_reporter(m['user_id']),
+                        reporter=self._get_reporter(m['user_id']),
                         doi=Doi().extract_doi(m['message']),)
                 for m in posts['posts'].values()]
 
-    def filter_incoming(self, posts):
-        """Filter messages from mattermost to only papers."""
+    def _filter_incoming(self, posts):
+        """Filter posts from mattermost to only papers."""
         return [p for p in posts if "doi" in p.message]
 
     def retrieve(self):
         """Retrieve papers from mattermost channel."""
-        msgs = self.retrieve_all_messages()
-        self.msgs = self.filter_incoming(msgs)
-        return self.msgs
+        if not self._loggedin:
+            self._login()
+        posts = self._retrieve_all_posts()
+        self.posts = self._filter_incoming(posts)
+        return self.posts
 
     def check_doi_exits(self, doi):
         """Check for doi in current paper list."""
         doi_needle = Doi().extract_doi(doi)
-        msg_found = [msg for msg in self.msgs
-                     if Doi().extract_doi(msg.doi) == doi_needle]
-        return bool(msg_found)
-
-    def get_filtered(self, needle):
-        """Filter posts by needle."""
-        return [m for m in self.msgs
-                if needle.lower() in m.message.lower()
-                or needle.lower() in m.reporter.lower()]
+        posts_found = [posts for posts in self.posts
+                       if Doi().extract_doi(posts.doi) == doi_needle]
+        return bool(posts_found)
 
     def post(self, message):
         """Post message to thread."""
-        self.mattermost.posts.create_post({"channel_id": self.channel,
-                                           "message": message})
+        self._mattermost.posts.create_post({"channel_id": self._channel,
+                                            "message": message})
diff --git a/papersurfer/papersurfer.py b/papersurfer/papersurfer.py
index bb809b9692fb30353787b5593de9196561b60c5f..48a97c00d5e1a05f8a87903f22ec4e099c3b84a8 100644
--- a/papersurfer/papersurfer.py
+++ b/papersurfer/papersurfer.py
@@ -17,14 +17,91 @@ import sys
 import re
 import urwid
 import configargparse
+from tinydb import TinyDB, Query
 from .exceptions import ConfigError
 from .ui_elements import PrettyButton
 from .mattermost import Mattermost
 from .doi import Doi
 from .bibtex import Bibtex
+from .dtos import PostDTO
 
 
 class Papersurfer:
+    """Organize and cache paper/post data.
+
+    This handles interaction with mattermost, doi and a local database.
+
+    Papers and posts are similar but distinct concepts. A post contains
+    information on a single mattermost entry, containing a paper reference.
+    A paper contains information on a single scientific paper and a reference
+    back to the mattermost post.
+    """
+    def __init__(self, url, channelname, username, password):
+        self._filters = {
+            "needle": "",
+            "fromdate": None,
+            "untildate": None
+        }
+
+        self.mattermost = Mattermost(url, channelname, username, password)
+
+        self.db_path = "."
+
+        self.isLoggedIn = False
+        self.db_posts = None
+        self.db_papers = None
+        self.db_file_posts = "papersurfer_posts_db.json"
+        self.db_file_papers = "papersurfer_papers_db.json"
+
+    def load(self):
+        """Load data from mattermost and save to storage."""
+        self._connect_db()
+        posts = self.mattermost.retrieve()
+        self._update_db(posts=posts)
+
+    def _connect_db(self):
+        """Establish db connection. Noop if already connected."""
+        if not self.db_posts:
+            self.db_posts = TinyDB(f"{self.db_path}/{self.db_file_posts}")
+        if not self.db_papers:
+            self.db_papers = TinyDB(f"{self.db_path}/{self.db_file_papers}")
+
+    def _update_db(self, posts=[], papers=[]):
+        """"Merge new data into database."""
+        self._upsert_multiple(posts, self.db_posts)
+        self._upsert_multiple(papers, self.db_papers)
+
+    def _upsert_multiple(self, records, db):
+        """"Update record in db unless it exits, then insert.
+
+        Would be trivial if we could just change the unique id in tinydb to the
+        doi property, but we can't.
+        """
+        for record in records:
+            q = Query()
+            db.upsert(record.__dict__, q.doi == record.doi)
+
+    def get_posts(self):
+        """Get all posts in storage."""
+        self._connect_db()
+        return [PostDTO(p["id"], p["message"], p["reporter"], p["doi"])
+                for p in self.db_posts.all()]
+
+    def get_posts_filtered(self, needle=None):
+        """Return a list of papers, filtered by filter."""
+        self._filters['needle'] = needle = (needle
+                                            if needle
+                                            else self._filters['needle'])
+        return [m for m in self.get_posts()
+                if needle.lower() in m.message.lower()
+                or needle.lower() in m.reporter.lower()]
+
+    def get_papers(self):
+        """Get all papers in storage."""
+        return self.db_papers.all()
+
+
+class PapersurferUi:
     """Provide UI and interface with mattermost class."""
 
     _palette = [
@@ -39,9 +116,9 @@ class Papersurfer:
     ]
 
     def __init__(self, url, channel, username, password):
+        self.papersurfer = Papersurfer(url, channel, username, password)
         self._screen = urwid.raw_display.Screen()
         self.size = self._screen.get_cols_rows()
-        self.filter = ""
 
         ask = urwid.Edit(('I say', u"Filter?\n"))
         exitbutton = PrettyButton(u'Exit', on_press=self.on_exit_clicked)
@@ -51,8 +128,6 @@ class Papersurfer:
                                     on_press=self.open_submit_paper)
         div = urwid.Divider(u'-')
 
-        self.mtm = Mattermost(url, channel, username, password)
-
         body = [urwid.Text("")]
         self.listcontent = urwid.SimpleFocusListWalker(body)
 
@@ -84,6 +159,7 @@ class Papersurfer:
         self.mainloop = urwid.MainLoop(self._over, self._palette,
                                        unhandled_input=self.h_unhandled_input)
         self.mainloop.set_alarm_in(.1, self.load_list)
+        self.mainloop.set_alarm_in(.2, self.update_data)
         self.mainloop.run()
 
     def h_unhandled_input(self, key):
@@ -93,11 +169,18 @@ class Papersurfer:
 
     def load_list(self, _loop, _data):
         """Load and display paper list."""
-        body = [self.list_item(paper) for paper in self.mtm.retrieve()]
+        body = [self.list_item(post) for post in self.papersurfer.get_posts()]
+        if len(body) == 0:
+            return
         self.listcontent.clear()
         self.listcontent.extend(body)
         self.mainloop.widget = self.top
 
+    def update_data(self, _loop, _data):
+        """Load and display paper list."""
+        self.papersurfer.load()
+        self.mainloop.set_alarm_in(.1, self.load_list)
+
     def loading_indicator(self):
         """Create loading indicator dialog."""
         body_text = urwid.Text("Loading...", align='center')
@@ -179,20 +262,19 @@ class Papersurfer:
 
     def onchange(self, _, needle):
         """Handle filter change."""
-        self.filter = needle
         self.listcontent.clear()
-        self.listcontent.extend([self.list_item(paper, needle)
-                                 for paper in self.mtm.get_filtered(needle)])
+        self.listcontent.extend([
+            self.list_item(paper, needle)
+            for paper in self.papersurfer.get_posts_filtered(needle)])
 
     def running_export(self, state):
         """Set exporting state."""
-        btn = self.exportbutton
-        label = btn.get_label()
+        label = self.exportbutton.get_label()
         running_indicator = " (running...)"
         if state:
-            btn.set_label(label + running_indicator)
+            self.exportbutton.set_label(label + running_indicator)
         else:
-            btn.set_label(label.replace(running_indicator, ""))
+            self.exportbutton.set_label(label.replace(running_indicator, ""))
         self.updscrn()
 
     def on_exit_clicked(self, button):
@@ -207,7 +289,7 @@ class Papersurfer:
 
     def export_to_bibtex(self):
         """Export current filtered list to bibtex file."""
-        papers = self.mtm.get_filtered(self.filter)
+        papers = self.papersurfer.get_posts_filtered()
         dois = [paper.doi for paper in papers]
         string = Bibtex().bib_from_dois(dois)
         with open("export.bib", 'w') as file:
@@ -487,7 +569,7 @@ def main():
     if opt.dump_bibtex:
         just_bibtex(opt.url, opt.channel, opt.username, opt.password)
     else:
-        Papersurfer(opt.url, opt.channel, opt.username, opt.password)
+        PapersurferUi(opt.url, opt.channel, opt.username, opt.password)
 
 
 if __name__ == "__main__":
diff --git a/setup.py b/setup.py
index 0ad45200f20ad2652ff97a61f320f0c748cda4cc..5080f19ce789288f1131a4111543eb4ac5d7b945 100644
--- a/setup.py
+++ b/setup.py
@@ -15,7 +15,6 @@ setup(
     description="",
     long_description=README,
     long_description_content_type="text/markdown",
-    url="",
     author="Johann Jacobsohn",
     author_email="johann.jacobsohn@uni-hamburg.de",
     license="MIT",
@@ -23,10 +22,12 @@ setup(
         "License :: OSI Approved :: MIT License",
         "Programming Language :: Python :: 3",
         "Programming Language :: Python :: 3.7",
+        'Development Status :: 1 - Planning',
     ],
     packages=["papersurfer"],
     include_package_data=True,
-    install_requires=["requests", "mattermostdriver", "urwid", "configargparse"],
+    install_requires=["requests", "mattermostdriver", "urwid",
+                      "configargparse", "tinydb"],
     entry_points={
         "console_scripts": [
             "papersurfer=papersurfer.papersurfer:main",