diff --git a/.gitignore b/.gitignore index 2416d16..53d2a56 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ Thumbs.db .nbproject nbproject config.php +!app/helpers/* !app/models/* !app/controllers/* !app/templates/* diff --git a/.travis.yml b/.travis.yml index a5bd74a..91d6ddd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,4 +16,4 @@ before_script: - composer install script: - - phpunit -c tests/phpunit.unit.xml + - ./vendor/bin/phpunit -c tests/phpunit.unit.xml diff --git a/ChangeLog b/ChangeLog new file mode 100644 index 0000000..02287fb --- /dev/null +++ b/ChangeLog @@ -0,0 +1,18 @@ +Version 1.2.0 (unreleased) +------------- + +* Major change to the database structure to have a single database for multiple users +* Web access token for the cronjob +* New config parameter to disable web access for the cronjob +* Debug mode parameter is moved to the config file +* The console web page have been removed +* New API methods (not backward compatible) +* Fever API tokens are longer than before +* Add support for Wallabag service +* Add unit tests + +Migration procedure from 1.1.x to 1.2.0: + +To import your old database to the new database format, use this script: + + php scripts/migrate-db.php --sqlite-db=/path/to/my/db.sqlite --admin==1 diff --git a/Makefile b/Makefile index ee6f298..3b14703 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,10 @@ +.PHONY: archive .PHONY: docker-image .PHONY: docker-push .PHONY: docker-destroy .PHONY: docker-run -.PHONY: archive .PHONY: js +.PHONY: unit-test-sqlite JS_FILE = assets/js/all.js CONTAINER = miniflux @@ -36,3 +37,6 @@ $(JS_FILE): assets/js/app.js \ # Build a new archive: make archive version=1.2.3 dst=/tmp archive: @ git archive --format=zip --prefix=miniflux/ v${version} -o ${dst}/miniflux-${version}.zip + +unit-test-sqlite: + @ ./vendor/bin/phpunit -c tests/phpunit.unit.xml diff --git a/app/common.php b/app/common.php index 9dd5034..c5f36aa 100644 --- a/app/common.php +++ b/app/common.php @@ -6,25 +6,27 @@ if (file_exists(__DIR__.'/../config.php')) { require __DIR__.'/../config.php'; } -require __DIR__.'/constants.php'; -require __DIR__.'/check_setup.php'; -require __DIR__.'/functions.php'; +require_once __DIR__.'/constants.php'; +require_once __DIR__.'/check_setup.php'; +require_once __DIR__.'/functions.php'; PicoDb\Database::setInstance('db', function() { $db = new PicoDb\Database(array( 'driver' => 'sqlite', - 'filename' => Miniflux\Model\Database\get_path(), + 'filename' => DB_FILENAME, )); + $db->getStatementHandler()->withLogging(); + if ($db->schema('\Miniflux\Schema')->check(Miniflux\Schema\VERSION)) { return $db; - } - else { + } else { $errors = $db->getLogMessages(); + $nb_errors = count($errors); $html = 'Unable to migrate the database schema, please copy and paste this message and create a bug report:
'; $html .= '
';
-        $html .= (isset($errors[0]) ? $errors[0] : 'Unknown SQL error').PHP_EOL.PHP_EOL;
+        $html .= (isset($errors[$nb_errors - 1]) ? $errors[$nb_errors - 1] : 'Unknown SQL error').PHP_EOL.PHP_EOL;
         $html .= '- PHP version: '.phpversion().PHP_EOL;
         $html .= '- SAPI: '.php_sapi_name().PHP_EOL;
         $html .= '- PDO Sqlite version: '.phpversion('pdo_sqlite').PHP_EOL;
diff --git a/app/constants.php b/app/constants.php
index 18cdac5..0a39b49 100644
--- a/app/constants.php
+++ b/app/constants.php
@@ -2,6 +2,7 @@
 
 defined('APP_VERSION') or define('APP_VERSION', Miniflux\Helper\parse_app_version('$Format:%d$','$Format:%H$'));
 
+define('HTTP_USER_AGENT', 'Miniflux (https://miniflux.net)');
 defined('HTTP_TIMEOUT') or define('HTTP_TIMEOUT', 20);
 defined('HTTP_MAX_RESPONSE_SIZE') or define('HTTP_MAX_RESPONSE_SIZE', 2097152);
 
@@ -12,9 +13,9 @@ defined('DATA_DIRECTORY') or define('DATA_DIRECTORY', ROOT_DIRECTORY.DIRECTORY_S
 defined('FAVICON_DIRECTORY') or define('FAVICON_DIRECTORY', DATA_DIRECTORY.DIRECTORY_SEPARATOR.'favicons');
 defined('FAVICON_URL_PATH') or define('FAVICON_URL_PATH', 'data/favicons');
 
-defined('ENABLE_MULTIPLE_DB') or define('ENABLE_MULTIPLE_DB', true);
-defined('DB_FILENAME') or define('DB_FILENAME', 'db.sqlite');
+defined('DB_FILENAME') or define('DB_FILENAME', DATA_DIRECTORY.DIRECTORY_SEPARATOR.'db.sqlite');
 
+defined('DEBUG_MODE') or define('DEBUG_MODE', false);
 defined('DEBUG_FILENAME') or define('DEBUG_FILENAME', DATA_DIRECTORY.DIRECTORY_SEPARATOR.'debug.log');
 
 defined('THEME_DIRECTORY') or define('THEME_DIRECTORY', 'themes');
@@ -35,6 +36,7 @@ defined('SUBSCRIPTION_CONCURRENT_REQUESTS') or define('SUBSCRIPTION_CONCURRENT_R
 defined('RULES_DIRECTORY') or define('RULES_DIRECTORY', ROOT_DIRECTORY.DIRECTORY_SEPARATOR.'rules');
 
 defined('ENABLE_HSTS') or define('ENABLE_HSTS', true);
+defined('ENABLE_CRONJOB_HTTP_ACCESS') or define('ENABLE_CRONJOB_HTTP_ACCESS', true);
 
 defined('BEANSTALKD_HOST') or define('BEANSTALKD_HOST', '127.0.0.1');
 defined('BEANSTALKD_QUEUE') or define('BEANSTALKD_QUEUE', 'feeds');
diff --git a/app/controllers/about.php b/app/controllers/about.php
new file mode 100644
index 0000000..07e3020
--- /dev/null
+++ b/app/controllers/about.php
@@ -0,0 +1,22 @@
+getUserId();
+
+    Response\html(Template\layout('about', array(
+        'csrf'   => Helper\generate_csrf(),
+        'config' => Model\Config\get_all($user_id),
+        'user'   => Model\User\get_user_by_id_without_password($user_id),
+        'menu'   => 'config',
+        'title'  => t('About'),
+    )));
+});
diff --git a/app/controllers/api.php b/app/controllers/api.php
new file mode 100644
index 0000000..3dc68cb
--- /dev/null
+++ b/app/controllers/api.php
@@ -0,0 +1,20 @@
+getUserId();
+
+    Response\html(Template\layout('api', array(
+        'config' => Model\Config\get_all($user_id),
+        'user'   => Model\User\get_user_by_id_without_password($user_id),
+        'menu'   => 'config',
+        'title'  => t('Preferences'),
+    )));
+});
diff --git a/app/controllers/user.php b/app/controllers/auth.php
similarity index 78%
rename from app/controllers/user.php
rename to app/controllers/auth.php
index f0781ae..9ab62c2 100644
--- a/app/controllers/user.php
+++ b/app/controllers/auth.php
@@ -1,8 +1,13 @@
 flush();
+    SessionManager::close();
+    RememberMe\destroy();
     Response\redirect('?action=login');
 });
 
 // Display form login
 Router\get_action('login', function () {
-    if (Model\User\is_loggedin()) {
+    if (SessionStorage::getInstance()->isLogged()) {
         Response\redirect('?action=unread');
     }
 
@@ -25,8 +32,6 @@ Router\get_action('login', function () {
         'values' => array(
             'csrf' => Helper\generate_csrf(),
         ),
-        'databases' => Model\Database\get_list(),
-        'current_database' => Model\Database\select()
     )));
 });
 
@@ -43,7 +48,5 @@ Router\post_action('login', function () {
     Response\html(Template\load('login', array(
         'errors' => $errors,
         'values' => $values + array('csrf' => Helper\generate_csrf()),
-        'databases' => Model\Database\get_list(),
-        'current_database' => Model\Database\select()
     )));
 });
diff --git a/app/controllers/bookmark.php b/app/controllers/bookmark.php
index d220da7..6df4d7f 100644
--- a/app/controllers/bookmark.php
+++ b/app/controllers/bookmark.php
@@ -1,120 +1,127 @@
 getUserId();
+    $item_id = Request\param('id');
     $value = Request\int_param('value');
 
+    if ($value == 1) {
+        Service\sync($user_id, $item_id);
+    }
+
     Response\json(array(
-        'id' => $id,
+        'id' => $item_id,
         'value' => $value,
-        'result' => Model\Bookmark\set_flag($id, $value),
+        'result' => Model\Bookmark\set_flag($user_id, $item_id, $value),
     ));
 });
 
 // Add new bookmark
 Router\get_action('bookmark', function () {
-    $id = Request\param('id');
+    $user_id = SessionStorage::getInstance()->getUserId();
+    $item_id = Request\param('id');
     $menu = Request\param('menu');
     $redirect = Request\param('redirect', 'unread');
     $offset = Request\int_param('offset', 0);
     $feed_id = Request\int_param('feed_id', 0);
+    $value = Request\int_param('value');
 
-    Model\Bookmark\set_flag($id, Request\int_param('value'));
-
-    if ($redirect === 'show') {
-        Response\redirect('?action=show&menu='.$menu.'&id='.$id);
+    if ($value == 1) {
+        Service\sync($user_id, $item_id);
     }
 
-    Response\redirect('?action='.$redirect.'&offset='.$offset.'&feed_id='.$feed_id.'#item-'.$id);
+    Model\Bookmark\set_flag($user_id, $item_id, $value);
+
+    if ($redirect === 'show') {
+        Response\redirect('?action=show&menu='.$menu.'&id='.$item_id);
+    }
+
+    Response\redirect('?action='.$redirect.'&offset='.$offset.'&feed_id='.$feed_id.'#item-'.$item_id);
 });
 
 // Display bookmarks page
 Router\get_action('bookmarks', function () {
+    $user_id = SessionStorage::getInstance()->getUserId();
     $offset = Request\int_param('offset', 0);
     $group_id = Request\int_param('group_id', null);
     $feed_ids = array();
 
     if ($group_id !== null) {
-        $feed_ids = Model\Group\get_feeds_by_group($group_id);
+        $feed_ids = Model\Group\get_feed_ids_by_group($group_id);
     }
 
-    $nb_items = Model\Bookmark\count_items($feed_ids);
-    $items = Model\Bookmark\get_all_items(
+    $nb_items = Model\Bookmark\count_bookmarked_items($user_id, $feed_ids);
+    $items = Model\Bookmark\get_bookmarked_items(
+        $user_id,
         $offset,
-        Model\Config\get('items_per_page'),
+        Helper\config('items_per_page'),
         $feed_ids
     );
 
     Response\html(Template\layout('bookmarks', array(
-        'favicons' => Model\Favicon\get_item_favicons($items),
-        'original_marks_read' => Model\Config\get('original_marks_read'),
+        'favicons' => Model\Favicon\get_items_favicons($items),
+        'original_marks_read' => Helper\config('original_marks_read'),
         'order' => '',
         'direction' => '',
-        'display_mode' => Model\Config\get('items_display_mode'),
-        'item_title_link' => Model\Config\get('item_title_link'),
+        'display_mode' => Helper\config('items_display_mode'),
+        'item_title_link' => Helper\config('item_title_link'),
         'group_id' => $group_id,
         'items' => $items,
         'nb_items' => $nb_items,
         'offset' => $offset,
-        'items_per_page' => Model\Config\get('items_per_page'),
+        'items_per_page' => Helper\config('items_per_page'),
         'nothing_to_read' => Request\int_param('nothing_to_read'),
-        'nb_unread_items' => Model\Item\count_by_status('unread'),
         'menu' => 'bookmarks',
-        'groups' => Model\Group\get_all(),
+        'groups' => Model\Group\get_all($user_id),
         'title' => t('Bookmarks').' ('.$nb_items.')'
     )));
 });
 
 // Display bookmark feeds
 Router\get_action('bookmark-feed', function () {
-    // Select database if the parameter is set
-    $database = Request\param('database');
+    $token = Request\param('token');
+    $user = Model\User\get_user_by_token('feed_token', $token);
 
-    if (!empty($database)) {
-        Model\Database\select($database);
+    if (empty($user)) {
+        Response\text('Unauthorized', 401);
     }
 
-    // Check token
-    $feed_token = Model\Config\get('feed_token');
-    $request_token = Request\param('token');
-
-    if ($feed_token !== $request_token) {
-        Response\text('Access Forbidden', 403);
-    }
-
-    // Build Feed
-    $bookmarks = Model\Bookmark\get_all_items();
+    $bookmarks = Model\Bookmark\get_bookmarked_items($user['id']);
 
     $feedBuilder = AtomFeedBuilder::create()
         ->withTitle(t('Bookmarks').' - Miniflux')
-        ->withFeedUrl(Helper\get_current_base_url().'?action=bookmark-feed&token='.urlencode($feed_token))
+        ->withFeedUrl(Helper\get_current_base_url().'?action=bookmark-feed&token='.urlencode($user['feed_token']))
         ->withSiteUrl(Helper\get_current_base_url())
         ->withDate(new DateTime())
     ;
 
     foreach ($bookmarks as $bookmark) {
-        $article = Model\Item\get($bookmark['id']);
         $articleDate = new DateTime();
-        $articleDate->setTimestamp($article['updated']);
+        $articleDate->setTimestamp($bookmark['updated']);
 
         $feedBuilder
             ->withItem(AtomItemBuilder::create($feedBuilder)
-                ->withId($article['id'])
-                ->withTitle($article['title'])
-                ->withUrl($article['url'])
+                ->withId($bookmark['id'])
+                ->withTitle($bookmark['title'])
+                ->withUrl($bookmark['url'])
                 ->withUpdatedDate($articleDate)
                 ->withPublishedDate($articleDate)
-                ->withContent($article['content'])
+                ->withContent($bookmark['content'])
             );
     }
 
diff --git a/app/controllers/common.php b/app/controllers/common.php
index 71c067f..02a3d40 100644
--- a/app/controllers/common.php
+++ b/app/controllers/common.php
@@ -1,10 +1,12 @@
 isLogged() && ! in_array($action, $safe_actions)) {
         if (! Model\RememberMe\authenticate()) {
-            Model\User\logout();
             Response\redirect('?action=login');
         }
-    } elseif (Model\RememberMe\has_cookie()) {
-        Model\RememberMe\refresh();
     }
 
     // Load translations
-    $language = Model\Config\get('language') ?: 'en_US';
+    $language = Helper\config('language', 'en_US');
     Translator\load($language);
 
     // Set timezone
-    date_default_timezone_set(Model\Config\get('timezone') ?: 'UTC');
+    date_default_timezone_set(Helper\config('timezone', 'UTC'));
 
     // HTTP secure headers
     Response\csp(array(
@@ -64,13 +49,53 @@ Router\before(function ($action) {
     }
 });
 
-// Show help
-Router\get_action('show-help', function () {
-    Response\html(Template\load('show_help'));
-});
-
 // Image proxy (avoid SSL mixed content warnings)
 Router\get_action('proxy', function () {
     Handler\Proxy\download(rawurldecode(Request\param('url')));
     exit;
 });
+
+function items_list($status)
+{
+    $order = Request\param('order', 'updated');
+    $direction = Request\param('direction', Helper\config('items_sorting_direction'));
+    $offset = Request\int_param('offset', 0);
+    $group_id = Request\int_param('group_id', null);
+    $nb_items_page = Helper\config('items_per_page');
+    $user_id = SessionStorage::getInstance()->getUserId();
+    $feed_ids = array();
+
+    if ($group_id !== null) {
+        $feed_ids = Model\Group\get_feed_ids_by_group($group_id);
+    }
+
+    $items = Model\Item\get_items_by_status(
+        $user_id,
+        $status,
+        $feed_ids,
+        $offset,
+        $nb_items_page,
+        $order,
+        $direction
+    );
+
+    $nb_items = Model\Item\count_by_status($user_id, $status, $feed_ids);
+    $nb_unread_items = Model\Item\count_by_status($user_id, $status);
+
+    return array(
+        'nothing_to_read'     => Request\int_param('nothing_to_read'),
+        'favicons'            => Model\Favicon\get_items_favicons($items),
+        'original_marks_read' => Helper\bool_config('original_marks_read'),
+        'display_mode'        => Helper\config('items_display_mode'),
+        'item_title_link'     => Helper\config('item_title_link'),
+        'items_per_page'      => $nb_items_page,
+        'offset'              => $offset,
+        'direction'           => $direction,
+        'order'               => $order,
+        'items'               => $items,
+        'nb_items'            => $nb_items,
+        'nb_unread_items'     => $nb_unread_items,
+        'group_id'            => $group_id,
+        'groups'              => Model\Group\get_all($user_id),
+    );
+}
diff --git a/app/controllers/config.php b/app/controllers/config.php
index edce1c8..a72f400 100644
--- a/app/controllers/config.php
+++ b/app/controllers/config.php
@@ -1,65 +1,19 @@
  array(),
-            'values' => array(
-                'csrf' => Helper\generate_csrf(),
-            ),
-            'nb_unread_items' => Model\Item\count_by_status('unread'),
-            'menu' => 'config',
-            'title' => t('New database')
-        )));
-    }
-
-    Response\redirect('?action=database');
-});
-
-// Create a new database
-Router\post_action('new-db', function () {
-    if (ENABLE_MULTIPLE_DB) {
-        $values = Request\values();
-        Helper\check_csrf_values($values);
-        list($valid, $errors) = Validator\User\validate_creation($values);
-
-        if ($valid) {
-            if (Model\Database\create(strtolower($values['name']).'.sqlite', $values['username'], $values['password'])) {
-                Session\flash(t('Database created successfully.'));
-            } else {
-                Session\flash_error(t('Unable to create the new database.'));
-            }
-
-            Response\redirect('?action=database');
-        }
-
-        Response\html(Template\layout('new_db', array(
-            'errors' => $errors,
-            'values' => $values + array('csrf' => Helper\generate_csrf()),
-            'nb_unread_items' => Model\Item\count_by_status('unread'),
-            'menu' => 'config',
-            'title' => t('New database')
-        )));
-    }
-
-    Response\redirect('?action=database');
-});
-
 // Confirmation box before auto-update
 Router\get_action('confirm-auto-update', function () {
     Response\html(Template\layout('confirm_auto_update', array(
-        'nb_unread_items' => Model\Item\count_by_status('unread'),
         'menu' => 'config',
         'title' => t('Confirmation')
     )));
@@ -68,10 +22,10 @@ Router\get_action('confirm-auto-update', function () {
 // Auto-update
 Router\get_action('auto-update', function () {
     if (ENABLE_AUTO_UPDATE) {
-        if (Model\AutoUpdate\execute(Model\Config\get('auto_update_url'))) {
-            Session\flash(t('Miniflux is updated!'));
+        if (Model\AutoUpdate\execute(Helper\config('auto_update_url'))) {
+            SessionStorage::getInstance()->setFlashMessage(t('Miniflux is updated!'));
         } else {
-            Session\flash_error(t('Unable to update Miniflux, check the console for errors.'));
+            SessionStorage::getInstance()->setFlashErrorMessage(t('Unable to update Miniflux, check the console for errors.'));
         }
     }
 
@@ -80,35 +34,22 @@ Router\get_action('auto-update', function () {
 
 // Re-generate tokens
 Router\get_action('generate-tokens', function () {
+    $user_id = SessionStorage::getInstance()->getUserId();
+
     if (Helper\check_csrf(Request\param('csrf'))) {
-        Model\Config\new_tokens();
+        Model\User\regenerate_tokens($user_id);
     }
 
     Response\redirect('?action=config');
 });
 
-// Optimize the database manually
-Router\get_action('optimize-db', function () {
-    if (Helper\check_csrf(Request\param('csrf'))) {
-        Database::getInstance('db')->getConnection()->exec('VACUUM');
-    }
-
-    Response\redirect('?action=database');
-});
-
-// Download the compressed database
-Router\get_action('download-db', function () {
-    if (Helper\check_csrf(Request\param('csrf'))) {
-        Response\force_download('db.sqlite.gz');
-        Response\binary(gzencode(file_get_contents(Model\Database\get_path())));
-    }
-});
-
 // Display preferences page
 Router\get_action('config', function () {
+    $user_id = SessionStorage::getInstance()->getUserId();
+
     Response\html(Template\layout('config', array(
         'errors' => array(),
-        'values' => Model\Config\get_all() + array('csrf' => Helper\generate_csrf()),
+        'values' => Model\Config\get_all($user_id) + array('csrf' => Helper\generate_csrf()),
         'languages' => Model\Config\get_languages(),
         'timezones' => Model\Config\get_timezones(),
         'autoflush_read_options' => Model\Config\get_autoflush_read_options(),
@@ -119,7 +60,6 @@ Router\get_action('config', function () {
         'display_mode' => Model\Config\get_display_mode(),
         'item_title_link' => Model\Config\get_item_title_link(),
         'redirect_nothing_to_read_options' => Model\Config\get_nothing_to_read_redirections(),
-        'nb_unread_items' => Model\Item\count_by_status('unread'),
         'menu' => 'config',
         'title' => t('Preferences')
     )));
@@ -127,15 +67,16 @@ Router\get_action('config', function () {
 
 // Update preferences
 Router\post_action('config', function () {
-    $values = Request\values() + array('nocontent' => 0, 'image_proxy' => 0, 'favicons' => 0, 'debug_mode' => 0, 'original_marks_read' => 0);
+    $user_id = SessionStorage::getInstance()->getUserId();
+    $values = Request\values() + array('nocontent' => 0, 'image_proxy' => 0, 'favicons' => 0, 'original_marks_read' => 0);
     Helper\check_csrf_values($values);
     list($valid, $errors) = Validator\Config\validate_modification($values);
 
     if ($valid) {
-        if (Model\Config\save($values)) {
-            Session\flash(t('Your preferences are updated.'));
+        if (Model\Config\save($user_id, $values)) {
+            SessionStorage::getInstance()->setFlashMessage(t('Your preferences are updated.'));
         } else {
-            Session\flash_error(t('Unable to update your preferences.'));
+            SessionStorage::getInstance()->setFlashErrorMessage(t('Unable to update your preferences.'));
         }
 
         Response\redirect('?action=config');
@@ -143,7 +84,7 @@ Router\post_action('config', function () {
 
     Response\html(Template\layout('config', array(
         'errors' => $errors,
-        'values' => Model\Config\get_all() + array('csrf' => Helper\generate_csrf()),
+        'values' => Model\Config\get_all($user_id) + array('csrf' => Helper\generate_csrf()),
         'languages' => Model\Config\get_languages(),
         'timezones' => Model\Config\get_timezones(),
         'autoflush_read_options' => Model\Config\get_autoflush_read_options(),
@@ -154,7 +95,6 @@ Router\post_action('config', function () {
         'redirect_nothing_to_read_options' => Model\Config\get_nothing_to_read_redirections(),
         'display_mode' => Model\Config\get_display_mode(),
         'item_title_link' => Model\Config\get_item_title_link(),
-        'nb_unread_items' => Model\Item\count_by_status('unread'),
         'menu' => 'config',
         'title' => t('Preferences')
     )));
@@ -162,84 +102,17 @@ Router\post_action('config', function () {
 
 // Get configuration parameters (AJAX request)
 Router\post_action('get-config', function () {
+    $user_id = SessionStorage::getInstance()->getUserId();
     $return = array();
     $options = Request\values();
 
     if (empty($options)) {
-        $return = Model\Config\get_all();
+        $return = Model\Config\get_all($user_id);
     } else {
         foreach ($options as $name) {
-            $return[$name] = Model\Config\get($name);
+            $return[$name] = Helper\config($name);
         }
     }
 
     Response\json($return);
 });
-
-// Display help page
-Router\get_action('help', function () {
-    Response\html(Template\layout('help', array(
-        'config' => Model\Config\get_all(),
-        'nb_unread_items' => Model\Item\count_by_status('unread'),
-        'menu' => 'config',
-        'title' => t('Preferences')
-    )));
-});
-
-// Display about page
-Router\get_action('about', function () {
-    Response\html(Template\layout('about', array(
-        'csrf' => Helper\generate_csrf(),
-        'config' => Model\Config\get_all(),
-        'db_name' => Model\Database\select(),
-        'nb_unread_items' => Model\Item\count_by_status('unread'),
-        'menu' => 'config',
-        'title' => t('Preferences')
-    )));
-});
-
-// Display database page
-Router\get_action('database', function () {
-    Response\html(Template\layout('database', array(
-        'csrf' => Helper\generate_csrf(),
-        'config' => Model\Config\get_all(),
-        'db_size' => filesize(Model\Database\get_path()),
-        'nb_unread_items' => Model\Item\count_by_status('unread'),
-        'menu' => 'config',
-        'title' => t('Preferences')
-    )));
-});
-
-// Display API page
-Router\get_action('api', function () {
-    Response\html(Template\layout('api', array(
-        'config' => Model\Config\get_all(),
-        'nb_unread_items' => Model\Item\count_by_status('unread'),
-        'menu' => 'config',
-        'title' => t('Preferences')
-    )));
-});
-
-// Display bookmark services page
-Router\get_action('services', function () {
-    Response\html(Template\layout('services', array(
-        'errors' => array(),
-        'values' => Model\Config\get_all() + array('csrf' => Helper\generate_csrf()),
-        'menu' => 'config',
-        'title' => t('Preferences')
-    )));
-});
-
-// Update bookmark services
-Router\post_action('services', function () {
-    $values = Request\values() + array('pinboard_enabled' => 0, 'instapaper_enabled' => 0, 'wallabag_enabled' => 0);
-    Helper\check_csrf_values($values);
-
-    if (Model\Config\save($values)) {
-        Session\flash(t('Your preferences are updated.'));
-    } else {
-        Session\flash_error(t('Unable to update your preferences.'));
-    }
-
-    Response\redirect('?action=services');
-});
diff --git a/app/controllers/console.php b/app/controllers/console.php
deleted file mode 100644
index 3b6c3eb..0000000
--- a/app/controllers/console.php
+++ /dev/null
@@ -1,22 +0,0 @@
- @file_get_contents(DEBUG_FILENAME),
-        'nb_unread_items' => Model\Item\count_by_status('unread'),
-        'menu' => 'config',
-        'title' => t('Console')
-    )));
-});
diff --git a/app/controllers/feed.php b/app/controllers/feed.php
index c158cc6..9dff1d7 100644
--- a/app/controllers/feed.php
+++ b/app/controllers/feed.php
@@ -1,11 +1,12 @@
 getUserId();
+    Handler\Feed\update_feeds($user_id);
+    SessionStorage::getInstance()->setFlashErrorMessage(t('Your subscriptions are updated'));
     Response\redirect('?action=unread');
 });
 
 // Edit feed form
 Router\get_action('edit-feed', function () {
-    $id = Request\int_param('feed_id');
+    $user_id = SessionStorage::getInstance()->getUserId();
+    $feed_id = Request\int_param('feed_id');
 
-    $values = Model\Feed\get($id);
+    $values = Model\Feed\get_feed($user_id, $feed_id);
     $values += array(
-        'feed_group_ids' => Model\Group\get_feed_group_ids($id)
+        'feed_group_ids' => Model\Group\get_feed_group_ids($feed_id)
     );
 
     Response\html(Template\layout('edit_feed', array(
         'values' => $values,
         'errors' => array(),
-        'nb_unread_items' => Model\Item\count_by_status('unread'),
-        'groups' => Model\Group\get_all(),
+        'groups' => Model\Group\get_all($user_id),
         'menu' => 'feeds',
         'title' => t('Edit subscription')
     )));
@@ -39,32 +41,32 @@ Router\get_action('edit-feed', function () {
 
 // Submit edit feed form
 Router\post_action('edit-feed', function () {
+    $user_id = SessionStorage::getInstance()->getUserId();
     $values = Request\values();
     $values += array(
         'enabled' => 0,
         'download_content' => 0,
         'rtl' => 0,
         'cloak_referrer' => 0,
+        'parsing_error' => 0,
         'feed_group_ids' => array(),
-        'create_group' => ''
     );
 
     list($valid, $errors) = Validator\Feed\validate_modification($values);
 
     if ($valid) {
-        if (Model\Feed\update($values)) {
-            Session\flash(t('Your subscription has been updated.'));
+        if (Model\Feed\update_feed($user_id, $values['id'], $values)) {
+            SessionStorage::getInstance()->setFlashMessage(t('Your subscription has been updated.'));
             Response\redirect('?action=feeds');
         } else {
-            Session\flash_error(t('Unable to edit your subscription.'));
+            SessionStorage::getInstance()->setFlashErrorMessage(t('Unable to edit your subscription.'));
         }
     }
 
     Response\html(Template\layout('edit_feed', array(
         'values' => $values,
         'errors' => $errors,
-        'nb_unread_items' => Model\Item\count_by_status('unread'),
-        'groups' => Model\Group\get_all(),
+        'groups' => Model\Group\get_all($user_id),
         'menu' => 'feeds',
         'title' => t('Edit subscription')
     )));
@@ -72,11 +74,11 @@ Router\post_action('edit-feed', function () {
 
 // Confirmation box to remove a feed
 Router\get_action('confirm-remove-feed', function () {
-    $id = Request\int_param('feed_id');
+    $user_id = SessionStorage::getInstance()->getUserId();
+    $feed_id = Request\int_param('feed_id');
 
     Response\html(Template\layout('confirm_remove_feed', array(
-        'feed' => Model\Feed\get($id),
-        'nb_unread_items' => Model\Item\count_by_status('unread'),
+        'feed' => Model\Feed\get_feed($user_id, $feed_id),
         'menu' => 'feeds',
         'title' => t('Confirmation')
     )));
@@ -84,12 +86,13 @@ Router\get_action('confirm-remove-feed', function () {
 
 // Remove a feed
 Router\get_action('remove-feed', function () {
-    $id = Request\int_param('feed_id');
+    $user_id = SessionStorage::getInstance()->getUserId();
+    $feed_id = Request\int_param('feed_id');
 
-    if ($id && Model\Feed\remove($id)) {
-        Session\flash(t('This subscription has been removed successfully.'));
+    if (Model\Feed\remove_feed($user_id, $feed_id)) {
+        SessionStorage::getInstance()->setFlashMessage(t('This subscription has been removed successfully.'));
     } else {
-        Session\flash_error(t('Unable to remove this subscription.'));
+        SessionStorage::getInstance()->setFlashErrorMessage(t('Unable to remove this subscription.'));
     }
 
     Response\redirect('?action=feeds');
@@ -97,40 +100,43 @@ Router\get_action('remove-feed', function () {
 
 // Refresh one feed and redirect to unread items
 Router\get_action('refresh-feed', function () {
+    $user_id = SessionStorage::getInstance()->getUserId();
     $feed_id = Request\int_param('feed_id');
     $redirect = Request\param('redirect', 'unread');
 
-    Model\Feed\refresh($feed_id);
+    Handler\Feed\update_feed($user_id, $feed_id);
     Response\redirect('?action='.$redirect.'&feed_id='.$feed_id);
 });
 
 // Ajax call to refresh one feed
 Router\post_action('refresh-feed', function () {
+    $user_id = SessionStorage::getInstance()->getUserId();
     $feed_id = Request\int_param('feed_id', 0);
 
     Response\json(array(
         'feed_id' => $feed_id,
-        'result' => Model\Feed\refresh($feed_id),
-        'items_count' => Model\Feed\count_items($feed_id),
+        'result' => Handler\Feed\update_feed($user_id, $feed_id),
+        'items_count' => Model\ItemFeed\count_items_by_status($user_id, $feed_id),
     ));
 });
 
 // Display all feeds
 Router\get_action('feeds', function () {
+    $user_id = SessionStorage::getInstance()->getUserId();
     $nothing_to_read = Request\int_param('nothing_to_read');
-    $nb_unread_items = Model\Item\count_by_status('unread');
+    $nb_unread_items = Model\Item\count_by_status($user_id, 'unread');
+    $feeds = Model\Feed\get_feeds_with_items_count($user_id);
 
-    // possible with remember me function
     if ($nothing_to_read === 1 && $nb_unread_items > 0) {
         Response\redirect('?action=unread');
     }
 
     Response\html(Template\layout('feeds', array(
-        'favicons' => Model\Favicon\get_all_favicons(),
-        'feeds' => Model\Feed\get_all_item_counts(),
+        'favicons' => Model\Favicon\get_feeds_favicons($feeds),
+        'feeds' => $feeds,
         'nothing_to_read' => $nothing_to_read,
         'nb_unread_items' => $nb_unread_items,
-        'nb_failed_feeds' => Model\Feed\count_failed_feeds(),
+        'nb_failed_feeds' => Model\Feed\count_failed_feeds($user_id),
         'menu' => 'feeds',
         'title' => t('Subscriptions')
     )));
@@ -138,21 +144,21 @@ Router\get_action('feeds', function () {
 
 // Display form to add one feed
 Router\get_action('add', function () {
+    $user_id = SessionStorage::getInstance()->getUserId();
     $values = array(
         'download_content' => 0,
-        'rtl' => 0,
-        'cloak_referrer' => 0,
-        'create_group' => '',
-        'feed_group_ids' => array()
+        'rtl'              => 0,
+        'cloak_referrer'   => 0,
+        'create_group'     => '',
+        'feed_group_ids'   => array(),
     );
 
     Response\html(Template\layout('add', array(
         'values' => $values + array('csrf' => Helper\generate_csrf()),
         'errors' => array(),
-        'nb_unread_items' => Model\Item\count_by_status('unread'),
-        'groups' => Model\Group\get_all(),
-        'menu' => 'feeds',
-        'title' => t('New subscription')
+        'groups' => Model\Group\get_all($user_id),
+        'menu'   => 'feeds',
+        'title'  => t('New subscription'),
     )));
 });
 
@@ -162,100 +168,49 @@ Router\action('subscribe', function () {
         $values = Request\values();
         Helper\check_csrf_values($values);
         $url = isset($values['url']) ? $values['url'] : '';
+        $user_id = SessionStorage::getInstance()->getUserId();
     } else {
-        $values = array();
         $url = Request\param('url');
         $token = Request\param('token');
+        $user = Model\User\get_user_by_token('bookmarklet_token', $token);
+        $values = array();
 
-        if ($token !== Model\Config\get('bookmarklet_token')) {
-            Response\text('Access Forbidden', 403);
+        if (empty($user)) {
+            Response\text('Unauthorized', 401);
         }
+
+        $user_id = $user['id'];
     }
 
     $values += array(
-        'url' => trim($url),
+        'url'              => trim($url),
         'download_content' => 0,
-        'rtl' => 0,
-        'cloak_referrer' => 0,
-        'create_group' => '',
-        'feed_group_ids' => array()
+        'rtl'              => 0,
+        'cloak_referrer'   => 0,
+        'feed_group_ids'   => array(),
     );
 
-    try {
-        $feed_id = Model\Feed\create(
-            $values['url'],
-            $values['download_content'],
-            $values['rtl'],
-            $values['cloak_referrer'],
-            $values['feed_group_ids'],
-            $values['create_group']
-        );
-    } catch (UnexpectedValueException $e) {
-        $error_message = t('This subscription already exists.');
-    } catch (PicoFeed\Client\InvalidCertificateException $e) {
-        $error_message = t('Invalid SSL certificate.');
-    } catch (PicoFeed\Client\InvalidUrlException $e) {
-        $error_message = $e->getMessage();
-    } catch (PicoFeed\Client\MaxRedirectException $e) {
-        $error_message = t('Maximum number of HTTP redirections exceeded.');
-    } catch (PicoFeed\Client\MaxSizeException $e) {
-        $error_message = t('The content size exceeds to maximum allowed size.');
-    } catch (PicoFeed\Client\TimeoutException $e) {
-        $error_message = t('Connection timeout.');
-    } catch (PicoFeed\Parser\MalformedXmlException $e) {
-        $error_message = t('Feed is malformed.');
-    } catch (PicoFeed\Reader\SubscriptionNotFoundException $e) {
-        $error_message = t('Unable to find a subscription.');
-    } catch (PicoFeed\Reader\UnsupportedFeedFormatException $e) {
-        $error_message = t('Unable to detect the feed format.');
-    }
+    list($feed_id, $error_message) = Handler\Feed\create_feed(
+        $user_id,
+        $values['url'],
+        $values['download_content'],
+        $values['rtl'],
+        $values['cloak_referrer'],
+        $values['feed_group_ids'],
+        $values['groups']
+    );
 
-    Model\Config\write_debug();
-
-    if (isset($feed_id) && $feed_id !== false) {
-        Session\flash(t('Subscription added successfully.'));
+    if ($feed_id >= 1) {
+        SessionStorage::getInstance()->setFlashMessage(t('Subscription added successfully.'));
         Response\redirect('?action=feed-items&feed_id='.$feed_id);
     } else {
-        if (! isset($error_message)) {
-            $error_message = t('Error occured.');
-        }
-
-        Session\flash_error($error_message);
+        SessionStorage::getInstance()->setFlashErrorMessage($error_message);
     }
 
     Response\html(Template\layout('add', array(
         'values' => $values + array('csrf' => Helper\generate_csrf()),
-        'nb_unread_items' => Model\Item\count_by_status('unread'),
-        'groups' => Model\Group\get_all(),
-        'menu' => 'feeds',
-        'title' => t('Subscriptions')
+        'groups' => Model\Group\get_all($user_id),
+        'menu'   => 'feeds',
+        'title'  => t('Subscriptions'),
     )));
 });
-
-// OPML export
-Router\get_action('export', function () {
-    Response\force_download('feeds.opml');
-    Response\xml(Handler\Opml\export_all_feeds());
-});
-
-// OPML import form
-Router\get_action('import', function () {
-    Response\html(Template\layout('import', array(
-        'errors' => array(),
-        'nb_unread_items' => Model\Item\count_by_status('unread'),
-        'menu' => 'feeds',
-        'title' => t('OPML Import')
-    )));
-});
-
-// OPML importation
-Router\post_action('import', function () {
-    try {
-        Model\Feed\import_opml(Request\file_content('file'));
-        Session\flash(t('Your feeds have been imported.'));
-        Response\redirect('?action=feeds');
-    } catch (MalformedXmlException $e) {
-        Session\flash_error(t('Unable to import your OPML file.').' ('.$e->getMessage().')');
-        Response\redirect('?action=import');
-    }
-});
diff --git a/app/controllers/help.php b/app/controllers/help.php
new file mode 100644
index 0000000..c7c5c9e
--- /dev/null
+++ b/app/controllers/help.php
@@ -0,0 +1,26 @@
+getUserId();
+
+    Response\html(Template\layout('help', array(
+        'config' => Model\Config\get_all($user_id),
+        'menu' => 'config',
+        'title' => t('Preferences')
+    )));
+});
+
+// Show help
+Router\get_action('show-help', function () {
+    Response\html(Template\load('show_help'));
+});
+
diff --git a/app/controllers/history.php b/app/controllers/history.php
index bd9c865..690583f 100644
--- a/app/controllers/history.php
+++ b/app/controllers/history.php
@@ -1,62 +1,30 @@
  Model\Favicon\get_item_favicons($items),
-        'original_marks_read' => Model\Config\get('original_marks_read'),
-        'items' => $items,
-        'order' => $order,
-        'direction' => $direction,
-        'display_mode' => Model\Config\get('items_display_mode'),
-        'item_title_link' => Model\Config\get('item_title_link'),
-        'group_id' => $group_id,
-        'nb_items' => $nb_items,
-        'nb_unread_items' => Model\Item\count_by_status('unread'),
-        'offset' => $offset,
-        'items_per_page' => Model\Config\get('items_per_page'),
-        'nothing_to_read' => Request\int_param('nothing_to_read'),
-        'menu' => 'history',
-        'groups' => Model\Group\get_all(),
-        'title' => t('History').' ('.$nb_items.')'
+    Response\html(Template\layout('history', $params + array(
+        'title' => t('History') . ' (' . $params['nb_items'] . ')',
+        'menu'  => 'history',
     )));
 });
 
 // Confirmation box to flush history
 Router\get_action('confirm-flush-history', function () {
-    $group_id = Request\int_param('group_id', null);
+    $group_id = Request\int_param('group_id');
     
     Response\html(Template\layout('confirm_flush_items', array(
         'group_id' => $group_id,
-        'nb_unread_items' => Model\Item\count_by_status('unread'),
         'menu' => 'history',
         'title' => t('Confirmation')
     )));
@@ -64,12 +32,13 @@ Router\get_action('confirm-flush-history', function () {
 
 // Flush history
 Router\get_action('flush-history', function () {
-    $group_id = Request\int_param('group_id', null);
+    $user_id = SessionStorage::getInstance()->getUserId();
+    $group_id = Request\int_param('group_id');
     
-    if ($group_id !== null) {
-        Model\ItemGroup\mark_all_as_removed($group_id);
+    if ($group_id !== 0) {
+        Model\ItemGroup\change_items_status($user_id, $group_id, Model\Item\STATUS_READ, Model\Item\STATUS_REMOVED);
     } else {
-        Model\Item\mark_all_as_removed();
+        Model\Item\change_items_status($user_id, Model\Item\STATUS_READ, Model\Item\STATUS_REMOVED);
     }
     
     Response\redirect('?action=history');
diff --git a/app/controllers/item.php b/app/controllers/item.php
index eb3967e..53c2010 100644
--- a/app/controllers/item.php
+++ b/app/controllers/item.php
@@ -1,101 +1,73 @@
 getUserId();
 
-    $order = Request\param('order', 'updated');
-    $direction = Request\param('direction', Model\Config\get('items_sorting_direction'));
-    $offset = Request\int_param('offset', 0);
-    $group_id = Request\int_param('group_id', null);
-    $feed_ids = array();
+    Model\Item\autoflush_read($user_id);
+    Model\Item\autoflush_unread($user_id);
 
-    if ($group_id !== null) {
-        $feed_ids = Model\Group\get_feeds_by_group($group_id);
-    }
+    $params = items_list(Model\Item\STATUS_UNREAD);
 
-    $items = Model\Item\get_all_by_status(
-        'unread',
-        $feed_ids,
-        $offset,
-        Model\Config\get('items_per_page'),
-        $order,
-        $direction
-    );
-
-    $nb_items = Model\Item\count_by_status('unread', $feed_ids);
-    $nb_unread_items = Model\Item\count_by_status('unread');
-
-    if ($nb_unread_items === 0) {
-        $action = Model\Config\get('redirect_nothing_to_read');
+    if ($params['nb_unread_items'] === 0) {
+        $action = Helper\config('redirect_nothing_to_read', 'feeds');
         Response\redirect('?action='.$action.'¬hing_to_read=1');
     }
 
-    Response\html(Template\layout('unread_items', array(
-        'favicons' => Model\Favicon\get_item_favicons($items),
-        'original_marks_read' => Model\Config\get('original_marks_read'),
-        'order' => $order,
-        'direction' => $direction,
-        'display_mode' => Model\Config\get('items_display_mode'),
-        'item_title_link' => Model\Config\get('item_title_link'),
-        'group_id' => $group_id,
-        'items' => $items,
-        'nb_items' => $nb_items,
-        'nb_unread_items' => $nb_unread_items,
-        'offset' => $offset,
-        'items_per_page' => Model\Config\get('items_per_page'),
-        'title' => 'Miniflux ('.$nb_items.')',
-        'menu' => 'unread',
-        'groups' => Model\Group\get_all()
+    Response\html(Template\layout('unread_items', $params + array(
+        'title' => 'Miniflux (' . $params['nb_items'] . ')',
+        'menu'  => 'unread',
     )));
 });
 
 // Show item
 Router\get_action('show', function () {
-    $id = Request\param('id');
+    $user_id = SessionStorage::getInstance()->getUserId();
+    $item_id = Request\param('id');
     $menu = Request\param('menu');
-    $item = Model\Item\get($id);
-    $feed = Model\Feed\get($item['feed_id']);
+    $item = Model\Item\get_item($user_id, $item_id);
+    $feed = Model\Feed\get_feed($user_id, $item['feed_id']);
     $group_id = Request\int_param('group_id', null);
 
-    Model\Item\set_read($id);
+    Model\Item\change_item_status($user_id, $item_id, Model\Item\STATUS_READ);
     $item['status'] = 'read';
 
     switch ($menu) {
         case 'unread':
-            $nav = Model\Item\get_nav($item, array('unread'), array(1, 0), null, $group_id);
+            $nav = Model\Item\get_item_nav($user_id, $item, array('unread'), array(1, 0), null, $group_id);
             break;
         case 'history':
-            $nav = Model\Item\get_nav($item, array('read'));
+            $nav = Model\Item\get_item_nav($user_id, $item, array('read'));
             break;
         case 'feed-items':
-            $nav = Model\Item\get_nav($item, array('unread', 'read'), array(1, 0), $item['feed_id']);
+            $nav = Model\Item\get_item_nav($user_id, $item, array('unread', 'read'), array(1, 0), $item['feed_id']);
             break;
         case 'bookmarks':
-            $nav = Model\Item\get_nav($item, array('unread', 'read'), array(1));
+            $nav = Model\Item\get_item_nav($user_id, $item, array('unread', 'read'), array(1));
             break;
     }
 
-    $image_proxy = (bool) Model\Config\get('image_proxy');
+    $image_proxy = (bool) Helper\config('image_proxy');
 
     // add the image proxy if requested and required
     $item['content'] = Handler\Proxy\rewrite_html($item['content'], $item['url'], $image_proxy, $feed['cloak_referrer']);
 
     if ($image_proxy && strpos($item['enclosure_type'], 'image') === 0) {
-        $item['enclosure'] = Handler\Proxy\rewrite_link($item['enclosure']);
+        $item['enclosure_url'] = Handler\Proxy\rewrite_link($item['enclosure_url']);
     }
 
     Response\html(Template\layout('show_item', array(
-        'nb_unread_items' => Model\Item\count_by_status('unread'),
         'item' => $item,
         'feed' => $feed,
         'item_nav' => isset($nav) ? $nav : null,
@@ -107,27 +79,27 @@ Router\get_action('show', function () {
 
 // Display feed items page
 Router\get_action('feed-items', function () {
+    $user_id = SessionStorage::getInstance()->getUserId();
     $feed_id = Request\int_param('feed_id', 0);
     $offset = Request\int_param('offset', 0);
-    $nb_items = Model\ItemFeed\count_items($feed_id);
-    $feed = Model\Feed\get($feed_id);
+    $feed = Model\Feed\get_feed($user_id, $feed_id);
     $order = Request\param('order', 'updated');
-    $direction = Request\param('direction', Model\Config\get('items_sorting_direction'));
-    $items = Model\ItemFeed\get_all_items($feed_id, $offset, Model\Config\get('items_per_page'), $order, $direction);
+    $direction = Request\param('direction', Helper\config('items_sorting_direction'));
+    $items = Model\ItemFeed\get_all_items($user_id, $feed_id, $offset, Helper\config('items_per_page'), $order, $direction);
+    $nb_items = Model\ItemFeed\count_items($user_id, $feed_id);
 
     Response\html(Template\layout('feed_items', array(
-        'favicons' => Model\Favicon\get_favicons(array($feed['id'])),
-        'original_marks_read' => Model\Config\get('original_marks_read'),
+        'favicons' => Model\Favicon\get_favicons_by_feed_ids(array($feed['id'])),
+        'original_marks_read' => Helper\config('original_marks_read'),
         'order' => $order,
         'direction' => $direction,
-        'display_mode' => Model\Config\get('items_display_mode'),
+        'display_mode' => Helper\config('items_display_mode'),
         'feed' => $feed,
         'items' => $items,
         'nb_items' => $nb_items,
-        'nb_unread_items' => Model\Item\count_by_status('unread'),
         'offset' => $offset,
-        'items_per_page' => Model\Config\get('items_per_page'),
-        'item_title_link' => Model\Config\get('item_title_link'),
+        'items_per_page' => Helper\config('items_per_page'),
+        'item_title_link' => Helper\config('item_title_link'),
         'menu' => 'feed-items',
         'title' => '('.$nb_items.') '.$feed['title']
     )));
@@ -135,43 +107,56 @@ Router\get_action('feed-items', function () {
 
 // Ajax call to download an item (fetch the full content from the original website)
 Router\post_action('download-item', function () {
-    $id = Request\param('id');
+    $user_id = SessionStorage::getInstance()->getUserId();
+    $item_id = Request\param('id');
 
-    $item = Model\Item\get($id);
-    $feed = Model\Feed\get($item['feed_id']);
+    $item = Model\Item\get_item($user_id, $item_id);
+    $feed = Model\Feed\get_feed($user_id, $item['feed_id']);
 
-    $download = Model\Item\download_contents($id);
-    $download['content'] = Handler\Proxy\rewrite_html($download['content'], $item['url'], Model\Config\get('image_proxy'), $feed['cloak_referrer']);
+    $download = Handler\Item\download_item_content($user_id, $item_id);
+    $download['content'] = Handler\Proxy\rewrite_html(
+        $download['content'],
+        $item['url'],
+        Helper\bool_config('image_proxy'),
+        (bool) $feed['cloak_referrer']
+    );
 
     Response\json($download);
 });
 
 // Ajax call to mark item read
 Router\post_action('mark-item-read', function () {
-    Model\Item\set_read(Request\param('id'));
+    $user_id = SessionStorage::getInstance()->getUserId();
+    $item_id = Request\param('id');
+    Model\Item\change_item_status($user_id, $item_id, Model\Item\STATUS_READ);
     Response\json(array('Ok'));
 });
 
 // Ajax call to mark item as removed
 Router\post_action('mark-item-removed', function () {
-    Model\Item\set_removed(Request\param('id'));
+    $user_id = SessionStorage::getInstance()->getUserId();
+    $item_id = Request\param('id');
+    Model\Item\change_item_status($user_id, $item_id, Model\Item\STATUS_REMOVED);
     Response\json(array('Ok'));
 });
 
 // Ajax call to mark item unread
 Router\post_action('mark-item-unread', function () {
-    Model\Item\set_unread(Request\param('id'));
+    $user_id = SessionStorage::getInstance()->getUserId();
+    $item_id = Request\param('id');
+    Model\Item\change_item_status($user_id, $item_id, Model\Item\STATUS_UNREAD);
     Response\json(array('Ok'));
 });
 
 // Mark unread items as read
 Router\get_action('mark-all-read', function () {
+    $user_id = SessionStorage::getInstance()->getUserId();
     $group_id = Request\int_param('group_id', null);
 
     if ($group_id !== null) {
-        Model\ItemGroup\mark_all_as_read($group_id);
+        Model\ItemGroup\change_items_status($user_id, $group_id, Model\Item\STATUS_UNREAD, Model\Item\STATUS_READ);
     } else {
-        Model\Item\mark_all_as_read();
+        Model\Item\change_items_status($user_id, Model\Item\STATUS_UNREAD, Model\Item\STATUS_READ);
     }
 
     Response\redirect('?action=unread');
@@ -179,9 +164,11 @@ Router\get_action('mark-all-read', function () {
 
 // Mark all unread items as read for a specific feed
 Router\get_action('mark-feed-as-read', function () {
+    $user_id = SessionStorage::getInstance()->getUserId();
     $feed_id = Request\int_param('feed_id');
 
-    Model\ItemFeed\mark_all_as_read($feed_id);
+    Model\ItemFeed\change_items_status($user_id, $feed_id, Model\Item\STATUS_UNREAD, Model\Item\STATUS_READ);
+
     Response\redirect('?action=feed-items&feed_id='.$feed_id);
 });
 
@@ -190,48 +177,55 @@ Router\get_action('mark-feed-as-read', function () {
 // that where marked read from the frontend, since the number of unread items
 // on page 2+ is unknown.
 Router\post_action('mark-feed-as-read', function () {
-    Model\ItemFeed\mark_all_as_read(Request\int_param('feed_id'));
-    $nb_items = Model\Item\count_by_status('unread');
+    $user_id = SessionStorage::getInstance()->getUserId();
+    $feed_id = Request\int_param('feed_id');
 
+    Model\ItemFeed\change_items_status($user_id, $feed_id, Model\Item\STATUS_UNREAD, Model\Item\STATUS_READ);
+
+    $nb_items = Model\Item\count_by_status($user_id, Model\Item\STATUS_READ);
     Response\raw($nb_items);
 });
 
 // Mark item as read and redirect to the listing page
 Router\get_action('mark-item-read', function () {
-    $id = Request\param('id');
+    $user_id = SessionStorage::getInstance()->getUserId();
+    $item_id = Request\param('id');
     $redirect = Request\param('redirect', 'unread');
     $offset = Request\int_param('offset', 0);
     $feed_id = Request\int_param('feed_id', 0);
 
-    Model\Item\set_read($id);
-    Response\redirect('?action='.$redirect.'&offset='.$offset.'&feed_id='.$feed_id.'#item-'.$id);
+    Model\Item\change_item_status($user_id, $item_id, Model\Item\STATUS_READ);
+    Response\redirect('?action='.$redirect.'&offset='.$offset.'&feed_id='.$feed_id.'#item-'.$item_id);
 });
 
 // Mark item as unread and redirect to the listing page
 Router\get_action('mark-item-unread', function () {
-    $id = Request\param('id');
+    $user_id = SessionStorage::getInstance()->getUserId();
+    $item_id = Request\param('id');
     $redirect = Request\param('redirect', 'history');
     $offset = Request\int_param('offset', 0);
     $feed_id = Request\int_param('feed_id', 0);
 
-    Model\Item\set_unread($id);
-    Response\redirect('?action='.$redirect.'&offset='.$offset.'&feed_id='.$feed_id.'#item-'.$id);
+    Model\Item\change_item_status($user_id, $item_id, Model\Item\STATUS_UNREAD);
+    Response\redirect('?action='.$redirect.'&offset='.$offset.'&feed_id='.$feed_id.'#item-'.$item_id);
 });
 
 // Mark item as removed and redirect to the listing page
 Router\get_action('mark-item-removed', function () {
-    $id = Request\param('id');
+    $user_id = SessionStorage::getInstance()->getUserId();
+    $item_id = Request\param('id');
     $redirect = Request\param('redirect', 'history');
     $offset = Request\int_param('offset', 0);
     $feed_id = Request\int_param('feed_id', 0);
 
-    Model\Item\set_removed($id);
+    Model\Item\change_item_status($user_id, $item_id, Model\Item\STATUS_REMOVED);
     Response\redirect('?action='.$redirect.'&offset='.$offset.'&feed_id='.$feed_id);
 });
 
 Router\post_action('latest-feeds-items', function () {
-    $items = Model\Item\get_latest_feeds_items();
-    $nb_unread_items = Model\Item\count_by_status('unread');
+    $user_id = SessionStorage::getInstance()->getUserId();
+    $items = Model\Item\get_latest_feeds_items($user_id);
+    $nb_unread_items = Model\Item\count_by_status($user_id, 'unread');
 
     $feeds = array_reduce($items, function ($result, $item) {
         $result[$item['id']] = array(
diff --git a/app/controllers/opml.php b/app/controllers/opml.php
new file mode 100644
index 0000000..af4081e
--- /dev/null
+++ b/app/controllers/opml.php
@@ -0,0 +1,40 @@
+getUserId();
+    Response\force_download('feeds.opml');
+    Response\xml(Handler\Opml\export_all_feeds($user_id));
+});
+
+// OPML import form
+Router\get_action('import', function () {
+    Response\html(Template\layout('import', array(
+        'errors' => array(),
+        'menu' => 'feeds',
+        'title' => t('OPML Import')
+    )));
+});
+
+// OPML importation
+Router\post_action('import', function () {
+    try {
+        $user_id = SessionStorage::getInstance()->getUserId();
+        Handler\Opml\import_opml($user_id, Request\file_content('file'));
+        SessionStorage::getInstance()->setFlashMessage(t('Your feeds have been imported.'));
+        Response\redirect('?action=feeds');
+    } catch (Exception $e) {
+        SessionStorage::getInstance()->setFlashErrorMessage(t('Unable to import your OPML file.').' ('.$e->getMessage().')');
+        Response\redirect('?action=import');
+    }
+});
diff --git a/app/controllers/profile.php b/app/controllers/profile.php
new file mode 100644
index 0000000..e6b9dbe
--- /dev/null
+++ b/app/controllers/profile.php
@@ -0,0 +1,48 @@
+getUserId();
+
+    Response\html(Template\layout('profile', array(
+        'errors' => array(),
+        'values' => Model\User\get_user_by_id_without_password($user_id) + array('csrf' => Helper\generate_csrf()),
+        'menu' => 'config',
+        'title' => t('User Profile')
+    )));
+});
+
+Router\post_action('profile', function () {
+    $user_id = SessionStorage::getInstance()->getUserId();
+    $values = Request\values();
+    Helper\check_csrf_values($values);
+    list($valid, $errors) = Validator\User\validate_modification($values);
+
+    if ($valid) {
+        $new_password = empty($values['password']) ? null : $values['password'];
+        if (Model\User\update_user($user_id, $values['username'], $new_password)) {
+            SessionStorage::getInstance()->setFlashMessage(t('Your preferences are updated.'));
+        } else {
+            SessionStorage::getInstance()->setFlashErrorMessage(t('Unable to update your preferences.'));
+        }
+
+        Response\redirect('?action=profile');
+    }
+
+    Response\html(Template\layout('profile', array(
+        'errors' => $errors,
+        'values' => Model\User\get_user_by_id_without_password($user_id) + array('csrf' => Helper\generate_csrf()),
+        'menu' => 'config',
+        'title' => t('User Profile')
+    )));
+});
diff --git a/app/controllers/search.php b/app/controllers/search.php
index 2b0ed56..47e5b28 100644
--- a/app/controllers/search.php
+++ b/app/controllers/search.php
@@ -1,39 +1,40 @@
 getUserId();
     $text = Request\param('text', '');
     $offset = Request\int_param('offset', 0);
 
     $items = array();
     $nb_items = 0;
     if ($text) {
-        $items = Model\Search\get_all_items($text, $offset, Model\Config\get('items_per_page'));
-        $nb_items = Model\Search\count_items($text);
+        $items = Model\ItemSearch\get_all_items($user_id, $text, $offset, Helper\config('items_per_page'));
+        $nb_items = Model\ItemSearch\count_items($user_id, $text);
     }
 
     Response\html(Template\layout('search', array(
-        'favicons' => Model\Favicon\get_item_favicons($items),
-        'original_marks_read' => Model\Config\get('original_marks_read'),
+        'favicons' => Model\Favicon\get_items_favicons($items),
+        'original_marks_read' => Helper\config('original_marks_read'),
         'text' => $text,
         'items' => $items,
         'order' => '',
         'direction' => '',
-        'display_mode' => Model\Config\get('items_display_mode'),
-        'item_title_link' => Model\Config\get('item_title_link'),
+        'display_mode' => Helper\config('items_display_mode'),
+        'item_title_link' => Helper\config('item_title_link'),
         'group_id' => array(),
         'nb_items' => $nb_items,
-        'nb_unread_items' => Model\Item\count_by_status('unread'),
         'offset' => $offset,
-        'items_per_page' => Model\Config\get('items_per_page'),
+        'items_per_page' => Helper\config('items_per_page'),
         'nothing_to_read' => Request\int_param('nothing_to_read'),
         'menu' => 'search',
         'title' => t('Search').' ('.$nb_items.')'
diff --git a/app/controllers/services.php b/app/controllers/services.php
new file mode 100644
index 0000000..a3fa059
--- /dev/null
+++ b/app/controllers/services.php
@@ -0,0 +1,38 @@
+getUserId();
+
+    Response\html(Template\layout('services', array(
+        'errors' => array(),
+        'values' => Model\Config\get_all($user_id) + array('csrf' => Helper\generate_csrf()),
+        'menu' => 'config',
+        'title' => t('Preferences')
+    )));
+});
+
+// Update bookmark services
+Router\post_action('services', function () {
+    $user_id = SessionStorage::getInstance()->getUserId();
+    $values = Request\values() + array('pinboard_enabled' => 0, 'instapaper_enabled' => 0, 'wallabag_enabled' => 0);
+    Helper\check_csrf_values($values);
+
+    if (Model\Config\save($user_id, $values)) {
+        SessionStorage::getInstance()->setFlashMessage(t('Your preferences are updated.'));
+    } else {
+        SessionStorage::getInstance()->setFlashErrorMessage(t('Unable to update your preferences.'));
+    }
+
+    Response\redirect('?action=services');
+});
diff --git a/app/controllers/users.php b/app/controllers/users.php
new file mode 100644
index 0000000..a4fd7a2
--- /dev/null
+++ b/app/controllers/users.php
@@ -0,0 +1,134 @@
+isAdmin()) {
+        Response\text('Access Forbidden', 403);
+    }
+
+    Response\html(Template\layout('users', array(
+        'users'      => Model\User\get_all_users(),
+        'menu'       => 'config',
+        'title'      => t('Users'),
+    )));
+});
+
+Router\get_action('new-user', function () {
+    if (! SessionStorage::getInstance()->isAdmin()) {
+        Response\text('Access Forbidden', 403);
+    }
+
+    Response\html(Template\layout('new_user', array(
+        'values' => array('csrf' => Helper\generate_csrf()),
+        'errors' => array(),
+        'menu'   => 'config',
+        'title'  => t('New User'),
+    )));
+});
+
+Router\post_action('new-user', function () {
+    if (! SessionStorage::getInstance()->isAdmin()) {
+        Response\text('Access Forbidden', 403);
+    }
+
+    $values = Request\values() + array('is_admin' => 0);
+    Helper\check_csrf_values($values);
+    list($valid, $errors) = Validator\User\validate_creation($values);
+
+    if ($valid) {
+        if (Model\User\create_user($values['username'], $values['password'], (bool) $values['is_admin'])) {
+            SessionStorage::getInstance()->setFlashMessage(t('New user created successfully.'));
+        } else {
+            SessionStorage::getInstance()->setFlashErrorMessage(t('Unable to create this user.'));
+        }
+
+        Response\redirect('?action=users');
+    }
+
+    Response\html(Template\layout('new_user', array(
+        'values' => $values + array('csrf' => Helper\generate_csrf()),
+        'errors' => $errors,
+        'menu'   => 'config',
+        'title'  => t('New User'),
+    )));
+});
+
+Router\get_action('edit-user', function () {
+    if (! SessionStorage::getInstance()->isAdmin()) {
+        Response\text('Access Forbidden', 403);
+    }
+
+    $user = Model\User\get_user_by_id_without_password(Request\int_param('user_id'));
+
+    if (empty($user)) {
+        Response\redirect('?action=users');
+    }
+
+    Response\html(Template\layout('edit_user', array(
+        'values' => $user + array('csrf' => Helper\generate_csrf()),
+        'errors' => array(),
+        'menu'   => 'config',
+        'title'  => t('Edit User'),
+    )));
+});
+
+Router\post_action('edit-user', function () {
+    if (! SessionStorage::getInstance()->isAdmin()) {
+        Response\text('Access Forbidden', 403);
+    }
+
+    $values = Request\values() + array('is_admin' => 0);
+    Helper\check_csrf_values($values);
+    list($valid, $errors) = Validator\User\validate_modification($values);
+
+    if ($valid) {
+        $new_password = empty($values['password']) ? null : $values['password'];
+        $is_admin = $values['is_admin'] == 1 ? 1 : 0;
+        if (Model\User\update_user($values['id'], $values['username'], $new_password, $is_admin)) {
+            SessionStorage::getInstance()->setFlashMessage(t('User modified successfully.'));
+        } else {
+            SessionStorage::getInstance()->setFlashErrorMessage(t('Unable to edit this user.'));
+        }
+
+        Response\redirect('?action=users');
+    }
+
+    Response\html(Template\layout('edit_user', array(
+        'values' => $values + array('csrf' => Helper\generate_csrf()),
+        'errors' => $errors,
+        'menu'   => 'config',
+        'title'  => t('Edit User'),
+    )));
+});
+
+Router\get_action('confirm-remove-user', function () {
+    if (! SessionStorage::getInstance()->isAdmin()) {
+        Response\text('Access Forbidden', 403);
+    }
+
+    Response\html(Template\layout('confirm_remove_user', array(
+        'user'       => Model\User\get_user_by_id_without_password(Request\int_param('user_id')),
+        'csrf_token' => Helper\generate_csrf(),
+        'menu'       => 'config',
+        'title'      => t('Remove User'),
+    )));
+});
+
+Router\get_action('remove-user', function () {
+    if (! SessionStorage::getInstance()->isAdmin() || ! Helper\check_csrf(Request\param('csrf'))) {
+        Response\text('Access Forbidden', 403);
+    }
+
+    Model\User\remove_user(Request\int_param('user_id'));
+    Response\redirect('?action=users');
+});
\ No newline at end of file
diff --git a/app/core/session.php b/app/core/session.php
index 430cde9..00d05d1 100644
--- a/app/core/session.php
+++ b/app/core/session.php
@@ -2,54 +2,155 @@
 
 namespace Miniflux\Session;
 
-const SESSION_LIFETIME = 2678400;
+use Miniflux\Helper;
 
-function open($base_path = '/', $save_path = '', $session_lifetime = SESSION_LIFETIME)
+class SessionManager
 {
-    if ($save_path !== '') {
-        session_save_path($save_path);
+    const SESSION_LIFETIME = 2678400;
+
+    public static function open($base_path = '/', $save_path = '', $duration = self::SESSION_LIFETIME)
+    {
+        if ($save_path !== '') {
+            session_save_path($save_path);
+        }
+
+        // HttpOnly and secure flags for session cookie
+        session_set_cookie_params(
+            $duration,
+            $base_path ?: '/',
+            null,
+            Helper\is_secure_connection(),
+            true
+        );
+
+        // Avoid session id in the URL
+        ini_set('session.use_only_cookies', true);
+
+        // Ensure session ID integrity
+        ini_set('session.entropy_file', '/dev/urandom');
+        ini_set('session.entropy_length', '32');
+        ini_set('session.hash_bits_per_character', 6);
+
+        // Custom session name
+        session_name('MX_SID');
+
+        session_start();
+
+        // Regenerate the session id to avoid session fixation issue
+        if (empty($_SESSION['__validated'])) {
+            session_regenerate_id(true);
+            $_SESSION['__validated'] = 1;
+        }
     }
 
-    // HttpOnly and secure flags for session cookie
-    session_set_cookie_params(
-        $session_lifetime,
-        $base_path ?: '/',
-        null,
-        isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on',
-        true
-    );
-
-    // Avoid session id in the URL
-    ini_set('session.use_only_cookies', true);
-
-    // Ensure session ID integrity
-    ini_set('session.entropy_file', '/dev/urandom');
-    ini_set('session.entropy_length', '32');
-    ini_set('session.hash_bits_per_character', 6);
-
-    // Custom session name
-    session_name('__$');
-
-    session_start();
-
-    // Regenerate the session id to avoid session fixation issue
-    if (empty($_SESSION['__validated'])) {
-        session_regenerate_id(true);
-        $_SESSION['__validated'] = 1;
+    public static function close()
+    {
+        session_destroy();
     }
 }
 
-function close()
-{
-    session_destroy();
-}
 
-function flash($message)
+class SessionStorage
 {
-    $_SESSION['flash_message'] = $message;
-}
+    private static $instance = null;
 
-function flash_error($message)
-{
-    $_SESSION['flash_error_message'] = $message;
+    public function __construct(array $session = null)
+    {
+        if (! isset($_SESSION)) {
+            $_SESSION = array();
+        }
+
+        $_SESSION = $session ?: $_SESSION;
+    }
+
+    public static function getInstance(array $session = null)
+    {
+        if (self::$instance === null) {
+            self::$instance = new static($session);
+        }
+
+        return self::$instance;
+    }
+
+    public function flush()
+    {
+        $_SESSION = array();
+        return $this;
+    }
+
+    public function flushConfig()
+    {
+        unset($_SESSION['config']);
+        return $this;
+    }
+
+    public function setConfig(array $config)
+    {
+        $_SESSION['config'] = $config;
+        return $this;
+    }
+
+    public function getConfig()
+    {
+        return $this->getValue('config');
+    }
+
+    public function setUser(array $user)
+    {
+        $_SESSION['user_id'] = $user['id'];
+        $_SESSION['username'] = $user['username'];
+        $_SESSION['is_admin'] = (bool) $user['is_admin'];
+        return $this;
+    }
+
+    public function getUserId()
+    {
+        return $this->getValue('user_id');
+    }
+
+    public function getUsername()
+    {
+        return $this->getValue('username');
+    }
+
+    public function isAdmin()
+    {
+        return $this->getValue('is_admin');
+    }
+
+    public function isLogged()
+    {
+        return $this->getValue('user_id') !== null;
+    }
+
+    public function setFlashMessage($message)
+    {
+        $_SESSION['flash_message'] = $message;
+        return $this;
+    }
+
+    public function setFlashErrorMessage($message)
+    {
+        $_SESSION['flash_error_message'] = $message;
+        return $this;
+    }
+
+    public function getFlashMessage()
+    {
+        $message = $this->getValue('flash_message');
+        unset($_SESSION['flash_message']);
+        return $message;
+    }
+
+    public function getFlashErrorMessage()
+    {
+        $message = $this->getValue('flash_error_message');
+        unset($_SESSION['flash_error_message']);
+        return $message;
+    }
+
+    protected function getValue($key)
+    {
+        return isset($_SESSION[$key]) ? $_SESSION[$key] : null;
+    }
 }
diff --git a/app/core/template.php b/app/core/template.php
index b5e6297..a5dd94f 100644
--- a/app/core/template.php
+++ b/app/core/template.php
@@ -2,6 +2,8 @@
 
 namespace Miniflux\Template;
 
+use Miniflux\Model;
+
 const PATH = 'app/templates/';
 
 // Template\load('template_name', ['bla' => 'value']);
@@ -30,5 +32,8 @@ function load()
 
 function layout($template_name, array $template_args = array(), $layout_name = 'layout')
 {
-    return load($layout_name, $template_args + array('content_for_layout' => load($template_name, $template_args)));
+    return load(
+        $layout_name,
+        $template_args + array('content_for_layout' => load($template_name, $template_args))
+    );
 }
diff --git a/app/functions.php b/app/functions.php
index 2c828d8..30da50e 100644
--- a/app/functions.php
+++ b/app/functions.php
@@ -24,3 +24,14 @@ function dt()
 {
     return call_user_func_array('\Miniflux\Translator\datetime', func_get_args());
 }
+
+function get_cli_option($option, array $options)
+{
+    $value = null;
+
+    if (! empty($options[$option]) && ctype_digit($options[$option])) {
+        $value = (int) $options[$option];
+    }
+
+    return $value;
+}
diff --git a/app/handlers/feed.php b/app/handlers/feed.php
new file mode 100644
index 0000000..88495a4
--- /dev/null
+++ b/app/handlers/feed.php
@@ -0,0 +1,161 @@
+discover($url, $last_modified, $etag);
+
+        if ($resource->isModified()) {
+            $parser = $reader->getParser(
+                $resource->getUrl(),
+                $resource->getContent(),
+                $resource->getEncoding()
+            );
+
+            if ($download_content) {
+                $parser->enableContentGrabber();
+            }
+
+            $feed = $parser->execute();
+        }
+    } catch (PicoFeed\Client\InvalidCertificateException $e) {
+        $error_message = t('Invalid SSL certificate.');
+    } catch (PicoFeed\Client\InvalidUrlException $e) {
+        $error_message = $e->getMessage();
+    } catch (PicoFeed\Client\MaxRedirectException $e) {
+        $error_message = t('Maximum number of HTTP redirection exceeded.');
+    } catch (PicoFeed\Client\MaxSizeException $e) {
+        $error_message = t('The content size exceeds to maximum allowed size.');
+    } catch (PicoFeed\Client\TimeoutException $e) {
+        $error_message = t('Connection timeout.');
+    } catch (PicoFeed\Parser\MalformedXmlException $e) {
+        $error_message = t('Feed is malformed.');
+    } catch (PicoFeed\Reader\SubscriptionNotFoundException $e) {
+        $error_message = t('Unable to find a subscription.');
+    } catch (PicoFeed\Reader\UnsupportedFeedFormatException $e) {
+        $error_message = t('Unable to detect the feed format.');
+    }
+
+    return array($feed, $resource, $error_message);
+}
+
+function create_feed($user_id, $url, $download_content = false, $rtl = false, $cloak_referrer = false, array $feed_group_ids = array(), $group_name = null)
+{
+    $feed_id = null;
+    list($feed, $resource, $error_message) = fetch_feed($url, $download_content);
+
+    if ($feed !== null) {
+        $feed_id = Model\Feed\create(
+            $user_id,
+            $feed,
+            $resource->getEtag(),
+            $resource->getLastModified(),
+            $rtl,
+            $download_content,
+            $cloak_referrer
+        );
+
+        if ($feed_id === -1) {
+            $error_message = t('This subscription already exists.');
+        } else if ($feed_id === false) {
+            $error_message = t('Unable to save this subscription in the database.');
+        } else {
+            Model\Favicon\create_feed_favicon($feed_id, $feed->getSiteUrl(), $feed->getIcon());
+
+            if (! empty($feed_group_ids)) {
+                Model\Group\update_feed_groups($user_id, $feed_id, $feed_group_ids, $group_name);
+            }
+        }
+    }
+
+    return array($feed_id, $error_message);
+}
+
+function update_feed($user_id, $feed_id)
+{
+    $subscription = Model\Feed\get_feed($user_id, $feed_id);
+
+    list($feed, $resource, $error_message) = fetch_feed(
+        $subscription['feed_url'],
+        (bool) $subscription['download_content'],
+        $subscription['etag'],
+        $subscription['last_modified']
+    );
+
+    if (! empty($error_message)) {
+        Model\Feed\update_feed($user_id, $feed_id, array(
+            'last_checked' => time(),
+            'parsing_error' => 1,
+        ));
+
+        return false;
+    } else {
+
+        Model\Feed\update_feed($user_id, $feed_id, array(
+            'etag' => $resource->getEtag(),
+            'last_modified' => $resource->getLastModified(),
+            'last_checked' => time(),
+            'parsing_error' => 0,
+        ));
+    }
+
+    if ($feed !== null) {
+        Model\Item\update_feed_items($user_id, $feed_id, $feed->getItems(), $subscription['rtl']);
+        Model\Favicon\create_feed_favicon($feed_id, $feed->getSiteUrl(), $feed->getIcon());
+    }
+
+    return true;
+}
+
+function update_feeds($user_id, $limit = null)
+{
+    foreach (Model\Feed\get_feed_ids($user_id, $limit) as $feed_id) {
+        update_feed($user_id, $feed_id);
+    }
+}
+
+function get_reader_config()
+{
+    $config = new ReaderConfig;
+    $config->setTimezone(Helper\config('timezone'));
+
+    // Client
+    $config->setClientTimeout(HTTP_TIMEOUT);
+    $config->setClientUserAgent(HTTP_USER_AGENT);
+    $config->setMaxBodySize(HTTP_MAX_RESPONSE_SIZE);
+
+    // Grabber
+    $config->setGrabberRulesFolder(RULES_DIRECTORY);
+
+    // Proxy
+    $config->setProxyHostname(PROXY_HOSTNAME);
+    $config->setProxyPort(PROXY_PORT);
+    $config->setProxyUsername(PROXY_USERNAME);
+    $config->setProxyPassword(PROXY_PASSWORD);
+
+    // Filter
+    $config->setFilterIframeWhitelist(Model\Config\get_iframe_whitelist());
+
+    // Parser
+    $config->setParserHashAlgo('crc32b');
+
+    if (DEBUG_MODE) {
+        Logger::enable();
+    }
+
+    return $config;
+}
diff --git a/app/handlers/item.php b/app/handlers/item.php
new file mode 100644
index 0000000..8db9954
--- /dev/null
+++ b/app/handlers/item.php
@@ -0,0 +1,33 @@
+table('items')
+                ->eq('id', $item['id'])
+                ->save(array('content' => $content));
+        }
+
+        return array(
+            'result' => true,
+            'content' => $content
+        );
+    }
+
+    return array(
+        'result' => false,
+        'content' => ''
+    );
+}
diff --git a/app/handlers/opml.php b/app/handlers/opml.php
index 323788a..654040a 100644
--- a/app/handlers/opml.php
+++ b/app/handlers/opml.php
@@ -2,20 +2,20 @@
 
 namespace Miniflux\Handler\Opml;
 
-use Miniflux\Model\Feed;
-use Miniflux\Model\Group;
+use Miniflux\Model;
+use PicoDb\Database;
 use PicoFeed\Serialization\Subscription;
 use PicoFeed\Serialization\SubscriptionList;
 use PicoFeed\Serialization\SubscriptionListBuilder;
+use PicoFeed\Serialization\SubscriptionListParser;
 
-
-function export_all_feeds()
+function export_all_feeds($user_id)
 {
-    $feeds = Feed\get_all();
+    $feeds = Model\Feed\get_feeds($user_id);
     $subscriptionList = SubscriptionList::create()->setTitle(t('Subscriptions'));
 
     foreach ($feeds as $feed) {
-        $groups = Group\get_feed_groups($feed['id']);
+        $groups = Model\Group\get_feed_groups($feed['id']);
         $category = '';
 
         if (!empty($groups)) {
@@ -32,3 +32,36 @@ function export_all_feeds()
 
     return SubscriptionListBuilder::create($subscriptionList)->build();
 }
+
+function import_opml($user_id, $content)
+{
+    $subscriptionList = SubscriptionListParser::create($content)->parse();
+
+    $db = Database::getInstance('db');
+    $db->startTransaction();
+
+    foreach ($subscriptionList->subscriptions as $subscription) {
+        if (! $db->table('feeds')->eq('user_id', $user_id)->eq('feed_url', $subscription->getFeedUrl())->exists()) {
+            $db->table('feeds')->insert(array(
+                'user_id' => $user_id,
+                'title' => $subscription->getTitle(),
+                'site_url' => $subscription->getSiteUrl(),
+                'feed_url' => $subscription->getFeedUrl(),
+            ));
+
+            if ($subscription->getCategory() !== '') {
+                $feed_id = $db->getLastId();
+                $group_id = Model\Group\get_group_id_from_title($user_id, $subscription->getCategory());
+
+                if (empty($group_id)) {
+                    $group_id = Model\Group\create_group($user_id, $subscription->getCategory());
+                }
+
+                Model\Group\associate_feed_groups($feed_id, array($group_id));
+            }
+        }
+    }
+
+    $db->closeTransaction();
+    return true;
+}
diff --git a/app/handlers/proxy.php b/app/handlers/proxy.php
index dd672cb..3023bc8 100644
--- a/app/handlers/proxy.php
+++ b/app/handlers/proxy.php
@@ -3,7 +3,6 @@
 namespace Miniflux\Handler\Proxy;
 
 use Miniflux\Helper;
-use Miniflux\Model\Config;
 use PicoFeed\Client\ClientException;
 use PicoFeed\Config\Config as PicoFeedConfig;
 use PicoFeed\Filter\Filter;
@@ -51,15 +50,14 @@ function rewrite_html($html, $website, $proxy_images, $cloak_referrer)
 function download($url)
 {
     try {
-        if ((bool) Config\get('debug_mode')) {
+        if (DEBUG_MODE) {
             Logger::enable();
         }
 
         $client = Client::getInstance();
-        $client->setUserAgent(Config\HTTP_USER_AGENT);
+        $client->setUserAgent(HTTP_USER_AGENT);
         $client->enablePassthroughMode();
         $client->execute($url);
-    } catch (ClientException $e) {}
-
-    Config\write_debug();
+    } catch (ClientException $e) {
+    }
 }
diff --git a/app/handlers/scraper.php b/app/handlers/scraper.php
index 957dd11..a806ebb 100644
--- a/app/handlers/scraper.php
+++ b/app/handlers/scraper.php
@@ -3,13 +3,13 @@
 namespace Miniflux\Handler\Scraper;
 
 use PicoFeed\Scraper\Scraper;
-use Miniflux\Model\Config;
+use Miniflux\Handler;
 
-function download_contents($url)
+function download_content($url)
 {
     $contents = '';
 
-    $scraper = new Scraper(Config\get_reader_config());
+    $scraper = new Scraper(Handler\Feed\get_reader_config());
     $scraper->setUrl($url);
     $scraper->execute();
 
diff --git a/app/handlers/service.php b/app/handlers/service.php
index 62de47b..88fb541 100644
--- a/app/handlers/service.php
+++ b/app/handlers/service.php
@@ -2,24 +2,24 @@
 
 namespace Miniflux\Handler\Service;
 
+use Miniflux\Model;
+use Miniflux\Helper;
 use PicoFeed\Client\Client;
 use PicoFeed\Client\ClientException;
-use Miniflux\Model\Config;
-use Miniflux\Model\Item;
 
-function sync($item_id)
+function sync($user_id, $item_id)
 {
-    $item = Item\get($item_id);
+    $item = Model\Item\get_item($user_id, $item_id);
 
-    if ((bool) Config\get('pinboard_enabled')) {
+    if (Helper\bool_config('pinboard_enabled')) {
         pinboard_sync($item);
     }
 
-    if ((bool) Config\get('instapaper_enabled')) {
+    if (Helper\bool_config('instapaper_enabled')) {
         instapaper_sync($item);
     }
 
-    if ((bool) Config\get('wallabag_enabled')) {
+    if (Helper\bool_config('wallabag_enabled')) {
         wallabag_sync($item);
     }
 }
@@ -27,8 +27,8 @@ function sync($item_id)
 function instapaper_sync(array $item)
 {
     $params = array(
-        'username' => Config\get('instapaper_username'),
-        'password' => Config\get('instapaper_password'),
+        'username' => Helper\config('instapaper_username'),
+        'password' => Helper\config('instapaper_password'),
         'url' => $item['url'],
         'title' => $item['title'],
     );
@@ -47,11 +47,11 @@ function instapaper_sync(array $item)
 function pinboard_sync(array $item)
 {
     $params = array(
-        'auth_token' => Config\get('pinboard_token'),
+        'auth_token' => Helper\config('pinboard_token'),
         'format' => 'json',
         'url' => $item['url'],
         'description' => $item['title'],
-        'tags' => Config\get('pinboard_tags'),
+        'tags' => Helper\config('pinboard_tags'),
     );
 
     $url = 'https://api.pinboard.in/v1/posts/add?'.http_build_query($params);
@@ -79,7 +79,7 @@ function wallabag_has_url($url)
     if ($token === false) {
         return false;
     }
-    $apiUrl = rtrim(Config\get('wallabag_url'), '\/') . '/api/entries/exists.json?url=' . urlencode($url);
+    $apiUrl = rtrim(Helper\config('wallabag_url'), '\/') . '/api/entries/exists.json?url=' . urlencode($url);
     $headers = array('Authorization: Bearer ' . $token);
     $response = api_get_call($apiUrl, $headers);
     if ($response !== false) {
@@ -94,7 +94,7 @@ function wallabag_add_item($url, $title)
     if ($token === false) {
         return false;
     }
-    $apiUrl = rtrim(Config\get('wallabag_url'), '\/') . '/api/entries.json';
+    $apiUrl = rtrim(Helper\config('wallabag_url'), '\/') . '/api/entries.json';
     $headers = array('Authorization: Bearer ' . $token);
     $data = array(
         'url' => $url,
@@ -112,13 +112,13 @@ function wallabag_get_access_token()
     if (!empty($_SESSION['wallabag_access_token'])) {
         return $_SESSION['wallabag_access_token'];
     }
-    $url = rtrim(Config\get('wallabag_url'), '\/') . '/oauth/v2/token';
+    $url = rtrim(Helper\config('wallabag_url'), '\/') . '/oauth/v2/token';
     $data = array(
         'grant_type' => 'password',
-        'client_id' => Config\get('wallabag_client_id'),
-        'client_secret' => Config\get('wallabag_client_secret'),
-        'username' => Config\get('wallabag_username'),
-        'password' => Config\get('wallabag_password')
+        'client_id' => Helper\config('wallabag_client_id'),
+        'client_secret' => Helper\config('wallabag_client_secret'),
+        'username' => Helper\config('wallabag_username'),
+        'password' => Helper\config('wallabag_password')
     );
     $response = api_post_call($url, $data);
     if ($response !== false) {
@@ -135,7 +135,7 @@ function api_get_call($url, array $headers = array())
 {
     try {
         $client = Client::getInstance();
-        $client->setUserAgent(Config\HTTP_USER_AGENT);
+        $client->setUserAgent(HTTP_USER_AGENT);
         if ($headers) {
             $client->setHeaders($headers);
         }
diff --git a/app/helpers/app.php b/app/helpers/app.php
index ec3eea8..19d8122 100644
--- a/app/helpers/app.php
+++ b/app/helpers/app.php
@@ -2,6 +2,8 @@
 
 namespace Miniflux\Helper;
 
+use PicoFeed\Logging\Logger;
+
 function escape($value)
 {
     return htmlspecialchars($value, ENT_QUOTES, 'UTF-8', false);
@@ -43,7 +45,11 @@ function get_current_base_url()
 {
     $url = is_secure_connection() ? 'https://' : 'http://';
     $url .= $_SERVER['HTTP_HOST'];
-    $url .= $_SERVER['SERVER_PORT'] == 80 || $_SERVER['SERVER_PORT'] == 443 ? '' : ':'.$_SERVER['SERVER_PORT'];
+
+    if (strpos($_SERVER['HTTP_HOST'], ':') === false) {
+        $url .= $_SERVER['SERVER_PORT'] == 80 || $_SERVER['SERVER_PORT'] == 443 ? '' : ':'.$_SERVER['SERVER_PORT'];
+    }
+
     $url .= str_replace('\\', '/', dirname($_SERVER['PHP_SELF'])) !== '/' ? str_replace('\\', '/', dirname($_SERVER['PHP_SELF'])).'/' : '/';
 
     return $url;
@@ -53,3 +59,9 @@ function is_secure_connection()
 {
     return ! empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off';
 }
+
+function write_debug_file() {
+    if (DEBUG_MODE) {
+        file_put_contents(DEBUG_FILENAME, implode(PHP_EOL, Logger::getMessages()), FILE_APPEND|LOCK_EX);
+    }
+}
diff --git a/app/helpers/config.php b/app/helpers/config.php
new file mode 100644
index 0000000..e4f4ceb
--- /dev/null
+++ b/app/helpers/config.php
@@ -0,0 +1,38 @@
+getConfig();
+    $value = null;
+
+    if (empty($cache)) {
+        $cache = Model\Config\get_all($session->getUserId());
+        $session->setConfig($cache);
+    }
+
+    if (array_key_exists($parameter, $cache)) {
+        $value = $cache[$parameter];
+    }
+
+    if ($value === null) {
+        $value = $default;
+    }
+
+    return $value;
+}
+
+function bool_config($parameter, $default = false)
+{
+    return (bool) config($parameter, $default);
+}
+
+function int_config($parameter, $default = false)
+{
+    return (int) config($parameter, $default);
+}
diff --git a/app/helpers/favicon.php b/app/helpers/favicon.php
index 9d5a669..77a3456 100644
--- a/app/helpers/favicon.php
+++ b/app/helpers/favicon.php
@@ -22,7 +22,7 @@ function favicon_extension($type)
 function favicon(array $favicons, $feed_id)
 {
     if (! empty($favicons[$feed_id])) {
-        return '';
+        return '';
     }
 
     return '';
diff --git a/app/helpers/template.php b/app/helpers/template.php
index a0cf7b4..9c0d6fd 100644
--- a/app/helpers/template.php
+++ b/app/helpers/template.php
@@ -2,7 +2,17 @@
 
 namespace Miniflux\Helper;
 
-use Miniflux\Model\Config;
+use Miniflux\Session\SessionStorage;
+
+function get_user_id()
+{
+    return SessionStorage::getInstance()->getUserId();
+}
+
+function is_admin()
+{
+    return SessionStorage::getInstance()->isAdmin();
+}
 
 function flash($type, $html)
 {
@@ -16,14 +26,18 @@ function flash($type, $html)
     return $data;
 }
 
-function is_rtl(array $item)
+function rtl(array $item)
 {
-    return ! empty($item['rtl']) || \PicoFeed\Parser\Parser::isLanguageRTL($item['language']);
+    if ($item['rtl'] == 1) {
+        return 'dir="rtl"';
+    }
+
+    return 'dir="ltr"';
 }
 
 function css()
 {
-    $theme = Config\get('theme');
+    $theme = config('theme');
 
     if ($theme !== 'original') {
         $css_file = THEME_DIRECTORY.'/'.$theme.'/css/app.css';
diff --git a/app/locales/ar_AR/translations.php b/app/locales/ar_AR/translations.php
index 3abbf85..088dd3b 100644
--- a/app/locales/ar_AR/translations.php
+++ b/app/locales/ar_AR/translations.php
@@ -157,7 +157,6 @@ return array(
     'Auto-Update URL' => ':تحديث تلقائي للميني فلكس من الرابط',
     'Update Miniflux' => 'تحديث برنامج Miniflux',
     'Miniflux is updated!' => 'بنجاح! Miniflux تمت عملية تحديث برنامج',
-    'Unable to update Miniflux, check the console for errors.' => 'غير قادر على تحديث برنامج Miniflux لمزيد من المعلومات يرجى الذهاب إلى نافذة رسائل الإشعارات',
     'Don\'t forget to backup your database' => 'لاتنسى إنشاء نسخة إحتياطية من قاعدة البيانات',
     'The name must have only alpha-numeric characters' => 'يجب إدخال أحرف أو أرقام فقط',
     'New database' => 'إنشاء قاعدة بيانات جديده',
@@ -200,7 +199,6 @@ return array(
     'about' => 'حول البرنامج',
     'This action will update Miniflux with the last development version, are you sure?' => 'سيتم إستبدال هذه النسخة من برنامج ميني فلكس بأحدث نسخه ... هل أنت متأكد من انك تريد ذلك؟ ?',
     'database' => 'قاعدة البيانات',
-    'Console' => 'Console',
     'Miniflux API' => 'Miniflux API',
     'menu' => 'قائمة',
     'Default' => 'إفتراضي',
diff --git a/app/locales/cs_CZ/translations.php b/app/locales/cs_CZ/translations.php
index 0a9bf6d..ab84c53 100644
--- a/app/locales/cs_CZ/translations.php
+++ b/app/locales/cs_CZ/translations.php
@@ -157,7 +157,6 @@ return array(
     'Auto-Update URL' => 'URL automatické aktualizace',
     'Update Miniflux' => 'Aktualizovat Miniflux',
     'Miniflux is updated!' => 'Miniflux je aktualizovaný!',
-    'Unable to update Miniflux, check the console for errors.' => 'Nelze aktualizovat Miniflux, zkontrolujte konzoli na chyby.',
     'Don\'t forget to backup your database' => 'Nezapomeňte zálohovat vaši databázi',
     'The name must have only alpha-numeric characters' => 'Jméno smí obsahovat pouze písmena a číslice',
     'New database' => 'Nová databáze',
@@ -200,7 +199,6 @@ return array(
     'about' => 'o',
     'This action will update Miniflux with the last development version, are you sure?' => 'Tato akce aktualizuje Miniflux na poslední vývojovou verzi. Jste si jistí?',
     'database' => 'databáze',
-    'Console' => 'konzole',
     'Miniflux API' => 'Miniflux API',
     'menu' => 'nabídka',
     'Default' => 'Výchozí',
diff --git a/app/locales/de_DE/translations.php b/app/locales/de_DE/translations.php
index 10b59d9..2b155fb 100644
--- a/app/locales/de_DE/translations.php
+++ b/app/locales/de_DE/translations.php
@@ -157,7 +157,6 @@ return array(
     'Auto-Update URL' => 'Auto-Update URL',
     'Update Miniflux' => 'Miniflux aktualisieren',
     'Miniflux is updated!' => 'Miniflux wurde erfolgreich aktualisiert!',
-    'Unable to update Miniflux, check the console for errors.' => 'Aktualisierung von Miniflux fehlgeschlagen, überprüfe die Konsole nach Fehlermeldungen.',
     'Don\'t forget to backup your database' => 'Vergiss nicht, die Datenbank zu sichern',
     'The name must have only alpha-numeric characters' => 'Der Name darf nur alphanumerische Zeichen enthalten',
     'New database' => 'Neue Datenbank',
@@ -200,7 +199,6 @@ return array(
     'about' => 'über',
     'This action will update Miniflux with the last development version, are you sure?' => 'Miniflux wird auf die aktuelle Entwicklungsversion aktualisiert. Bist du sicher?',
     'database' => 'Datenbank',
-    'Console' => 'Konsole',
     'Miniflux API' => 'Miniflux API',
     'menu' => 'Menü',
     'Default' => 'Standard',
diff --git a/app/locales/es_ES/translations.php b/app/locales/es_ES/translations.php
index 5f8fb56..42e6c03 100644
--- a/app/locales/es_ES/translations.php
+++ b/app/locales/es_ES/translations.php
@@ -157,7 +157,6 @@ return array(
     'Auto-Update URL' => 'Actualizar automáticamente la URL',
     'Update Miniflux' => 'Actualizar Miniflux',
     'Miniflux is updated!' => 'Miniflux esta actualizado',
-    'Unable to update Miniflux, check the console for errors.' => 'No ha sido posible actualizar Miniflux, lea los errores en la consola',
     'Don\'t forget to backup your database' => 'No olvides de hacer una copia de seguridad de la base de datos',
     'The name must have only alpha-numeric characters' => 'El nombre sólo puede contener caractéres alfanuméricos',
     'New database' => 'Nueva base de datos',
@@ -200,7 +199,6 @@ return array(
     'about' => 'acerca de',
     'This action will update Miniflux with the last development version, are you sure?' => 'Esta acción actualizará Miniflux a la última versión de desarrollo, ¿está seguro?',
     'database' => 'base de datos',
-    'Console' => 'Consola',
     'Miniflux API' => 'API Miniflux',
     'menu' => 'menú',
     'Default' => 'Por defecto',
diff --git a/app/locales/fr_FR/translations.php b/app/locales/fr_FR/translations.php
index 08f4a15..d74edf6 100644
--- a/app/locales/fr_FR/translations.php
+++ b/app/locales/fr_FR/translations.php
@@ -157,7 +157,6 @@ return array(
     'Auto-Update URL' => 'URL de mise à jour automatique',
     'Update Miniflux' => 'Mettre à jour Miniflux',
     'Miniflux is updated!' => 'Miniflux a été mis à jour avec succès !',
-    'Unable to update Miniflux, check the console for errors.' => 'Impossible de mettre à jour Miniflux, allez-voir les erreurs dans la console.',
     'Don\'t forget to backup your database' => 'N\'oubliez pas de sauvegarder votre base de données',
     'The name must have only alpha-numeric characters' => 'Le nom doit avoir seulement des caractères alphanumériques',
     'New database' => 'Nouvelle base de données',
@@ -200,7 +199,6 @@ return array(
     'about' => 'a propos',
     'This action will update Miniflux with the last development version, are you sure?' => 'Cette action va mettre à jour Miniflux avec la dernière version en cours de développement, êtes-vous certain ?',
     'database' => 'base de données',
-    'Console' => 'Console',
     'Miniflux API' => 'Miniflux API',
     'menu' => 'menu',
     'Default' => 'Défaut',
diff --git a/app/locales/it_IT/translations.php b/app/locales/it_IT/translations.php
index 5fc5341..1c6869f 100644
--- a/app/locales/it_IT/translations.php
+++ b/app/locales/it_IT/translations.php
@@ -157,7 +157,6 @@ return array(
     // 'Auto-Update URL' => '',
     // 'Update Miniflux' => '',
     // 'Miniflux is updated!' => '',
-    // 'Unable to update Miniflux, check the console for errors.' => '',
     // 'Don\'t forget to backup your database' => '',
     // 'The name must have only alpha-numeric characters' => '',
     // 'New database' => '',
@@ -200,7 +199,6 @@ return array(
     // 'about' => '',
     // 'This action will update Miniflux with the last development version, are you sure?' => '',
     // 'database' => '',
-    // 'Console' => '',
     // 'Miniflux API' => '',
     // 'menu' => '',
     // 'Default' => '',
diff --git a/app/locales/ja_JP/translations.php b/app/locales/ja_JP/translations.php
index 44f9a58..17c534a 100644
--- a/app/locales/ja_JP/translations.php
+++ b/app/locales/ja_JP/translations.php
@@ -159,7 +159,6 @@ return array(
     'Auto-Update URL' => '自動更新のURL',
     'Update Miniflux' => 'Minifluxを更新',
     'Miniflux is updated!' => 'Minifluxは更新されました!',
-    'Unable to update Miniflux, check the console for errors.' => 'Minifluxを更新できません。エラーコンソールを確認してください。',
     'Don\'t forget to backup your database' => 'データベースのバックアップを忘れないで下さい',
     'The name must have only alpha-numeric characters' => '名前には英数字のみを使用することが出来ます',
     'New database' => '新しいデータベース',
@@ -202,7 +201,6 @@ return array(
     'about' => 'Minifluxについて',
     'This action will update Miniflux with the last development version, are you sure?' => '最新の開発バージョンでMinifluxを更新します。よろしいですか?',
     'database' => 'データベース',
-    'Console' => 'コンソール',
     'Miniflux API' => 'Miniflux API',
     'menu' => 'メニュー',
     'Default' => 'デフォルト',
diff --git a/app/locales/pt_BR/translations.php b/app/locales/pt_BR/translations.php
index 7da3e3f..aa306d6 100644
--- a/app/locales/pt_BR/translations.php
+++ b/app/locales/pt_BR/translations.php
@@ -157,7 +157,6 @@ return array(
     'Auto-Update URL' => 'URL de atualização automática',
     'Update Miniflux' => 'Atualizar Miniflux',
     'Miniflux is updated!' => 'Miniflux foi atualizado!',
-    'Unable to update Miniflux, check the console for errors.' => 'Incapaz de atualizar Miniflux, verifique o console para erros',
     'Don\'t forget to backup your database' => 'Não esqueça de fazer backup de seu banco de dados',
     'The name must have only alpha-numeric characters' => 'O nome deve conter apenas caracteres alfa-numéricos',
     'New database' => 'Novo banco de dados',
@@ -200,7 +199,6 @@ return array(
     'about' => 'sobre',
     'This action will update Miniflux with the last development version, are you sure?' => 'Esta ação irá atualizar o Miniflux com a última versão de desenvolvimento, você tem certeza?',
     'database' => 'banco de dados',
-    'Console' => 'Console',
     'Miniflux API' => 'API do Miniflux',
     'menu' => 'menu',
     'Default' => 'Padrão',
diff --git a/app/locales/ru_RU/translations.php b/app/locales/ru_RU/translations.php
index 30bb13d..cd370d8 100644
--- a/app/locales/ru_RU/translations.php
+++ b/app/locales/ru_RU/translations.php
@@ -157,7 +157,6 @@ return array(
     'Auto-Update URL' => 'URL автоматического обновления',
     'Update Miniflux' => 'Обновить Miniflux',
     'Miniflux is updated!' => 'Miniflux обновлен!',
-    'Unable to update Miniflux, check the console for errors.' => 'Невозможно обновить Miniflux, смотрите ошибки в консоле.',
     'Don\'t forget to backup your database' => 'Не забудьте предварительно сделать резервную копию базы данных',
     'The name must have only alpha-numeric characters' => 'Название должно состоять только из алфавитно-цифровых символов',
     'New database' => 'Новая база данных',
@@ -200,7 +199,6 @@ return array(
     'about' => 'о программе',
     'This action will update Miniflux with the last development version, are you sure?' => 'Это действие обновит Miniflux до последней разрабатываемой версии, вы уверены?',
     'database' => 'база данных',
-    'Console' => 'Консоль',
     'Miniflux API' => 'Miniflux API',
     'menu' => 'меню',
     'Default' => 'По-умолчанию',
diff --git a/app/locales/sr_RS/translations.php b/app/locales/sr_RS/translations.php
index c1791bd..7a83e83 100644
--- a/app/locales/sr_RS/translations.php
+++ b/app/locales/sr_RS/translations.php
@@ -157,7 +157,6 @@ return array(
     'Auto-Update URL' => 'УРЛ за аутоматско ажурирање',
     'Update Miniflux' => 'Ажурирај Минифлукс',
     'Miniflux is updated!' => 'Минифлукс је успешно ажуриран !',
-    'Unable to update Miniflux, check the console for errors.' => 'Неуспешно ажурирање Минифлукса, проверите конзолу за списак грешака.',
     'Don\'t forget to backup your database' => 'Не заборавите да бекапујете базу података',
     'The name must have only alpha-numeric characters' => 'Име може садржати само бројеве или слова',
     'New database' => 'Нова база података',
@@ -200,7 +199,6 @@ return array(
     'about' => 'о програму',
     'This action will update Miniflux with the last development version, are you sure?' => 'Ова акција ће ажурирати Минифлукс на најновију развојну верију, да ли сте сигурни?',
     'database' => 'база података',
-    'Console' => 'Конзола',
     'Miniflux API' => 'АПИ Минифлукса',
     'menu' => 'мени',
     'Default' => 'Основна',
diff --git a/app/locales/sr_RS@latin/translations.php b/app/locales/sr_RS@latin/translations.php
index 5e1651b..b745f9e 100644
--- a/app/locales/sr_RS@latin/translations.php
+++ b/app/locales/sr_RS@latin/translations.php
@@ -157,7 +157,6 @@ return array(
     'Auto-Update URL' => 'URL za automatsko ažuriranje',
     'Update Miniflux' => 'Ažuriraj Miniflux',
     'Miniflux is updated!' => 'Miniflux je uspešno ažuriran !',
-    'Unable to update Miniflux, check the console for errors.' => 'Neuspešno ažuriranje Minifluxa, proverite konzolu za spisak grešaka.',
     'Don\'t forget to backup your database' => 'Ne zaboravite da bekapujete bazu podataka',
     'The name must have only alpha-numeric characters' => 'Ime može sadržati samo brojeve ili slova',
     'New database' => 'Nova baza podataka',
@@ -200,7 +199,6 @@ return array(
     'about' => 'o programu',
     'This action will update Miniflux with the last development version, are you sure?' => 'Ova akcija će ažurirati Miniflux na najnoviju razvojnu veriju, da li ste sigurni?',
     'database' => 'baza podataka',
-    'Console' => 'Konzola',
     'Miniflux API' => 'API Minifluxa',
     'menu' => 'meni',
     'Default' => 'Osnovna',
diff --git a/app/locales/tr_TR/translations.php b/app/locales/tr_TR/translations.php
index 40d39f1..d7b1761 100644
--- a/app/locales/tr_TR/translations.php
+++ b/app/locales/tr_TR/translations.php
@@ -157,7 +157,6 @@ return array(
     'Auto-Update URL' => 'Otomatik güncelleme bağlantısı',
     'Update Miniflux' => 'Miniflux\'ı Güncelle',
     'Miniflux is updated!' => 'Miniflux güncellendi!',
-    'Unable to update Miniflux, check the console for errors.' => 'Miniflux güncellenemiyor, hataları konsol üzerinden kontrol edin.',
     'Don\'t forget to backup your database' => 'Veritabanınızı yedeklemeyi unutmayın',
     'The name must have only alpha-numeric characters' => 'İsim yalnızca alfanümerik karakterler içermeli',
     'New database' => 'Yeni veritabanı',
@@ -200,7 +199,6 @@ return array(
     'about' => 'hakkında',
     'This action will update Miniflux with the last development version, are you sure?' => 'Bu işlem Miniflux\'u en son yayınlanan geliştirme sürümüne güncelleyecektir, emin misiniz?',
     'database' => 'veritabanı',
-    'Console' => 'Konsol',
     'Miniflux API' => 'Miniflux API',
     'menu' => 'menü',
     'Default' => 'Varsayılan',
diff --git a/app/locales/zh_CN/translations.php b/app/locales/zh_CN/translations.php
index aab4bc4..1cefa92 100644
--- a/app/locales/zh_CN/translations.php
+++ b/app/locales/zh_CN/translations.php
@@ -157,7 +157,6 @@ return array(
     'Auto-Update URL' => '自动更新URL',
     'Update Miniflux' => '更新Miniflux',
     'Miniflux is updated!' => 'Miniflux已被更新!',
-    'Unable to update Miniflux, check the console for errors.' => '无法更新Miniflux,检查控制台上的错误',
     'Don\'t forget to backup your database' => '不要忘记备份你的数据库',
     'The name must have only alpha-numeric characters' => '名字只能包含字母和数字',
     'New database' => '新数据库',
@@ -200,7 +199,6 @@ return array(
     'about' => '关于',
     'This action will update Miniflux with the last development version, are you sure?' => '这个操作将更新Miniflux到最新的开发版,你确认吗?',
     'database' => '数据库',
-    'Console' => '控制台',
     'Miniflux API' => 'Miniflux API',
     'menu' => '菜单',
     'Default' => '默认',
diff --git a/app/models/auto_update.php b/app/models/auto_update.php
index 5bcf7d9..b45880c 100644
--- a/app/models/auto_update.php
+++ b/app/models/auto_update.php
@@ -48,8 +48,6 @@ function is_excluded_path($path, array $exclude_list)
 // Synchronize 2 directories (copy/remove files)
 function synchronize($source_directory, $destination_directory)
 {
-    Config\debug('[SYNCHRONIZE] '.$source_directory.' to '.$destination_directory);
-
     $src_files = get_files_list($source_directory);
     $dst_files = get_files_list($destination_directory);
 
@@ -59,7 +57,6 @@ function synchronize($source_directory, $destination_directory)
     foreach ($remove_files as $file) {
         if ($file !== '.htaccess') {
             $destination_file = $destination_directory.DIRECTORY_SEPARATOR.$file;
-            Config\debug('[REMOVE] '.$destination_file);
 
             if (! @unlink($destination_file)) {
                 return false;
@@ -72,8 +69,6 @@ function synchronize($source_directory, $destination_directory)
         $directory = $destination_directory.DIRECTORY_SEPARATOR.dirname($file);
 
         if (! is_dir($directory)) {
-            Config\debug('[MKDIR] '.$directory);
-
             if (! @mkdir($directory, 0755, true)) {
                 return false;
             }
@@ -82,8 +77,6 @@ function synchronize($source_directory, $destination_directory)
         $source_file = $source_directory.DIRECTORY_SEPARATOR.$file;
         $destination_file = $destination_directory.DIRECTORY_SEPARATOR.$file;
 
-        Config\debug('[COPY] '.$source_file.' to '.$destination_file);
-
         if (! @copy($source_file, $destination_file)) {
             return false;
         }
@@ -96,9 +89,6 @@ function synchronize($source_directory, $destination_directory)
 function uncompress_archive($url, $download_directory = AUTO_UPDATE_DOWNLOAD_DIRECTORY, $archive_directory = AUTO_UPDATE_ARCHIVE_DIRECTORY)
 {
     $archive_file = $download_directory.DIRECTORY_SEPARATOR.'update.zip';
-
-    Config\debug('[DOWNLOAD] '.$url);
-
     if (($data = @file_get_contents($url)) === false) {
         return false;
     }
@@ -107,8 +97,6 @@ function uncompress_archive($url, $download_directory = AUTO_UPDATE_DOWNLOAD_DIR
         return false;
     }
 
-    Config\debug('[UNZIP] '.$archive_file);
-
     $zip = new ZipArchive;
 
     if (! $zip->open($archive_file)) {
@@ -124,8 +112,6 @@ function uncompress_archive($url, $download_directory = AUTO_UPDATE_DOWNLOAD_DIR
 // Remove all files for a given directory
 function cleanup_directory($directory)
 {
-    Config\debug('[CLEANUP] '.$directory);
-
     $dir = new DirectoryIterator($directory);
 
     foreach ($dir as $fileinfo) {
@@ -133,7 +119,6 @@ function cleanup_directory($directory)
             $filename = $fileinfo->getRealPath();
 
             if ($fileinfo->isFile()) {
-                Config\debug('[REMOVE] '.$filename);
                 @unlink($filename);
             } else {
                 cleanup_directory($filename);
@@ -165,14 +150,10 @@ function find_archive_root($base_directory = AUTO_UPDATE_ARCHIVE_DIRECTORY)
     }
 
     if (empty($directory)) {
-        Config\debug('[FIND ARCHIVE] No directory found');
         return false;
     }
 
-    $path = $base_directory.DIRECTORY_SEPARATOR.$directory;
-    Config\debug('[FIND ARCHIVE] '.$path);
-
-    return $path;
+    return $base_directory.DIRECTORY_SEPARATOR.$directory;
 }
 
 // Check if everything is setup correctly
diff --git a/app/models/bookmark.php b/app/models/bookmark.php
index aa4fbab..3f08768 100644
--- a/app/models/bookmark.php
+++ b/app/models/bookmark.php
@@ -2,60 +2,68 @@
 
 namespace Miniflux\Model\Bookmark;
 
+use Miniflux\Helper;
+use Miniflux\Model;
 use PicoDb\Database;
-use Miniflux\Handler\Service;
-use Miniflux\Model\Config;
 
-function count_items($feed_ids = array())
+function count_bookmarked_items($user_id, array $feed_ids = array())
 {
     return Database::getInstance('db')
-        ->table('items')
+        ->table(Model\Item\TABLE)
         ->eq('bookmark', 1)
+        ->eq('user_id', $user_id)
         ->in('feed_id', $feed_ids)
-        ->in('status', array('read', 'unread'))
+        ->in('status', array(Model\Item\STATUS_READ, Model\Item\STATUS_UNREAD))
         ->count();
 }
 
-function get_all_items($offset = null, $limit = null, $feed_ids = array())
+function get_bookmarked_items($user_id, $offset = null, $limit = null, array $feed_ids = array())
 {
     return Database::getInstance('db')
-        ->table('items')
+        ->table(Model\Item\TABLE)
         ->columns(
             'items.id',
+            'items.checksum',
             'items.title',
             'items.updated',
             'items.url',
-            'items.enclosure',
+            'items.enclosure_url',
             'items.enclosure_type',
             'items.bookmark',
             'items.status',
             'items.content',
             'items.feed_id',
             'items.language',
+            'items.rtl',
             'items.author',
             'feeds.site_url',
-            'feeds.title AS feed_title',
-            'feeds.rtl'
+            'feeds.title AS feed_title'
         )
-        ->join('feeds', 'id', 'feed_id')
-        ->in('feed_id', $feed_ids)
-        ->in('status', array('read', 'unread'))
-        ->eq('bookmark', 1)
-        ->orderBy('updated', Config\get('items_sorting_direction'))
+        ->join(Model\Feed\TABLE, 'id', 'feed_id')
+        ->eq('items.user_id', $user_id)
+        ->in('items.feed_id', $feed_ids)
+        ->neq('items.status', Model\Item\STATUS_REMOVED)
+        ->eq('items.bookmark', 1)
+        ->orderBy('items.updated', Helper\config('items_sorting_direction'))
         ->offset($offset)
         ->limit($limit)
         ->findAll();
 }
 
-function set_flag($id, $value)
+function get_bookmarked_item_ids($user_id)
 {
-    if ($value == 1) {
-        Service\sync($id);
-    }
-
     return Database::getInstance('db')
-        ->table('items')
-        ->eq('id', $id)
-        ->in('status', array('read', 'unread'))
-        ->save(array('bookmark' => $value));
+        ->table(Model\Item\TABLE)
+        ->eq('user_id', $user_id)
+        ->eq('bookmark', 1)
+        ->findAllByColumn('id');
+}
+
+function set_flag($user_id, $item_id, $value)
+{
+    return Database::getInstance('db')
+        ->table(Model\Item\TABLE)
+        ->eq('user_id', $user_id)
+        ->eq('id', $item_id)
+        ->update(array('bookmark' => (int) $value));
 }
diff --git a/app/models/config.php b/app/models/config.php
index 87a13e9..043e29c 100644
--- a/app/models/config.php
+++ b/app/models/config.php
@@ -3,46 +3,12 @@
 namespace Miniflux\Model\Config;
 
 use Miniflux\Helper;
-use Miniflux\Translator;
+use Miniflux\Model;
 use DirectoryIterator;
+use Miniflux\Session\SessionStorage;
 use PicoDb\Database;
-use PicoFeed\Config\Config as ReaderConfig;
-use PicoFeed\Logging\Logger;
 
-const HTTP_USER_AGENT = 'Miniflux (https://miniflux.net)';
-
-// Get PicoFeed config
-function get_reader_config()
-{
-    $config = new ReaderConfig;
-    $config->setTimezone(get('timezone'));
-
-    // Client
-    $config->setClientTimeout(HTTP_TIMEOUT);
-    $config->setClientUserAgent(HTTP_USER_AGENT);
-    $config->setMaxBodySize(HTTP_MAX_RESPONSE_SIZE);
-
-    // Grabber
-    $config->setGrabberRulesFolder(RULES_DIRECTORY);
-
-    // Proxy
-    $config->setProxyHostname(PROXY_HOSTNAME);
-    $config->setProxyPort(PROXY_PORT);
-    $config->setProxyUsername(PROXY_USERNAME);
-    $config->setProxyPassword(PROXY_PASSWORD);
-
-    // Filter
-    $config->setFilterIframeWhitelist(get_iframe_whitelist());
-
-    if ((bool) get('debug_mode')) {
-        Logger::enable();
-    }
-
-    // Parser
-    $config->setParserHashAlgo('crc32b');
-
-    return $config;
-}
+const TABLE = 'user_settings';
 
 function get_iframe_whitelist()
 {
@@ -56,60 +22,41 @@ function get_iframe_whitelist()
     );
 }
 
-// Send a debug message to the console
-function debug($line)
-{
-    Logger::setMessage($line);
-    write_debug();
-}
-
-// Write PicoFeed debug output to a file
-function write_debug()
-{
-    if ((bool) get('debug_mode')) {
-        file_put_contents(DEBUG_FILENAME, implode(PHP_EOL, Logger::getMessages()));
-    }
-}
-
-// Get available timezone
 function get_timezones()
 {
     $timezones = timezone_identifiers_list();
     return array_combine(array_values($timezones), $timezones);
 }
 
-// Returns true if the language is RTL
 function is_language_rtl()
 {
     $languages = array(
         'ar_AR'
     );
 
-    return in_array(get('language'), $languages);
+    return in_array(Helper\config('language'), $languages);
 }
 
-// Get all supported languages
 function get_languages()
 {
     return array(
-        'ar_AR' => 'عربي',
-        'cs_CZ' => 'Čeština',
-        'de_DE' => 'Deutsch',
-        'en_US' => 'English',
-        'es_ES' => 'Español',
-        'fr_FR' => 'Français',
-        'it_IT' => 'Italiano',
-        'ja_JP' => '日本語',
-        'pt_BR' => 'Português',
-        'zh_CN' => '简体中国',
-        'sr_RS' => 'српски',
+        'ar_AR'       => 'عربي',
+        'cs_CZ'       => 'Čeština',
+        'de_DE'       => 'Deutsch',
+        'en_US'       => 'English',
+        'es_ES'       => 'Español',
+        'fr_FR'       => 'Français',
+        'it_IT'       => 'Italiano',
+        'ja_JP'       => '日本語',
+        'pt_BR'       => 'Português',
+        'zh_CN'       => '简体中国',
+        'sr_RS'       => 'српски',
         'sr_RS@latin' => 'srpski',
-        'ru_RU' => 'Русский',
-        'tr_TR' => 'Türkçe',
+        'ru_RU'       => 'Русский',
+        'tr_TR'       => 'Türkçe',
     );
 }
 
-// Get all skins
 function get_themes()
 {
     $themes = array(
@@ -129,52 +76,47 @@ function get_themes()
     return $themes;
 }
 
-// Sorting direction choices for items
 function get_sorting_directions()
 {
     return array(
-        'asc' => t('Older items first'),
+        'asc'  => t('Older items first'),
         'desc' => t('Most recent first'),
     );
 }
 
-// Display summaries or full contents on lists
 function get_display_mode()
 {
     return array(
-        'titles' => t('Titles'),
+        'titles'    => t('Titles'),
         'summaries' => t('Summaries'),
-        'full' => t('Full contents')
+        'full'      => t('Full contents'),
     );
 }
 
-// Item title links to original or full contents
 function get_item_title_link()
 {
     return array(
         'original' => t('Original'),
-        'full' => t('Full contents')
+        'full'     => t('Full contents'),
     );
 }
 
-// Autoflush choices for read items
 function get_autoflush_read_options()
 {
     return array(
-        '0' => t('Never'),
+        '0'  => t('Never'),
         '-1' => t('Immediately'),
-        '1' => t('After %d day', 1),
-        '5' => t('After %d day', 5),
+        '1'  => t('After %d day', 1),
+        '5'  => t('After %d day', 5),
         '15' => t('After %d day', 15),
-        '30' => t('After %d day', 30)
+        '30' => t('After %d day', 30),
     );
 }
 
-// Autoflush choices for unread items
 function get_autoflush_unread_options()
 {
     return array(
-        '0' => t('Never'),
+        '0'  => t('Never'),
         '15' => t('After %d day', 15),
         '30' => t('After %d day', 30),
         '45' => t('After %d day', 45),
@@ -182,14 +124,13 @@ function get_autoflush_unread_options()
     );
 }
 
-// Number of items per pages
 function get_paging_options()
 {
     return array(
-        10 => 10,
-        20 => 20,
-        30 => 30,
-        50 => 50,
+        10  => 10,
+        20  => 20,
+        30  => 30,
+        50  => 50,
         100 => 100,
         150 => 150,
         200 => 200,
@@ -197,84 +138,96 @@ function get_paging_options()
     );
 }
 
-// Get redirect options when there is nothing to read
 function get_nothing_to_read_redirections()
 {
     return array(
-        'feeds' => t('Subscriptions'),
-        'history' => t('History'),
+        'feeds'     => t('Subscriptions'),
+        'history'   => t('History'),
         'bookmarks' => t('Bookmarks'),
     );
 }
 
-
-// Regenerate tokens for the API and bookmark feed
-function new_tokens()
+function get_default_values()
 {
-    $values = array(
-        'api_token' => Helper\generate_token(),
-        'feed_token' => Helper\generate_token(),
-        'bookmarklet_token' => Helper\generate_token(),
-        'fever_token' => substr(Helper\generate_token(), 0, 8),
+    return array(
+        'language'                      => 'en_US',
+        'timezone'                      => 'UTC',
+        'theme'                         => 'original',
+        'autoflush'                     => 15,
+        'autoflush_unread'              => 45,
+        'frontend_updatecheck_interval' => 10,
+        'favicons'                      => 1,
+        'nocontent'                     => 0,
+        'image_proxy'                   => 0,
+        'original_marks_read'           => 1,
+        'instapaper_enabled'            => 0,
+        'pinboard_enabled'              => 0,
+        'pinboard_tags'                 => 'miniflux',
+        'items_per_page'                => 100,
+        'items_display_mode'            => 'summaries',
+        'items_sorting_direction'       => 'desc',
+        'redirect_nothing_to_read'      => 'feeds',
+        'item_title_link'               => 'full',
     );
-
-    return Database::getInstance('db')->hashtable('settings')->put($values);
 }
 
-// Get a config value from the DB or from the session
-function get($name)
+function get_all($user_id)
 {
-    if (! isset($_SESSION)) {
-        return current(Database::getInstance('db')->hashtable('settings')->get($name));
-    } else {
-        if (! isset($_SESSION['config'][$name])) {
-            $_SESSION['config'] = get_all();
-        }
+    $settings = Database::getInstance('db')
+        ->hashtable(TABLE)
+        ->eq('user_id', $user_id)
+        ->getAll('key', 'value');
 
-        if (isset($_SESSION['config'][$name])) {
-            return $_SESSION['config'][$name];
-        }
+    if (empty($settings)) {
+        save_defaults($user_id);
+        $settings = Database::getInstance('db')
+            ->hashtable(TABLE)
+            ->eq('user_id', $user_id)
+            ->getAll('key', 'value');
     }
 
-    return null;
+    return $settings;
 }
 
-// Get all config parameters
-function get_all()
+function save_defaults($user_id)
 {
-    $config = Database::getInstance('db')->hashtable('settings')->get();
-    unset($config['password']);
-    return $config;
+    return save($user_id, get_default_values());
 }
 
-// Save config into the database and update the session
-function save(array $values)
+function save($user_id, array $values)
 {
-    // Update the password if needed
-    if (! empty($values['password'])) {
-        $values['password'] = password_hash($values['password'], PASSWORD_BCRYPT);
-    } else {
-        unset($values['password']);
-    }
+    $db = Database::getInstance('db');
+    $results = array();
+    $db->startTransaction();
 
-    unset($values['confirmation']);
-
-    // If the user does not want content of feeds, remove it in previous ones
     if (isset($values['nocontent']) && (bool) $values['nocontent']) {
-        Database::getInstance('db')->table('items')->update(array('content' => ''));
+        $db
+            ->table(Model\Item\TABLE)
+            ->eq('user_id', $user_id)
+            ->update(array('content' => ''));
     }
 
-    if (Database::getInstance('db')->hashtable('settings')->put($values)) {
-        reload();
-        return true;
+    foreach ($values as $key => $value) {
+        if ($db->table(TABLE)->eq('user_id', $user_id)->eq('key', $key)->exists()) {
+            $results[] = $db->table(TABLE)
+                ->eq('user_id', $user_id)
+                ->eq('key', $key)
+                ->update(array('value' => $value));
+        } else {
+            $results[] = $db->table(TABLE)->insert(array(
+                'key'     => $key,
+                'value'   => $value,
+                'user_id' => $user_id,
+            ));
+        }
     }
 
-    return false;
-}
+    if (in_array(false, $results, true)) {
+        $db->cancelTransaction();
+        return false;
+    }
 
-// Reload the cache in session
-function reload()
-{
-    $_SESSION['config'] = get_all();
-    Translator\load(get('language'));
+    $db->closeTransaction();
+    SessionStorage::getInstance()->flushConfig();
+    return true;
 }
diff --git a/app/models/database.php b/app/models/database.php
deleted file mode 100644
index 1f21980..0000000
--- a/app/models/database.php
+++ /dev/null
@@ -1,102 +0,0 @@
- 'sqlite',
-            'filename' => $filename,
-        ));
-
-        if ($db->schema('\Miniflux\Schema')->check(Schema\VERSION)) {
-            $credentials = array(
-                'username' => $username,
-                'password' => password_hash($password, PASSWORD_BCRYPT)
-            );
-
-            $db->hashtable('settings')->put($credentials);
-
-            return true;
-        }
-    }
-
-    return false;
-}
-
-// Get or set the current database
-function select($filename = '')
-{
-    static $current_filename = DB_FILENAME;
-
-    // function gets called with a filename at least once the database
-    // connection is established
-    if (! empty($filename)) {
-        if (ENABLE_MULTIPLE_DB && in_array($filename, get_all())) {
-            $current_filename = $filename;
-
-            // unset the authenticated flag if the database is changed
-            if (empty($_SESSION['database']) || $_SESSION['database'] !== $filename) {
-                if (isset($_SESSION)) {
-                    unset($_SESSION['loggedin']);
-                }
-
-                $_SESSION['database'] = $filename;
-                $_SESSION['config'] = Config\get_all();
-            }
-        } else {
-            return false;
-        }
-    }
-
-    return $current_filename;
-}
-
-// Get database path
-function get_path()
-{
-    return DATA_DIRECTORY.DIRECTORY_SEPARATOR.select();
-}
-
-// Get the list of available databases
-function get_all()
-{
-    $listing = array();
-
-    $dir = new DirectoryIterator(DATA_DIRECTORY);
-
-    foreach ($dir as $fileinfo) {
-	$filename = $fileinfo->getFilename();
-        if (preg_match('/sqlite$/', $filename)) {
-            $listing[] = $filename;
-        }
-    }
-
-    return $listing;
-}
-
-// Get the formated db list
-function get_list()
-{
-    $listing = array();
-
-    foreach (get_all() as $filename) {
-        if ($filename === DB_FILENAME) {
-            $label = t('Default database');
-        } else {
-            $label = ucfirst(substr($filename, 0, -7));
-        }
-
-        $listing[$filename] = $label;
-    }
-
-    return $listing;
-}
diff --git a/app/models/favicon.php b/app/models/favicon.php
index 75cedcf..047d0c8 100644
--- a/app/models/favicon.php
+++ b/app/models/favicon.php
@@ -2,44 +2,38 @@
 
 namespace Miniflux\Model\Favicon;
 
-use Miniflux\Model\Config;
-use Miniflux\Model\Group;
 use Miniflux\Helper;
+use Miniflux\Model;
 use PicoDb\Database;
 use PicoFeed\Reader\Favicon;
 
-// Create a favicons
+const TABLE      = 'favicons';
+const JOIN_TABLE = 'favicons_feeds';
+
 function create_feed_favicon($feed_id, $site_url, $icon_link)
 {
-    if (has_favicon($feed_id)) {
-        return true;
-    }
-
-    $favicon = fetch($feed_id, $site_url, $icon_link);
-
+    $favicon = fetch_favicon($feed_id, $site_url, $icon_link);
     if ($favicon === false) {
         return false;
     }
 
-    $favicon_id = store($favicon->getType(), $favicon->getContent());
-
+    $favicon_id = store_favicon($favicon->getType(), $favicon->getContent());
     if ($favicon_id === false) {
         return false;
     }
 
     return Database::getInstance('db')
-            ->table('favicons_feeds')
-            ->save(array(
-                'feed_id' => $feed_id,
-                'favicon_id' => $favicon_id
-            ));
+        ->table(JOIN_TABLE)
+        ->save(array(
+            'feed_id'    => $feed_id,
+            'favicon_id' => $favicon_id
+        ));
 }
 
-// Download a favicon
-function fetch($feed_id, $site_url, $icon_link)
+function fetch_favicon($feed_id, $site_url, $icon_link)
 {
-    if (Config\get('favicons') == 1 && ! has_favicon($feed_id)) {
-        $favicon = new Favicon;
+    if (Helper\bool_config('favicons') && ! has_favicon($feed_id)) {
+        $favicon = new Favicon();
         $favicon->find($site_url, $icon_link);
         return $favicon;
     }
@@ -47,145 +41,125 @@ function fetch($feed_id, $site_url, $icon_link)
     return false;
 }
 
-// Store the favicon (only if it does not exist yet)
-function store($type, $icon)
+function store_favicon($mime_type, $blob)
 {
-    if ($icon === '') {
+    if (empty($blob)) {
         return false;
     }
 
-    $hash = sha1($icon);
-
+    $hash = sha1($blob);
     $favicon_id = get_favicon_id($hash);
 
     if ($favicon_id) {
         return $favicon_id;
     }
 
-    $file = $hash.Helper\favicon_extension($type);
-
-    if (file_put_contents(FAVICON_DIRECTORY.DIRECTORY_SEPARATOR.$file, $icon) === false) {
+    $file = $hash.Helper\favicon_extension($mime_type);
+    if (file_put_contents(FAVICON_DIRECTORY.DIRECTORY_SEPARATOR.$file, $blob) === false) {
         return false;
     }
 
-    $saved = Database::getInstance('db')
-            ->table('favicons')
-            ->save(array(
-                'hash' => $hash,
-                'type' => $type
-            ));
+    return Database::getInstance('db')
+        ->table(TABLE)
+        ->persist(array(
+            'hash' => $hash,
+            'type' => $mime_type
+        ));
+}
 
-    if ($saved === false) {
-        return false;
-    }
-
-    return get_favicon_id($hash);
+function get_favicon_data_url($filename, $mime_type)
+{
+    $blob = base64_encode(file_get_contents(FAVICON_DIRECTORY.DIRECTORY_SEPARATOR.$filename));
+    return sprintf('data:%s;base64,%s', $mime_type, $blob);
 }
 
 function get_favicon_id($hash)
 {
     return Database::getInstance('db')
-          ->table('favicons')
-          ->eq('hash', $hash)
-          ->findOneColumn('id');
+        ->table(TABLE)
+        ->eq('hash', $hash)
+        ->findOneColumn('id');
 }
 
-// Delete the favicon
-function delete_favicon($favicon)
+function delete_favicon(array $favicon)
 {
     unlink(FAVICON_DIRECTORY.DIRECTORY_SEPARATOR.$favicon['hash'].Helper\favicon_extension($favicon['type']));
+
     Database::getInstance('db')
-        ->table('favicons')
+        ->table(TABLE)
         ->eq('hash', $favicon['hash'])
         ->remove();
 }
 
-// Purge orphaned favicons from database
-function purge_favicons()
-{
-    $favicons = Database::getInstance('db')
-                ->table('favicons')
-                ->columns(
-                    'favicons.type',
-                    'favicons.hash',
-                    'favicons_feeds.feed_id'
-                )
-                ->join('favicons_feeds', 'favicon_id', 'id')
-                ->isNull('favicons_feeds.feed_id')
-                ->findAll();
-
-    foreach ($favicons as $favicon) {
-        delete_favicon($favicon);
-    }
-}
-
-// Return true if the feed has a favicon
 function has_favicon($feed_id)
 {
-    return Database::getInstance('db')->table('favicons_feeds')->eq('feed_id', $feed_id)->count() === 1;
+    return Database::getInstance('db')
+        ->table(JOIN_TABLE)
+        ->eq('feed_id', $feed_id)
+        ->exists();
 }
 
-// Get favicons for those feeds
-function get_favicons(array $feed_ids)
+function get_favicons_by_feed_ids(array $feed_ids)
 {
-    if (Config\get('favicons') == 0) {
-        return array();
-    }
-
     $result = array();
 
-    foreach ($feed_ids as $feed_id) {
-        $result[$feed_id] = Database::getInstance('db')
-              ->table('favicons')
-              ->columns(
-                  'favicons.type',
-                  'favicons.hash'
-              )
-              ->join('favicons_feeds', 'favicon_id', 'id')
-              ->eq('favicons_feeds.feed_id', $feed_id)
-              ->findOne();
+    if (! Helper\bool_config('favicons')) {
+        return $result;
+    }
+
+    $favicons = Database::getInstance('db')
+        ->table(TABLE)
+        ->columns(
+            'favicons.type',
+            'favicons.hash',
+            'favicons_feeds.feed_id'
+        )
+        ->join('favicons_feeds', 'favicon_id', 'id')
+        ->in('favicons_feeds.feed_id', $feed_ids)
+        ->findAll();
+
+    foreach ($favicons as $favicon) {
+        $result[$favicon['feed_id']] = $favicon;
     }
 
     return $result;
 }
 
-// Get all favicons for a list of items
-function get_item_favicons(array $items)
+function get_items_favicons(array $items)
 {
     $feed_ids = array();
 
     foreach ($items as $item) {
-        $feed_ids[$item['feed_id']] = $item['feed_id'];
+        $feed_ids[] = $item['feed_id'];
     }
 
-    return get_favicons($feed_ids);
+    return get_favicons_by_feed_ids(array_unique($feed_ids));
 }
 
-// Get all favicons
-function get_all_favicons()
+function get_feeds_favicons(array $feeds)
 {
-    if (Config\get('favicons') == 0) {
-        return array();
+    $feed_ids = array();
+
+    foreach ($feeds as $feed) {
+        $feed_ids[] = $feed['id'];
     }
 
-    $result = Database::getInstance('db')
-            ->table('favicons')
-            ->columns(
-                'favicons_feeds.feed_id',
-                'favicons.type',
-                'favicons.hash'
-            )
-            ->join('favicons_feeds', 'favicon_id', 'id')
-            ->findAll();
-
-    $map = array();
-
-    foreach ($result as $row) {
-        $map[$row['feed_id']] = array(
-        "type" => $row['type'],
-        "hash" => $row['hash']
-      );
-    }
-
-    return $map;
+    return get_favicons_by_feed_ids($feed_ids);
+}
+
+function get_favicons_with_data_url($user_id)
+{
+    $favicons = Database::getInstance('db')
+        ->table(TABLE)
+        ->columns(JOIN_TABLE.'.feed_id', TABLE.'.file', TABLE.'.type')
+        ->join(JOIN_TABLE, 'favicon_id', 'id', TABLE)
+        ->join(Model\Feed\TABLE, 'id', 'feed_id')
+        ->eq(Model\Feed\TABLE.'.user_id', $user_id)
+        ->findAll();
+
+    foreach ($favicons as &$favicon) {
+        $favicon['url'] = get_favicon_data_url($favicon['file'], $favicon['mime_type']);
+    }
+
+    return $favicons;
 }
diff --git a/app/models/feed.php b/app/models/feed.php
index 11a87b3..78a7420 100644
--- a/app/models/feed.php
+++ b/app/models/feed.php
@@ -2,251 +2,65 @@
 
 namespace Miniflux\Model\Feed;
 
-use UnexpectedValueException;
-use Miniflux\Model\Config;
 use Miniflux\Model\Item;
 use Miniflux\Model\Group;
-use Miniflux\Model\Favicon;
-use Miniflux\Helper;
 use PicoDb\Database;
-use PicoFeed\Reader\Reader;
-use PicoFeed\PicoFeedException;
-use PicoFeed\Serialization\SubscriptionListParser;
+use PicoFeed\Parser\Feed;
 
-const LIMIT_ALL = -1;
+const STATUS_ACTIVE   = 1;
+const STATUS_INACTIVE = 0;
+const TABLE           = 'feeds';
 
-// Update feed information
-function update(array $values)
+function create($user_id, Feed $feed, $etag, $last_modified, $rtl = false, $scraper = false, $cloak_referrer = false)
 {
-    Database::getInstance('db')->startTransaction();
-
-    $result = Database::getInstance('db')
-            ->table('feeds')
-            ->eq('id', $values['id'])
-            ->save(array(
-                'title' => $values['title'],
-                'site_url' => $values['site_url'],
-                'feed_url' => $values['feed_url'],
-                'enabled' => $values['enabled'],
-                'rtl' => $values['rtl'],
-                'download_content' => $values['download_content'],
-                'cloak_referrer' => $values['cloak_referrer'],
-                'parsing_error' => 0,
-            ));
-
-    if ($result) {
-        if (! Group\update_feed_groups($values['id'], $values['feed_group_ids'], $values['create_group'])) {
-            Database::getInstance('db')->cancelTransaction();
-            $result = false;
-        }
-    }
-
-    Database::getInstance('db')->closeTransaction();
-
-    return $result;
-}
-
-// Import OPML file
-function import_opml($content)
-{
-    $subscriptionList = SubscriptionListParser::create($content)->parse();
-
-    $db = Database::getInstance('db');
-    $db->startTransaction();
-
-    foreach ($subscriptionList->subscriptions as $subscription) {
-        if (! $db->table('feeds')->eq('feed_url', $subscription->getFeedUrl())->exists()) {
-            $db->table('feeds')->insert(array(
-                'title' => $subscription->getTitle(),
-                'site_url' => $subscription->getSiteUrl(),
-                'feed_url' => $subscription->getFeedUrl(),
-            ));
-
-            if ($subscription->getCategory() !== '') {
-                $feed_id = $db->getLastId();
-                $group_id = Group\get_group_id($subscription->getCategory());
-
-                if (empty($group_id)) {
-                    $group_id = Group\create($subscription->getCategory());
-                }
-
-                Group\add($feed_id, array($group_id));
-            }
-        }
-    }
-
-    $db->closeTransaction();
-    Config\write_debug();
-
-    return true;
-}
-
-// Add a new feed from an URL
-function create($url, $enable_grabber = false, $force_rtl = false, $cloak_referrer = false, $group_ids = array(), $create_group = '')
-{
-    $feed_id = false;
-
     $db = Database::getInstance('db');
 
-    // Discover the feed
-    $reader = new Reader(Config\get_reader_config());
-    $resource = $reader->discover($url);
-
-    // Feed already there
-    if ($db->table('feeds')->eq('feed_url', $resource->getUrl())->count()) {
-        throw new UnexpectedValueException;
+    if ($db->table('feeds')->eq('user_id', $user_id)->eq('feed_url', $feed->getFeedUrl())->exists()) {
+        return -1;
     }
 
-    // Parse the feed
-    $parser = $reader->getParser(
-        $resource->getUrl(),
-        $resource->getContent(),
-        $resource->getEncoding()
-    );
+    $feed_id = $db
+        ->table(TABLE)
+        ->persist(array(
+            'user_id'          => $user_id,
+            'title'            => $feed->getTitle(),
+            'site_url'         => $feed->getSiteUrl(),
+            'feed_url'         => $feed->getFeedUrl(),
+            'download_content' => $scraper ? 1 : 0,
+            'rtl'              => $rtl ? 1 : 0,
+            'etag'             => $etag,
+            'last_modified'    => $last_modified,
+            'last_checked'     => time(),
+            'cloak_referrer'   => $cloak_referrer ? 1 : 0,
+        ));
 
-    if ($enable_grabber) {
-        $parser->enableContentGrabber();
-    }
-
-    $feed = $parser->execute();
-
-    // Save the feed
-    $result = $db->table('feeds')->save(array(
-        'title' => $feed->getTitle(),
-        'site_url' => $feed->getSiteUrl(),
-        'feed_url' => $feed->getFeedUrl(),
-        'download_content' => $enable_grabber ? 1 : 0,
-        'rtl' => $force_rtl ? 1 : 0,
-        'last_modified' => $resource->getLastModified(),
-        'last_checked' => time(),
-        'etag' => $resource->getEtag(),
-        'cloak_referrer' => $cloak_referrer ? 1 : 0,
-    ));
-
-    if ($result) {
-        $feed_id = $db->getLastId();
-
-        Group\update_feed_groups($feed_id, $group_ids, $create_group);
-        Item\update_all($feed_id, $feed->getItems());
-        Favicon\create_feed_favicon($feed_id, $feed->getSiteUrl(), $feed->getIcon());
+    if ($feed_id !== false) {
+        Item\update_feed_items($user_id, $feed_id, $feed->getItems(), $rtl);
     }
 
     return $feed_id;
 }
 
-// Refresh all feeds
-function refresh_all($limit = LIMIT_ALL)
-{
-    foreach (get_ids($limit) as $feed_id) {
-        refresh($feed_id);
-    }
-
-    // Auto-vacuum for people using the cronjob
-    Database::getInstance('db')->getConnection()->exec('VACUUM');
-
-    return true;
-}
-
-// Refresh one feed
-function refresh($feed_id)
-{
-    try {
-        $feed = get($feed_id);
-
-        if (empty($feed)) {
-            return false;
-        }
-
-        $reader = new Reader(Config\get_reader_config());
-
-        $resource = $reader->download(
-            $feed['feed_url'],
-            $feed['last_modified'],
-            $feed['etag']
-        );
-
-        // Update the `last_checked` column each time, HTTP cache or not
-        update_last_checked($feed_id);
-
-        // Feed modified
-        if ($resource->isModified()) {
-            $parser = $reader->getParser(
-                $resource->getUrl(),
-                $resource->getContent(),
-                $resource->getEncoding()
-            );
-
-            if ($feed['download_content']) {
-                $parser->enableContentGrabber();
-
-                // Don't fetch previous items, only new one
-                $parser->setGrabberIgnoreUrls(
-                    Database::getInstance('db')->table('items')->eq('feed_id', $feed_id)->findAllByColumn('url')
-                );
-            }
-
-            $feed = $parser->execute();
-
-            update_cache($feed_id, $resource->getLastModified(), $resource->getEtag());
-
-            Item\update_all($feed_id, $feed->getItems());
-            Favicon\create_feed_favicon($feed_id, $feed->getSiteUrl(), $feed->getIcon());
-        }
-
-        update_parsing_error($feed_id, 0);
-        Config\write_debug();
-
-        return true;
-    } catch (PicoFeedException $e) {
-    }
-
-    update_parsing_error($feed_id, 1);
-    Config\write_debug();
-
-    return false;
-}
-
-// Get the list of feeds ID to refresh
-function get_ids($limit = LIMIT_ALL)
-{
-    $query = Database::getInstance('db')->table('feeds')->eq('enabled', 1)->asc('last_checked');
-
-    if ($limit !== LIMIT_ALL) {
-        $query->limit((int) $limit);
-    }
-
-    return $query->findAllByColumn('id');
-}
-
-// get number of feeds with errors
-function count_failed_feeds()
+function get_feeds($user_id)
 {
     return Database::getInstance('db')
-        ->table('feeds')
-        ->eq('parsing_error', '1')
-        ->count();
-}
-
-// Get all feeds
-function get_all()
-{
-    return Database::getInstance('db')
-        ->table('feeds')
+        ->table(TABLE)
+        ->eq('user_id', $user_id)
         ->asc('title')
         ->findAll();
 }
 
-// Get all feeds with the number unread/total items in the order failed, working, disabled
-function get_all_item_counts()
+function get_feeds_with_items_count($user_id)
 {
     return Database::getInstance('db')
-        ->table('feeds')
+        ->table(TABLE)
         ->columns(
             'feeds.*',
             'SUM(CASE WHEN items.status IN ("unread") THEN 1 ELSE 0 END) as "items_unread"',
             'SUM(CASE WHEN items.status IN ("read", "unread") THEN 1 ELSE 0 END) as "items_total"'
-          )
+        )
         ->join('items', 'feed_id', 'id')
+        ->eq('feeds.user_id', $user_id)
         ->groupBy('feeds.id')
         ->desc('feeds.parsing_error')
         ->desc('feeds.enabled')
@@ -254,98 +68,94 @@ function get_all_item_counts()
         ->findAll();
 }
 
-// Get unread/total count for one feed
-function count_items($feed_id)
+function get_feed_ids($user_id, $limit = null)
 {
-    $counts = Database::getInstance('db')
-        ->table('items')
-        ->columns('status', 'count(*) as item_count')
-        ->in('status', array('read', 'unread'))
-        ->eq('feed_id', $feed_id)
-        ->groupBy('status')
-        ->findAll();
+    $query = Database::getInstance('db')
+        ->table(TABLE)
+        ->eq('user_id', $user_id)
+        ->eq('enabled', STATUS_ACTIVE)
+        ->asc('last_checked')
+        ->asc('id');
 
-    $result = array(
-        'items_unread' => 0,
-        'items_total' => 0,
-    );
-
-    foreach ($counts as &$count) {
-        if ($count['status'] === 'unread') {
-            $result['items_unread'] = (int) $count['item_count'];
-        }
-
-        $result['items_total'] += $count['item_count'];
+    if ($limit !== null) {
+        $query->limit($limit);
     }
 
-    return $result;
+    return $query->findAllByColumn('id');
 }
 
-// Get one feed
-function get($feed_id)
+function get_feed($user_id, $feed_id)
 {
     return Database::getInstance('db')
-        ->table('feeds')
+        ->table(TABLE)
+        ->eq('user_id', $user_id)
         ->eq('id', $feed_id)
         ->findOne();
 }
 
-// Update parsing error column
-function update_parsing_error($feed_id, $value)
+function update_feed($user_id, $feed_id, array $values)
 {
-    Database::getInstance('db')->table('feeds')->eq('id', $feed_id)->save(array('parsing_error' => $value));
+    $db = Database::getInstance('db');
+    $db->startTransaction();
+
+    $feed = $values;
+    unset($feed['id']);
+    unset($feed['group_name']);
+    unset($feed['feed_group_ids']);
+
+    $result = Database::getInstance('db')
+            ->table('feeds')
+            ->eq('user_id', $user_id)
+            ->eq('id', $feed_id)
+            ->save($feed);
+
+    if ($result) {
+        if (isset($values['feed_group_ids']) && isset($values['group_name']) &&
+            ! Group\update_feed_groups($user_id, $values['id'], $values['feed_group_ids'], $values['group_name'])) {
+            $db->cancelTransaction();
+            return false;
+        }
+
+        $db->closeTransaction();
+        return true;
+    }
+
+    $db->cancelTransaction();
+    return false;
 }
 
-// Update last check date
-function update_last_checked($feed_id)
+function change_feed_status($user_id, $feed_id, $status = STATUS_ACTIVE)
 {
-    Database::getInstance('db')
-        ->table('feeds')
+    return Database::getInstance('db')
+        ->table(TABLE)
+        ->eq('user_id', $user_id)
         ->eq('id', $feed_id)
-        ->save(array(
-            'last_checked' => time()
-        ));
+        ->save((array('enabled' => $status)));
 }
 
-// Update Etag and last Modified columns
-function update_cache($feed_id, $last_modified, $etag)
+function remove_feed($user_id, $feed_id)
 {
-    Database::getInstance('db')
-        ->table('feeds')
+    return Database::getInstance('db')
+        ->table(TABLE)
+        ->eq('user_id', $user_id)
         ->eq('id', $feed_id)
-        ->save(array(
-            'last_modified' => $last_modified,
-            'etag'          => $etag
-        ));
+        ->remove();
 }
 
-// Remove one feed
-function remove($feed_id)
+function count_failed_feeds($user_id)
 {
-    Group\remove_all($feed_id);
-
-    // Items are removed by a sql constraint
-    $result = Database::getInstance('db')->table('feeds')->eq('id', $feed_id)->remove();
-    Favicon\purge_favicons();
-    return $result;
+    return Database::getInstance('db')
+        ->table(TABLE)
+        ->eq('user_id', $user_id)
+        ->eq('parsing_error', 1)
+        ->count();
 }
 
-// Remove all feeds
-function remove_all()
+function count_feeds($user_id)
 {
-    $result = Database::getInstance('db')->table('feeds')->remove();
-    Favicon\purge_favicons();
-    return $result;
+    return Database::getInstance('db')
+        ->table(TABLE)
+        ->eq('user_id', $user_id)
+        ->count();
 }
 
-// Enable a feed (activate refresh)
-function enable($feed_id)
-{
-    return Database::getInstance('db')->table('feeds')->eq('id', $feed_id)->save((array('enabled' => 1)));
-}
-
-// Disable feed
-function disable($feed_id)
-{
-    return Database::getInstance('db')->table('feeds')->eq('id', $feed_id)->save((array('enabled' => 0)));
-}
diff --git a/app/models/group.php b/app/models/group.php
index bc89f52..36f3933 100644
--- a/app/models/group.php
+++ b/app/models/group.php
@@ -4,77 +4,47 @@ namespace Miniflux\Model\Group;
 
 use PicoDb\Database;
 
-/**
- * Get all groups
- *
- * @return array
- */
-function get_all()
+const TABLE      = 'groups';
+const JOIN_TABLE = 'feeds_groups';
+
+function get_all($user_id)
 {
     return Database::getInstance('db')
-            ->table('groups')
-            ->orderBy('title')
-            ->findAll();
+        ->table(TABLE)
+        ->eq('user_id', $user_id)
+        ->orderBy('title')
+        ->findAll();
 }
 
-/**
- * Get assoc array of group ids with assigned feeds ids
- *
- * @return array
- */
-function get_map()
+function get_groups_feed_ids($user_id)
 {
-    $result = Database::getInstance('db')
-            ->table('feeds_groups')
-            ->findAll();
+    $result = array();
+    $rows = Database::getInstance('db')
+        ->table(JOIN_TABLE)
+        ->columns('feed_id', 'group_id')
+        ->join(TABLE, 'id', 'group_id')
+        ->eq('user_id', $user_id)
+        ->findAll();
 
-    // TODO: add PDO::FETCH_COLUMN|PDO::FETCH_GROUP to picodb and use it instead
-    // of the following lines
-    $map = array();
-
-    foreach ($result as $row) {
+    foreach ($rows as $row) {
         $group_id = $row['group_id'];
         $feed_id = $row['feed_id'];
 
-        if (isset($map[$group_id])) {
-            $map[$group_id][] = $feed_id;
+        if (isset($result[$group_id])) {
+            $result[$group_id][] = $feed_id;
         } else {
-            $map[$group_id] = array($feed_id);
+            $result[$group_id] = array($feed_id);
         }
     }
 
-    return $map;
+    return $result;
 }
 
-/**
- * Get assoc array of feeds ids with assigned groups ids
- *
- * @return array
- */
-function get_feeds_map()
-{
-    $result = Database::getInstance('db')
-            ->table('feeds_groups')
-            ->findAll();
-    $map = array();
-    foreach ($result as $row) {
-        $map[$row['feed_id']][] = $row['group_id'];
-    }
-
-    return $map;
-}
-
-/**
- * Get all groups assigned to feed
- *
- * @param integer $feed_id id of the feed
- * @return array
- */
 function get_feed_group_ids($feed_id)
 {
     return Database::getInstance('db')
-            ->table('groups')
-            ->join('feeds_groups', 'group_id', 'id')
+            ->table(TABLE)
+            ->join(JOIN_TABLE, 'group_id', 'id')
             ->eq('feed_id', $feed_id)
             ->findAllByColumn('id');
 }
@@ -82,84 +52,77 @@ function get_feed_group_ids($feed_id)
 function get_feed_groups($feed_id)
 {
     return Database::getInstance('db')
-        ->table('groups')
+        ->table(TABLE)
         ->columns('groups.id', 'groups.title')
-        ->join('feeds_groups', 'group_id', 'id')
+        ->join(JOIN_TABLE, 'group_id', 'id')
         ->eq('feed_id', $feed_id)
         ->findAll();
 }
 
-/**
- * Get the id of a group
- *
- * @param string $title group name
- * @return mixed group id or false if not found
- */
-function get_group_id($title)
+function get_group_id_from_title($user_id, $title)
 {
     return Database::getInstance('db')
-            ->table('groups')
-            ->eq('title', $title)
-            ->findOneColumn('id');
+        ->table('groups')
+        ->eq('user_id', $user_id)
+        ->eq('title', $title)
+        ->findOneColumn('id');
 }
 
-/**
- * Get all feed ids assigned to a group
- *
- * @param integer $group_id
- * @return array
- */
-function get_feeds_by_group($group_id)
+function get_feed_ids_by_group($group_id)
 {
     return Database::getInstance('db')
-            ->table('feeds_groups')
-            ->eq('group_id', $group_id)
-            ->findAllByColumn('feed_id');
+        ->table(JOIN_TABLE)
+        ->eq('group_id', $group_id)
+        ->findAllByColumn('feed_id');
 }
 
-/**
- * Add a group to the Database
- *
- * Returns either the id of the new group or the id of an existing group with
- * the same name
- *
- * @param string $title group name
- * @return mixed id of the created group or false on error
- */
-function create($title)
+function create_group($user_id, $title)
 {
-    $data = array('title' => $title);
+    $group_id = get_group_id_from_title($user_id, $title);
 
-    // check if the group already exists
-    $group_id = get_group_id($title);
-
-    // create group if missing
     if ($group_id === false) {
-        Database::getInstance('db')
-                ->table('groups')
-                ->insert($data);
-
-        $group_id = get_group_id($title);
+        $group_id = Database::getInstance('db')
+            ->table(TABLE)
+            ->persist(array('title' => $title, 'user_id' => $user_id));
     }
 
     return $group_id;
 }
 
-/**
- * Add groups to feed
- *
- * @param integer $feed_id feed id
- * @param array $group_ids array of group ids
- * @return boolean true on success, false on error
- */
-function add($feed_id, array $group_ids)
+function update_feed_groups($user_id, $feed_id, array $group_ids, $group_name = '')
+{
+    if ($group_name !== '') {
+        $group_id = create_group($user_id, $group_name);
+        if ($group_id === false) {
+            return false;
+        }
+
+        if (! in_array($group_id, $group_ids)) {
+            $group_ids[] = $group_id;
+        }
+    }
+
+    $assigned = get_feed_group_ids($feed_id);
+    $superfluous = array_diff($assigned, $group_ids);
+    $missing = array_diff($group_ids, $assigned);
+
+    if (! empty($superfluous) && ! dissociate_feed_groups($feed_id, $superfluous)) {
+        return false;
+    }
+
+    if (! empty($missing) && ! associate_feed_groups($feed_id, $missing)) {
+        return false;
+    }
+
+    return true;
+}
+
+function associate_feed_groups($feed_id, array $group_ids)
 {
     foreach ($group_ids as $group_id) {
-        $data = array('feed_id' => $feed_id, 'group_id' => $group_id);
-
         $result = Database::getInstance('db')
-                ->table('feeds_groups')
-                ->insert($data);
+            ->table(JOIN_TABLE)
+            ->insert(array('feed_id' => $feed_id, 'group_id' => $group_id));
 
         if ($result === false) {
             return false;
@@ -169,104 +132,15 @@ function add($feed_id, array $group_ids)
     return true;
 }
 
-/**
- * Remove groups from feed
- *
- * @param integer $feed_id id of the feed
- * @param array $group_ids array of group ids
- * @return boolean true on success, false on error
- */
-function remove($feed_id, array $group_ids)
+function dissociate_feed_groups($feed_id, array $group_ids)
 {
-    $result = Database::getInstance('db')
-            ->table('feeds_groups')
-            ->eq('feed_id', $feed_id)
-            ->in('group_id', $group_ids)
-            ->remove();
-
-    // remove empty groups
-    if ($result) {
-        purge_groups();
-    }
-
-    return $result;
-}
-
-/**
- * Remove all groups from feed
- *
- * @param integer $feed_id id of the feed
- * @return boolean true on success, false on error
- */
-function remove_all($feed_id)
-{
-    $result = Database::getInstance('db')
-            ->table('feeds_groups')
-            ->eq('feed_id', $feed_id)
-            ->remove();
-
-    // remove empty groups
-    if ($result) {
-        purge_groups();
-    }
-
-    return $result;
-}
-
-/**
- * Purge orphaned groups from database
- */
-function purge_groups()
-{
-    $groups = Database::getInstance('db')
-                ->table('groups')
-                ->join('feeds_groups', 'group_id', 'id')
-                ->isNull('feed_id')
-                ->findAllByColumn('id');
-
-    if (! empty($groups)) {
-        Database::getInstance('db')
-            ->table('groups')
-            ->in('id', $groups)
-            ->remove();
-    }
-}
-
-/**
- * Update feed group associations
- *
- * @param integer $feed_id id of the feed to update
- * @param array $group_ids valid groups ids for feed
- * @param string $create_group group to create and assign to feed
- * @return boolean
- */
-function update_feed_groups($feed_id, array $group_ids, $create_group = '')
-{
-    if ($create_group !== '') {
-        $id = create($create_group);
-
-        if ($id === false) {
-            return false;
-        }
-
-        if (! in_array($id, $group_ids)) {
-            $group_ids[] = $id;
-        }
-    }
-
-    $assigned = get_feed_group_ids($feed_id);
-    $superfluous = array_diff($assigned, $group_ids);
-    $missing = array_diff($group_ids, $assigned);
-
-    // remove no longer assigned groups from feed
-    if (! empty($superfluous) && ! remove($feed_id, $superfluous)) {
+    if (empty($group_ids)) {
         return false;
     }
 
-    // add requested groups to feed
-    if (! empty($missing) && ! add($feed_id, $missing)) {
-        return false;
-    }
-
-    return true;
+    return Database::getInstance('db')
+        ->table(JOIN_TABLE)
+        ->eq('feed_id', $feed_id)
+        ->in('group_id', $group_ids)
+        ->remove();
 }
diff --git a/app/models/item.php b/app/models/item.php
index b43ab5e..5d8a90a 100644
--- a/app/models/item.php
+++ b/app/models/item.php
@@ -3,157 +3,177 @@
 namespace Miniflux\Model\Item;
 
 use PicoDb\Database;
-use PicoFeed\Logging\Logger;
-use Miniflux\Handler\Service;
-use Miniflux\Model\Config;
+use Miniflux\Model\Feed;
 use Miniflux\Model\Group;
 use Miniflux\Handler;
+use Miniflux\Helper;
+use PicoFeed\Parser\Parser;
 
-// Get all items without filtering
-function get_all()
+const TABLE          = 'items';
+const STATUS_UNREAD  = 'unread';
+const STATUS_READ    = 'read';
+const STATUS_REMOVED = 'removed';
+
+function change_item_status($user_id, $item_id, $status)
+{
+    if (! in_array($status, array(STATUS_READ, STATUS_UNREAD, STATUS_REMOVED))) {
+        return false;
+    }
+
+    return Database::getInstance('db')
+        ->table(TABLE)
+        ->eq('user_id', $user_id)
+        ->eq('id', $item_id)
+        ->save(array('status' => $status));
+}
+
+function change_items_status($user_id, $current_status, $new_status)
+{
+    if (! in_array($new_status, array(STATUS_READ, STATUS_UNREAD, STATUS_REMOVED))) {
+        return false;
+    }
+
+    return Database::getInstance('db')
+        ->table(TABLE)
+        ->eq('user_id', $user_id)
+        ->eq('status', $current_status)
+        ->save(array('status' => $new_status));
+}
+
+function change_item_ids_status($user_id, array $item_ids, $status)
+{
+    if (! in_array($status, array(STATUS_READ, STATUS_UNREAD, STATUS_REMOVED))) {
+        return false;
+    }
+
+    if (empty($item_ids)) {
+        return false;
+    }
+
+    return Database::getInstance('db')
+        ->table(TABLE)
+        ->eq('user_id', $user_id)
+        ->in('id', $item_ids)
+        ->save(array('status' => $status));
+}
+
+function update_feed_items($user_id, $feed_id, array $items, $rtl = false)
+{
+    $items_in_feed = array();
+    $db = Database::getInstance('db');
+    $db->startTransaction();
+
+    foreach ($items as $item) {
+        if ($item->getId() && $item->getUrl()) {
+            $item_id = get_item_id_from_checksum($feed_id, $item->getId());
+            $values = array(
+                'title'          => $item->getTitle(),
+                'url'            => $item->getUrl(),
+                'updated'        => $item->getDate()->getTimestamp(),
+                'author'         => $item->getAuthor(),
+                'content'        => Helper\bool_config('nocontent') ? '' : $item->getContent(),
+                'enclosure_url'  => $item->getEnclosureUrl(),
+                'enclosure_type' => $item->getEnclosureType(),
+                'language'       => $item->getLanguage(),
+                'rtl'            => $rtl || Parser::isLanguageRTL($item->getLanguage()) ? 1 : 0,
+            );
+
+            if ($item_id > 0) {
+                $db
+                    ->table(TABLE)
+                    ->eq('user_id', $user_id)
+                    ->eq('feed_id', $feed_id)
+                    ->eq('id', $item_id)
+                    ->update($values);
+            } else {
+                $values['checksum'] = $item->getId();
+                $values['user_id'] = $user_id;
+                $values['feed_id'] = $feed_id;
+                $values['status'] = STATUS_UNREAD;
+                $item_id = $db->table(TABLE)->persist($values);
+            }
+
+            $items_in_feed[] = $item_id;
+        }
+    }
+
+    cleanup_feed_items($feed_id, $items_in_feed);
+    $db->closeTransaction();
+}
+
+function cleanup_feed_items($feed_id, array $items_in_feed)
+{
+    if (! empty($items_in_feed)) {
+        $db = Database::getInstance('db');
+
+        $removed_items = $db
+            ->table(TABLE)
+            ->columns('id')
+            ->notIn('id', $items_in_feed)
+            ->eq('status', STATUS_REMOVED)
+            ->eq('feed_id', $feed_id)
+            ->desc('updated')
+            ->findAllByColumn('id');
+
+        // Keep a buffer of 2 items
+        // It's workaround for buggy feeds (cache issue with some Wordpress plugins)
+        if (is_array($removed_items)) {
+            $items_to_remove = array_slice($removed_items, 2);
+
+            if (! empty($items_to_remove)) {
+                // Handle the case when there is a huge number of items to remove
+                // Sqlite have a limit of 1000 sql variables by default
+                // Avoid the error message "too many SQL variables"
+                // We remove old items by batch of 500 items
+                $chunks = array_chunk($items_to_remove, 500);
+
+                foreach ($chunks as $chunk) {
+                    $db->table(TABLE)
+                        ->in('id', $chunk)
+                        ->eq('status', STATUS_REMOVED)
+                        ->eq('feed_id', $feed_id)
+                        ->remove();
+                }
+            }
+        }
+    }
+}
+
+function get_item_id_from_checksum($feed_id, $checksum)
+{
+    return (int) Database::getInstance('db')
+        ->table(TABLE)
+        ->eq('feed_id', $feed_id)
+        ->eq('checksum', $checksum)
+        ->findOneColumn('id');
+}
+
+function get_item($user_id, $item_id)
 {
     return Database::getInstance('db')
         ->table('items')
-        ->columns(
-            'items.id',
-            'items.title',
-            'items.updated',
-            'items.url',
-            'items.enclosure',
-            'items.enclosure_type',
-            'items.bookmark',
-            'items.feed_id',
-            'items.status',
-            'items.content',
-            'items.language',
-            'feeds.site_url',
-            'feeds.title AS feed_title',
-            'feeds.rtl'
-        )
-        ->join('feeds', 'id', 'feed_id')
-        ->in('status', array('read', 'unread'))
-        ->orderBy('updated', 'desc')
-        ->findAll();
-}
-
-// Get everthing since date (timestamp)
-function get_all_since($timestamp)
-{
-    return Database::getInstance('db')
-        ->table('items')
-        ->columns(
-            'items.id',
-            'items.title',
-            'items.updated',
-            'items.url',
-            'items.enclosure',
-            'items.enclosure_type',
-            'items.bookmark',
-            'items.feed_id',
-            'items.status',
-            'items.content',
-            'items.language',
-            'feeds.site_url',
-            'feeds.title AS feed_title',
-            'feeds.rtl'
-        )
-        ->join('feeds', 'id', 'feed_id')
-        ->in('status', array('read', 'unread'))
-        ->gte('updated', $timestamp)
-        ->orderBy('updated', 'desc')
-        ->findAll();
-}
-
-function get_latest_feeds_items()
-{
-    return Database::getInstance('db')
-        ->table('feeds')
-        ->columns(
-            'feeds.id',
-            'MAX(items.updated) as updated',
-            'items.status'
-        )
-        ->join('items', 'feed_id', 'id')
-        ->groupBy('feeds.id')
-        ->orderBy('feeds.id')
-        ->findAll();
-}
-
-// Get a list of [item_id => status,...]
-function get_all_status()
-{
-    return Database::getInstance('db')
-        ->hashtable('items')
-        ->in('status', array('read', 'unread'))
-        ->orderBy('updated', 'desc')
-        ->getAll('id', 'status');
-}
-
-// Get all items by status
-function get_all_by_status($status, $feed_ids = array(), $offset = null, $limit = null, $order_column = 'updated', $order_direction = 'desc')
-{
-    return Database::getInstance('db')
-        ->table('items')
-        ->columns(
-            'items.id',
-            'items.title',
-            'items.updated',
-            'items.url',
-            'items.enclosure',
-            'items.enclosure_type',
-            'items.bookmark',
-            'items.feed_id',
-            'items.status',
-            'items.content',
-            'items.language',
-            'items.author',
-            'feeds.site_url',
-            'feeds.title AS feed_title',
-            'feeds.rtl'
-        )
-        ->join('feeds', 'id', 'feed_id')
-        ->eq('status', $status)
-        ->in('feed_id', $feed_ids)
-        ->orderBy($order_column, $order_direction)
-        ->offset($offset)
-        ->limit($limit)
-        ->findAll();
-}
-
-// Get the number of items per status
-function count_by_status($status, $feed_ids = array())
-{
-    return Database::getInstance('db')
-        ->table('items')
-        ->eq('status', $status)
-        ->in('feed_id', $feed_ids)
-        ->count();
-}
-
-// Get one item by id
-function get($id)
-{
-    return Database::getInstance('db')
-        ->table('items')
-        ->eq('id', $id)
+        ->eq('user_id', $user_id)
+        ->eq('id', $item_id)
         ->findOne();
 }
 
-// Get item naviguation (next/prev items)
-function get_nav($item, $status = array('unread'), $bookmark = array(1, 0), $feed_id = null, $group_id = null)
+function get_item_nav($user_id, array $item, $status = array(STATUS_UNREAD), $bookmark = array(1, 0), $feed_id = null, $group_id = null)
 {
     $query = Database::getInstance('db')
-        ->table('items')
+        ->table(TABLE)
         ->columns('id', 'status', 'title', 'bookmark')
-        ->neq('status', 'removed')
-        ->orderBy('updated', Config\get('items_sorting_direction'));
+        ->neq('status', STATUS_REMOVED)
+        ->eq('user_id', $user_id)
+        ->orderBy('updated', Helper\config('items_sorting_direction'))
+        ->desc('id')
+    ;
 
     if ($feed_id) {
         $query->eq('feed_id', $feed_id);
     }
 
     if ($group_id) {
-        $query->in('feed_id', Group\get_feeds_by_group($group_id));
+        $query->in('feed_id', Group\get_feed_ids_by_group($group_id));
     }
 
     $items = $query->findAll();
@@ -199,240 +219,149 @@ function get_nav($item, $status = array('unread'), $bookmark = array(1, 0), $fee
     );
 }
 
-// Change item status to removed and clear content
-function set_removed($id)
+function get_items_by_status($user_id, $status, $feed_ids = array(), $offset = null, $limit = null, $order_column = 'updated', $order_direction = 'desc')
 {
     return Database::getInstance('db')
         ->table('items')
-        ->eq('id', $id)
-        ->save(array('status' => 'removed', 'content' => ''));
+        ->columns(
+            'items.id',
+            'items.checksum',
+            'items.title',
+            'items.updated',
+            'items.url',
+            'items.enclosure_url',
+            'items.enclosure_type',
+            'items.bookmark',
+            'items.feed_id',
+            'items.status',
+            'items.content',
+            'items.language',
+            'items.rtl',
+            'items.author',
+            'feeds.site_url',
+            'feeds.title AS feed_title'
+        )
+        ->join('feeds', 'id', 'feed_id')
+        ->eq('items.user_id', $user_id)
+        ->eq('items.status', $status)
+        ->in('items.feed_id', $feed_ids)
+        ->orderBy($order_column, $order_direction)
+        ->offset($offset)
+        ->limit($limit)
+        ->findAll();
 }
 
-// Change item status to read
-function set_read($id)
+function get_items($user_id, $since_id = null, array $item_ids = array(), $limit = 50)
 {
-    return Database::getInstance('db')
+    $query = Database::getInstance('db')
         ->table('items')
-        ->eq('id', $id)
-        ->save(array('status' => 'read'));
-}
+        ->columns(
+            'items.id',
+            'items.checksum',
+            'items.title',
+            'items.updated',
+            'items.url',
+            'items.enclosure_url',
+            'items.enclosure_type',
+            'items.bookmark',
+            'items.feed_id',
+            'items.status',
+            'items.content',
+            'items.language',
+            'items.rtl',
+            'items.author',
+            'feeds.site_url',
+            'feeds.title AS feed_title'
+        )
+        ->join('feeds', 'id', 'feed_id')
+        ->eq('items.user_id', $user_id)
+        ->neq('items.status', STATUS_REMOVED)
+        ->limit($limit)
+        ->asc('items.id');
 
-// Change item status to unread
-function set_unread($id)
-{
-    return Database::getInstance('db')
-        ->table('items')
-        ->eq('id', $id)
-        ->save(array('status' => 'unread'));
-}
-
-// Change item status to "read", "unread" or "removed"
-function set_status($status, array $items)
-{
-    if (! in_array($status, array('read', 'unread', 'removed'))) {
-        return false;
+    if ($since_id !== null) {
+        $query->gt('items.id', $since_id);
+    } elseif (! empty($item_ids)) {
+        $query->in('items.id', $item_ids);
     }
 
-    return Database::getInstance('db')
-        ->table('items')
-        ->in('id', $items)
-        ->save(array('status' => $status));
+    return $query->findAll();
 }
 
-// Mark all unread items as read
-function mark_all_as_read()
+function get_item_ids_by_status($user_id, $status)
 {
     return Database::getInstance('db')
         ->table('items')
-        ->eq('status', 'unread')
-        ->save(array('status' => 'read'));
+        ->eq('user_id', $user_id)
+        ->eq('status', $status)
+        ->findAllByColumn('id');
 }
 
-// Mark all read items to removed
-function mark_all_as_removed()
+function get_latest_feeds_items($user_id)
 {
     return Database::getInstance('db')
-        ->table('items')
-        ->eq('status', 'read')
-        ->eq('bookmark', 0)
-        ->save(array('status' => 'removed', 'content' => ''));
+        ->table(Feed\TABLE)
+        ->columns(
+            'feeds.id',
+            'MAX(items.updated) as updated',
+            'items.status'
+        )
+        ->join(TABLE, 'feed_id', 'id')
+        ->eq('feeds.user_id', $user_id)
+        ->groupBy('feeds.id')
+        ->orderBy('feeds.id')
+        ->findAll();
 }
 
-// Mark all read items to removed after X days
-function autoflush_read()
+function count_by_status($user_id, $status, $feed_ids = array())
 {
-    $autoflush = (int) Config\get('autoflush');
+    $query = Database::getInstance('db')
+        ->table('items')
+        ->eq('user_id', $user_id)
+        ->in('feed_id', $feed_ids);
+
+    if (is_array($status)) {
+        $query->in('status', $status);
+    } else {
+        $query->eq('status', $status);
+    }
+
+    return $query->count();
+}
+
+function autoflush_read($user_id)
+{
+    $autoflush = Helper\int_config('autoflush');
 
     if ($autoflush > 0) {
-
-        // Mark read items removed after X days
         Database::getInstance('db')
-            ->table('items')
+            ->table(TABLE)
+            ->eq('user_id', $user_id)
             ->eq('bookmark', 0)
-            ->eq('status', 'read')
+            ->eq('status', STATUS_READ)
             ->lt('updated', strtotime('-'.$autoflush.'day'))
-            ->save(array('status' => 'removed', 'content' => ''));
+            ->save(array('status' => STATUS_REMOVED, 'content' => ''));
     } elseif ($autoflush === -1) {
-
-        // Mark read items removed immediately
         Database::getInstance('db')
-            ->table('items')
+            ->table(TABLE)
+            ->eq('user_id', $user_id)
             ->eq('bookmark', 0)
-            ->eq('status', 'read')
-            ->save(array('status' => 'removed', 'content' => ''));
+            ->eq('status', STATUS_READ)
+            ->save(array('status' => STATUS_REMOVED, 'content' => ''));
     }
 }
 
-// Mark all unread items to removed after X days
-function autoflush_unread()
+function autoflush_unread($user_id)
 {
-    $autoflush = (int) Config\get('autoflush_unread');
+    $autoflush = Helper\int_config('autoflush_unread');
 
     if ($autoflush > 0) {
-
-        // Mark read items removed after X days
         Database::getInstance('db')
-            ->table('items')
+            ->table(TABLE)
+            ->eq('user_id', $user_id)
             ->eq('bookmark', 0)
-            ->eq('status', 'unread')
+            ->eq('status', STATUS_UNREAD)
             ->lt('updated', strtotime('-'.$autoflush.'day'))
-            ->save(array('status' => 'removed', 'content' => ''));
+            ->save(array('status' => STATUS_REMOVED, 'content' => ''));
     }
 }
-
-// Update all items
-function update_all($feed_id, array $items)
-{
-    $nocontent = (bool) Config\get('nocontent');
-
-    $items_in_feed = array();
-
-    $db = Database::getInstance('db');
-    $db->startTransaction();
-
-    foreach ($items as $item) {
-        Logger::setMessage('Item => '.$item->getId().' '.$item->getUrl());
-
-        // Item parsed correctly?
-        if ($item->getId() && $item->getUrl()) {
-            Logger::setMessage('Item parsed correctly');
-
-            // Get item record in database, if any
-            $itemrec = $db
-                ->table('items')
-                ->columns('enclosure')
-                ->eq('id', $item->getId())
-                ->findOne();
-
-            // Insert a new item
-            if ($itemrec === null) {
-                Logger::setMessage('Item added to the database');
-
-                $db->table('items')->save(array(
-                    'id' => $item->getId(),
-                    'title' => $item->getTitle(),
-                    'url' => $item->getUrl(),
-                    'updated' => $item->getDate()->getTimestamp(),
-                    'author' => $item->getAuthor(),
-                    'content' => $nocontent ? '' : $item->getContent(),
-                    'status' => 'unread',
-                    'feed_id' => $feed_id,
-                    'enclosure' => $item->getEnclosureUrl(),
-                    'enclosure_type' => $item->getEnclosureType(),
-                    'language' => $item->getLanguage(),
-                ));
-            } elseif (! $itemrec['enclosure'] && $item->getEnclosureUrl()) {
-                Logger::setMessage('Update item enclosure');
-
-                $db->table('items')->eq('id', $item->getId())->save(array(
-                    'status' => 'unread',
-                    'enclosure' => $item->getEnclosureUrl(),
-                    'enclosure_type' => $item->getEnclosureType(),
-                ));
-            } else {
-                Logger::setMessage('Item already in the database');
-            }
-
-            // Items inside this feed
-            $items_in_feed[] = $item->id;
-        }
-    }
-
-    // Cleanup old items
-    cleanup($feed_id, $items_in_feed);
-
-    $db->closeTransaction();
-}
-
-// Remove from the database items marked as "removed"
-// and not present inside the feed
-function cleanup($feed_id, array $items_in_feed)
-{
-    if (! empty($items_in_feed)) {
-        $db = Database::getInstance('db');
-
-        $removed_items = $db
-            ->table('items')
-            ->columns('id')
-            ->notIn('id', $items_in_feed)
-            ->eq('status', 'removed')
-            ->eq('feed_id', $feed_id)
-            ->desc('updated')
-            ->findAllByColumn('id');
-
-        // Keep a buffer of 2 items
-        // It's workaround for buggy feeds (cache issue with some Wordpress plugins)
-        if (is_array($removed_items)) {
-            $items_to_remove = array_slice($removed_items, 2);
-
-            if (! empty($items_to_remove)) {
-                $nb_items = count($items_to_remove);
-                Logger::setMessage('There is '.$nb_items.' items to remove');
-
-                // Handle the case when there is a huge number of items to remove
-                // Sqlite have a limit of 1000 sql variables by default
-                // Avoid the error message "too many SQL variables"
-                // We remove old items by batch of 500 items
-                $chunks = array_chunk($items_to_remove, 500);
-
-                foreach ($chunks as $chunk) {
-                    $db->table('items')
-                        ->in('id', $chunk)
-                        ->eq('status', 'removed')
-                        ->eq('feed_id', $feed_id)
-                        ->remove();
-                }
-            }
-        }
-    }
-}
-
-// Download item content
-function download_contents($item_id)
-{
-    $item = get($item_id);
-    $content = Handler\Scraper\download_contents($item['url']);
-
-    if (! empty($content)) {
-        if (! Config\get('nocontent')) {
-            Database::getInstance('db')
-                ->table('items')
-                ->eq('id', $item['id'])
-                ->save(array('content' => $content));
-        }
-
-        Config\write_debug();
-
-        return array(
-            'result' => true,
-            'content' => $content
-        );
-    }
-
-    Config\write_debug();
-
-    return array(
-        'result' => false,
-        'content' => ''
-    );
-}
diff --git a/app/models/item_feed.php b/app/models/item_feed.php
index a6bdf7d..5456223 100644
--- a/app/models/item_feed.php
+++ b/app/models/item_feed.php
@@ -2,18 +2,48 @@
 
 namespace Miniflux\Model\ItemFeed;
 
+use Miniflux\Model\Feed;
+use Miniflux\Model\Item;
 use PicoDb\Database;
 
-function count_items($feed_id)
+function count_items_by_status($user_id, $feed_id)
+{
+    $counts = Database::getInstance('db')
+        ->table(Item\TABLE)
+        ->columns('status', 'count(*) as item_count')
+        ->in('status', array(Item\STATUS_READ, Item\STATUS_UNREAD))
+        ->eq('user_id', $user_id)
+        ->eq('feed_id', $feed_id)
+        ->groupBy('status')
+        ->findAll();
+
+    $result = array(
+        'items_unread' => 0,
+        'items_total' => 0,
+    );
+
+    foreach ($counts as &$count) {
+        if ($count['status'] === Item\STATUS_UNREAD) {
+            $result['items_unread'] = (int) $count['item_count'];
+        }
+
+        $result['items_total'] += $count['item_count'];
+    }
+
+    return $result;
+}
+
+function count_items($user_id, $feed_id)
 {
     return Database::getInstance('db')
-        ->table('items')
+        ->table(Item\TABLE)
         ->eq('feed_id', $feed_id)
-        ->in('status', array('unread', 'read'))
+        ->eq('user_id', $user_id)
+        ->in('status', array(Item\STATUS_READ, Item\STATUS_UNREAD))
         ->count();
 }
 
-function get_all_items($feed_id, $offset = null, $limit = null, $order_column = 'updated', $order_direction = 'desc')
+function get_all_items($user_id, $feed_id, $offset = null, $limit = null, $order_column = 'updated', $order_direction = 'desc')
 {
     return Database::getInstance('db')
         ->table('items')
@@ -22,31 +52,39 @@ function get_all_items($feed_id, $offset = null, $limit = null, $order_column =
             'items.title',
             'items.updated',
             'items.url',
-            'items.enclosure',
+            'items.enclosure_url',
             'items.enclosure_type',
             'items.feed_id',
             'items.status',
             'items.content',
             'items.bookmark',
             'items.language',
+            'items.rtl',
             'items.author',
             'feeds.site_url',
-            'feeds.rtl'
+            'feeds.title AS feed_title'
         )
-        ->join('feeds', 'id', 'feed_id')
-        ->in('status', array('unread', 'read'))
-        ->eq('feed_id', $feed_id)
+        ->join(Feed\TABLE, 'id', 'feed_id')
+        ->in('status', array(Item\STATUS_UNREAD, Item\STATUS_READ))
+        ->eq('items.feed_id', $feed_id)
+        ->eq('items.user_id', $user_id)
         ->orderBy($order_column, $order_direction)
         ->offset($offset)
         ->limit($limit)
         ->findAll();
 }
 
-function mark_all_as_read($feed_id)
+function change_items_status($user_id, $feed_id, $current_status, $new_status, $before = null)
 {
-    return Database::getInstance('db')
-        ->table('items')
-        ->eq('status', 'unread')
+    $query = Database::getInstance('db')
+        ->table(Item\TABLE)
+        ->eq('status', $current_status)
         ->eq('feed_id', $feed_id)
-        ->update(array('status' => 'read'));
+        ->eq('user_id', $user_id);
+
+    if ($before !== null) {
+        $query->lte('updated', $before);
+    }
+
+    return $query->update(array('status' => $new_status));
 }
diff --git a/app/models/item_group.php b/app/models/item_group.php
index 53a7ef7..28e98b0 100644
--- a/app/models/item_group.php
+++ b/app/models/item_group.php
@@ -2,28 +2,27 @@
 
 namespace Miniflux\Model\ItemGroup;
 
-use PicoDb\Database;
+use Miniflux\Model\Item;
 use Miniflux\Model\Group;
+use PicoDb\Database;
 
-function mark_all_as_read($group_id)
+function change_items_status($user_id, $group_id, $current_status, $new_status, $before = null)
 {
-    $feed_ids = Group\get_feeds_by_group($group_id);
+    $feed_ids = Group\get_feed_ids_by_group($group_id);
 
-    return Database::getInstance('db')
-        ->table('items')
-        ->eq('status', 'unread')
-        ->in('feed_id', $feed_ids)
-        ->update(array('status' => 'read'));
-}
-
-function mark_all_as_removed($group_id)
-{
-    $feed_ids = Group\get_feeds_by_group($group_id);
-
-    return Database::getInstance('db')
-        ->table('items')
-        ->eq('status', 'read')
-        ->eq('bookmark', 0)
-        ->in('feed_id', $feed_ids)
-        ->save(array('status' => 'removed', 'content' => ''));
+    if (empty($feed_ids)) {
+        return false;
+    }
+
+    $query = Database::getInstance('db')
+        ->table(Item\TABLE)
+        ->eq('user_id', $user_id)
+        ->eq('status', $current_status)
+        ->in('feed_id', $feed_ids);
+
+    if ($before !== null) {
+        $query->lte('updated', $before);
+    }
+
+    return $query->update(array('status' => $new_status));
 }
diff --git a/app/models/remember_me.php b/app/models/remember_me.php
index 1a97d7f..4ca30c1 100644
--- a/app/models/remember_me.php
+++ b/app/models/remember_me.php
@@ -2,73 +2,35 @@
 
 namespace Miniflux\Model\RememberMe;
 
-use PicoDb\Database;
+use Miniflux\Session\SessionStorage;
 use Miniflux\Helper;
-use Miniflux\Model\Config;
-use Miniflux\Model\Database as DatabaseModel;
+use Miniflux\Model\User;
+use PicoDb\Database;
 
-const TABLE = 'remember_me';
+const TABLE       = 'remember_me';
 const COOKIE_NAME = '_R_';
-const EXPIRATION = 5184000;
+const EXPIRATION  = 5184000;
 
-/**
- * Get a remember me record
- *
- * @access public
- * @param  string $token
- * @param  string $sequence
- * @return mixed
- */
-function find($token, $sequence)
+function get_record($token, $sequence)
 {
     return Database::getInstance('db')
-                ->table(TABLE)
-                ->eq('token', $token)
-                ->eq('sequence', $sequence)
-                ->gt('expiration', time())
-                ->findOne();
+        ->table(TABLE)
+        ->eq('token', $token)
+        ->eq('sequence', $sequence)
+        ->gt('expiration', time())
+        ->findOne();
 }
 
-/**
- * Get all sessions
- *
- * @access public
- * @return array
- */
-function get_all()
-{
-    return Database::getInstance('db')
-                ->table(TABLE)
-                ->desc('date_creation')
-                ->columns('id', 'ip', 'user_agent', 'date_creation', 'expiration')
-                ->findAll();
-}
-
-/**
- * Authenticate the user with the cookie
- *
- * @access public
- * @return bool
- */
 function authenticate()
 {
     $credentials = read_cookie();
 
     if ($credentials !== false) {
-        $record = find($credentials['token'], $credentials['sequence']);
+        $record = get_record($credentials['token'], $credentials['sequence']);
 
         if ($record) {
-
-            // Update the sequence
-            write_cookie(
-                $record['token'],
-                update($record['token']),
-                $record['expiration']
-            );
-
-            // mark user as sucessfull logged in
-            $_SESSION['loggedin'] = true;
-
+            $user = User\get_user_by_id($record['user_id']);
+            SessionStorage::getInstance()->setUser($user);
             return true;
         }
     }
@@ -76,35 +38,6 @@ function authenticate()
     return false;
 }
 
-/**
- * Update the database and the cookie with a new sequence
- *
- * @access public
- */
-function refresh()
-{
-    $credentials = read_cookie();
-
-    if ($credentials !== false) {
-        $record = find($credentials['token'], $credentials['sequence']);
-
-        if ($record) {
-
-            // Update the sequence
-            write_cookie(
-                $record['token'],
-                update($record['token']),
-                $record['expiration']
-            );
-        }
-    }
-}
-
-/**
- * Remove the current RememberMe session and the cookie
- *
- * @access public
- */
 function destroy()
 {
     $credentials = read_cookie();
@@ -119,19 +52,9 @@ function destroy()
     delete_cookie();
 }
 
-/**
- * Create a new RememberMe session
- *
- * @access public
- * @param  integer  $dbname      Database name
- * @param  integer  $username    Username
- * @param  string   $ip          IP Address
- * @param  string   $user_agent  User Agent
- * @return array
- */
-function create($dbname, $username, $ip, $user_agent)
+function create($user_id, $ip, $user_agent)
 {
-    $token = hash('sha256', $dbname.$username.$user_agent.$ip.Helper\generate_token());
+    $token = hash('sha256', $user_id.$user_agent.$ip.Helper\generate_token());
     $sequence = Helper\generate_token();
     $expiration = time() + EXPIRATION;
 
@@ -140,7 +63,7 @@ function create($dbname, $username, $ip, $user_agent)
     Database::getInstance('db')
          ->table(TABLE)
          ->insert(array(
-            'username' => $username,
+            'user_id' => $user_id,
             'ip' => $ip,
             'user_agent' => $user_agent,
             'token' => $token,
@@ -156,27 +79,14 @@ function create($dbname, $username, $ip, $user_agent)
     );
 }
 
-/**
- * Remove old sessions
- *
- * @access public
- * @return bool
- */
 function cleanup()
 {
     return Database::getInstance('db')
-                ->table(TABLE)
-                ->lt('expiration', time())
-                ->remove();
+        ->table(TABLE)
+        ->lt('expiration', time())
+        ->remove();
 }
 
-/**
- * Return a new sequence token and update the database
- *
- * @access public
- * @param  string   $token        Session token
- * @return string
- */
 function update($token)
 {
     $new_sequence = Helper\generate_token();
@@ -189,33 +99,14 @@ function update($token)
     return $new_sequence;
 }
 
-/**
- * Encode the cookie
- *
- * @access public
- * @param  string   $token        Session token
- * @param  string   $sequence     Sequence token
- * @return string
- */
 function encode_cookie($token, $sequence)
 {
-    return implode('|', array(base64_encode(DatabaseModel\select()), $token, $sequence));
+    return implode('|', array($token, $sequence));
 }
 
-/**
- * Decode the value of a cookie
- *
- * @access public
- * @param  string   $value    Raw cookie data
- * @return array
- */
 function decode_cookie($value)
 {
-    @list($database, $token, $sequence) = explode('|', $value);
-
-    if (ENABLE_MULTIPLE_DB && ! DatabaseModel\select(base64_decode($database))) {
-        return false;
-    }
+    @list($token, $sequence) = explode('|', $value);
 
     return array(
         'token' => $token,
@@ -223,25 +114,11 @@ function decode_cookie($value)
     );
 }
 
-/**
- * Return true if the current user has a RememberMe cookie
- *
- * @access public
- * @return bool
- */
 function has_cookie()
 {
     return ! empty($_COOKIE[COOKIE_NAME]);
 }
 
-/**
- * Write and encode the cookie
- *
- * @access public
- * @param  string   $token        Session token
- * @param  string   $sequence     Sequence token
- * @param  string   $expiration   Cookie expiration
- */
 function write_cookie($token, $sequence, $expiration)
 {
     setcookie(
@@ -255,12 +132,6 @@ function write_cookie($token, $sequence, $expiration)
     );
 }
 
-/**
- * Read and decode the cookie
- *
- * @access public
- * @return mixed
- */
 function read_cookie()
 {
     if (empty($_COOKIE[COOKIE_NAME])) {
@@ -270,11 +141,6 @@ function read_cookie()
     return decode_cookie($_COOKIE[COOKIE_NAME]);
 }
 
-/**
- * Remove the cookie
- *
- * @access public
- */
 function delete_cookie()
 {
     setcookie(
diff --git a/app/models/search.php b/app/models/search.php
index 21e0599..05f5a8e 100644
--- a/app/models/search.php
+++ b/app/models/search.php
@@ -1,43 +1,47 @@
 table('items')
-        ->neq('status', 'removed')
+        ->table(Item\TABLE)
+        ->eq('user_id', $user_id)
+        ->neq('status', Item\STATUS_REMOVED)
         ->ilike('title', '%' . $text . '%')
         ->count();
 }
 
-function get_all_items($text, $offset = null, $limit = null)
+function get_all_items($user_id, $text, $offset = null, $limit = null)
 {
     return Database::getInstance('db')
-        ->table('items')
+        ->table(Item\TABLE)
         ->columns(
             'items.id',
             'items.title',
             'items.updated',
             'items.url',
-            'items.enclosure',
+            'items.enclosure_url',
             'items.enclosure_type',
             'items.bookmark',
             'items.feed_id',
             'items.status',
             'items.content',
             'items.language',
+            'items.rtl',
             'items.author',
             'feeds.site_url',
-            'feeds.title AS feed_title',
-            'feeds.rtl'
+            'feeds.title AS feed_title'
         )
-        ->join('feeds', 'id', 'feed_id')
-        ->neq('status', 'removed')
+        ->join(Feed\TABLE, 'id', 'feed_id')
+        ->eq('items.user_id', $user_id)
+        ->neq('items.status', Item\STATUS_REMOVED)
         ->ilike('items.title', '%' . $text . '%')
-        ->orderBy('updated', 'desc')
+        ->orderBy('items.updated', 'desc')
         ->offset($offset)
         ->limit($limit)
         ->findAll();
diff --git a/app/models/user.php b/app/models/user.php
index 3206273..f8bc70f 100644
--- a/app/models/user.php
+++ b/app/models/user.php
@@ -3,37 +3,142 @@
 namespace Miniflux\Model\User;
 
 use PicoDb\Database;
-use Miniflux\Session;
-use Miniflux\Request;
-use Miniflux\Model\Config;
-use Miniflux\Model\RememberMe;
-use Miniflux\Model\Database as DatabaseModel;
+use Miniflux\Helper;
 
-// Check if the user is logged in
-function is_loggedin()
+const TABLE = 'users';
+
+function create_user($username, $password, $is_admin = false)
 {
-    return ! empty($_SESSION['loggedin']);
+    list($fever_token, $fever_api_key) = generate_fever_api_key($username);
+
+    return Database::getInstance('db')
+        ->table(TABLE)
+        ->persist(array(
+            'username'          => $username,
+            'password'          => password_hash($password, PASSWORD_BCRYPT),
+            'is_admin'          => (int) $is_admin,
+            'api_token'         => Helper\generate_token(),
+            'bookmarklet_token' => Helper\generate_token(),
+            'cronjob_token'     => Helper\generate_token(),
+            'feed_token'        => Helper\generate_token(),
+            'fever_token'       => $fever_token,
+            'fever_api_key'     => $fever_api_key,
+        ));
 }
 
-// Destroy the session and the rememberMe cookie
-function logout()
+function update_user($user_id, $username, $password = null, $is_admin = null)
 {
-    RememberMe\destroy();
-    Session\close();
+    $user = get_user_by_id($user_id);
+    $values = array();
+
+    if ($user['username'] !== $username) {
+        list($fever_token, $fever_api_key) = generate_fever_api_key($user['username']);
+
+        $values['username'] = $username;
+        $values['fever_token'] = $fever_token;
+        $values['fever_api_key'] = $fever_api_key;
+    }
+
+    if ($password !== null) {
+        $values['password'] = password_hash($password, PASSWORD_BCRYPT);
+    }
+
+    if ($is_admin !== null) {
+        $values['is_admin'] = (int) $is_admin;
+    }
+
+    if (! empty($values)) {
+        return Database::getInstance('db')
+            ->table(TABLE)
+            ->eq('id', $user_id)
+            ->update($values);
+    }
+
+    return true;
 }
 
-// Get the credentials from the current selected database
-function get_credentials()
+function regenerate_tokens($user_id)
+{
+    $user = get_user_by_id($user_id);
+    list($fever_token, $fever_api_key) = generate_fever_api_key($user['username']);
+
+    return Database::getInstance('db')
+        ->table(TABLE)
+        ->eq('id', $user_id)
+        ->update(array(
+            'api_token'         => Helper\generate_token(),
+            'bookmarklet_token' => Helper\generate_token(),
+            'cronjob_token'     => Helper\generate_token(),
+            'feed_token'        => Helper\generate_token(),
+            'fever_token'       => $fever_token,
+            'fever_api_key'     => $fever_api_key,
+        ));
+}
+
+function remove_user($user_id)
 {
     return Database::getInstance('db')
-        ->hashtable('settings')
-        ->get('username', 'password');
+        ->table(TABLE)
+        ->eq('id', $user_id)
+        ->remove();
 }
 
-// Set last login date
-function set_last_login()
+function generate_fever_api_key($username)
+{
+    $token = Helper\generate_token();
+    $api_key = md5($username . ':' . $token);
+    return array($token, $api_key);
+}
+
+function get_all_users()
 {
     return Database::getInstance('db')
-        ->hashtable('settings')
-        ->put(array('last_login' => time()));
+        ->table(TABLE)
+        ->columns('id', 'username', 'is_admin', 'last_login')
+        ->asc('username')
+        ->asc('id')
+        ->findAll();
+}
+
+function get_user_by_id($user_id)
+{
+    return Database::getInstance('db')
+        ->table(TABLE)
+        ->eq('id', $user_id)
+        ->findOne();
+}
+
+function get_user_by_id_without_password($user_id)
+{
+    $user = Database::getInstance('db')
+        ->table(TABLE)
+        ->eq('id', $user_id)
+        ->findOne();
+
+    unset($user['password']);
+    return $user;
+}
+
+function get_user_by_username($username)
+{
+    return Database::getInstance('db')
+        ->table(TABLE)
+        ->eq('username', $username)
+        ->findOne();
+}
+
+function get_user_by_token($key, $token)
+{
+    return Database::getInstance('db')
+        ->table(TABLE)
+        ->eq($key, $token)
+        ->findOne();
+}
+
+function set_last_login_date($user_id)
+{
+    return Database::getInstance('db')
+        ->table(TABLE)
+        ->eq('id', $user_id)
+        ->update(array('last_login' => time()));
 }
diff --git a/app/schemas/sqlite.php b/app/schemas/sqlite.php
index 8d4125c..611cdf5 100644
--- a/app/schemas/sqlite.php
+++ b/app/schemas/sqlite.php
@@ -5,396 +5,129 @@ namespace Miniflux\Schema;
 use PDO;
 use Miniflux\Helper;
 
-const VERSION = 44;
-
-
-function version_44(PDO $pdo)
-{
-    $pdo->exec('INSERT INTO settings ("key", "value") VALUES ("item_title_link", "full")');
-}
-
-function version_43(PDO $pdo)
-{
-    $pdo->exec('DROP TABLE favicons');
-
-    $pdo->exec(
-        'CREATE TABLE favicons (
-            id INTEGER PRIMARY KEY,
-            hash TEXT UNIQUE,
-            type TEXT
-        )'
-    );
-
-    $pdo->exec('
-        CREATE TABLE "favicons_feeds" (
-            feed_id INTEGER NOT NULL,
-            favicon_id INTEGER NOT NULL,
-            PRIMARY KEY(feed_id, favicon_id),
-            FOREIGN KEY(favicon_id) REFERENCES favicons(id) ON DELETE CASCADE,
-            FOREIGN KEY(feed_id) REFERENCES feeds(id) ON DELETE CASCADE
-        )
-    ');
-}
-
-function version_42(PDO $pdo)
-{
-    $pdo->exec('DROP TABLE favicons');
-
-    $pdo->exec(
-        'CREATE TABLE favicons (
-            feed_id INTEGER UNIQUE,
-            link TEXT,
-            file TEXT,
-            type TEXT,
-            FOREIGN KEY(feed_id) REFERENCES feeds(id) ON DELETE CASCADE
-        )'
-    );
-}
-
-function version_41(PDO $pdo)
-{
-    $pdo->exec('
-        CREATE TABLE "groups" (
-            id INTEGER PRIMARY KEY,
-            title TEXT
-        )
-    ');
-
-    $pdo->exec('
-        CREATE TABLE "feeds_groups" (
-            feed_id INTEGER  NOT NULL,
-            group_id INTEGER NOT NULL,
-            PRIMARY KEY(feed_id, group_id),
-            FOREIGN KEY(group_id) REFERENCES groups(id) ON DELETE CASCADE,
-            FOREIGN KEY(feed_id) REFERENCES feeds(id) ON DELETE CASCADE
-        )
-    ');
-}
-
-function version_40(PDO $pdo)
-{
-    $pdo->exec('UPDATE settings SET "value"="https://github.com/miniflux/miniflux/archive/master.zip" WHERE "key"="auto_update_url"');
-}
-
-function version_39(PDO $pdo)
-{
-    $pdo->exec('ALTER TABLE feeds ADD COLUMN cloak_referrer INTEGER DEFAULT 0');
-}
-
-function version_38(PDO $pdo)
-{
-    $pdo->exec('INSERT INTO settings ("key", "value") VALUES ("original_marks_read", 1)');
-}
-
-function version_37(PDO $pdo)
-{
-    $pdo->exec('INSERT INTO settings ("key", "value") VALUES ("debug_mode", 0)');
-}
-
-function version_36(PDO $pdo)
-{
-    $pdo->exec('INSERT INTO settings ("key", "value") VALUES ("frontend_updatecheck_interval", 10)');
-}
-
-function version_35(PDO $pdo)
-{
-    $pdo->exec('DELETE FROM favicons WHERE icon = ""');
-
-    $pdo->exec('
-        CREATE TABLE settings (
-            "key" TEXT NOT NULL UNIQUE,
-            "value" TEXT Default NULL,
-            PRIMARY KEY(key)
-        )
-    ');
-
-    $pdo->exec("
-        INSERT INTO settings (key,value)
-            SELECT 'username', username FROM config UNION
-            SELECT 'password', password FROM config UNION
-            SELECT 'language', language FROM config UNION
-            SELECT 'autoflush', autoflush FROM config UNION
-            SELECT 'nocontent', nocontent FROM config UNION
-            SELECT 'items_per_page', items_per_page FROM config UNION
-            SELECT 'theme', theme FROM config UNION
-            SELECT 'api_token', api_token FROM config UNION
-            SELECT 'feed_token', feed_token FROM config UNION
-            SELECT 'items_sorting_direction', items_sorting_direction FROM config UNION
-            SELECT 'redirect_nothing_to_read', redirect_nothing_to_read FROM config UNION
-            SELECT 'timezone', timezone FROM config UNION
-            SELECT 'auto_update_url', auto_update_url FROM config UNION
-            SELECT 'bookmarklet_token', bookmarklet_token FROM config UNION
-            SELECT 'items_display_mode', items_display_mode FROM config UNION
-            SELECT 'fever_token', fever_token FROM config UNION
-            SELECT 'autoflush_unread', autoflush_unread FROM config UNION
-            SELECT 'pinboard_enabled', pinboard_enabled FROM config UNION
-            SELECT 'pinboard_token', pinboard_token FROM config UNION
-            SELECT 'pinboard_tags', pinboard_tags FROM config UNION
-            SELECT 'instapaper_enabled', instapaper_enabled FROM config UNION
-            SELECT 'instapaper_username', instapaper_username FROM config UNION
-            SELECT 'instapaper_password', instapaper_password FROM config UNION
-            SELECT 'image_proxy', image_proxy FROM config UNION
-            SELECT 'favicons', favicons FROM config
-    ");
-
-    $pdo->exec('DROP TABLE config');
-}
-
-function version_34(PDO $pdo)
-{
-    $pdo->exec('ALTER TABLE config ADD COLUMN favicons INTEGER DEFAULT 0');
-
-    $pdo->exec(
-        'CREATE TABLE favicons (
-            feed_id INTEGER UNIQUE,
-            link TEXT,
-            icon TEXT,
-            FOREIGN KEY(feed_id) REFERENCES feeds(id) ON DELETE CASCADE
-        )'
-    );
-}
-
-function version_33(PDO $pdo)
-{
-    $pdo->exec('ALTER TABLE config ADD COLUMN image_proxy INTEGER DEFAULT 0');
-}
-
-function version_32(PDO $pdo)
-{
-    $pdo->exec('ALTER TABLE config ADD COLUMN instapaper_enabled INTEGER DEFAULT 0');
-    $pdo->exec('ALTER TABLE config ADD COLUMN instapaper_username TEXT');
-    $pdo->exec('ALTER TABLE config ADD COLUMN instapaper_password TEXT');
-}
-
-function version_31(PDO $pdo)
-{
-    $pdo->exec('ALTER TABLE config ADD COLUMN pinboard_enabled INTEGER DEFAULT 0');
-    $pdo->exec('ALTER TABLE config ADD COLUMN pinboard_token TEXT');
-    $pdo->exec('ALTER TABLE config ADD COLUMN pinboard_tags TEXT DEFAULT "miniflux"');
-}
-
-function version_30(PDO $pdo)
-{
-    $pdo->exec('ALTER TABLE config ADD COLUMN autoflush_unread INTEGER DEFAULT 45');
-}
-
-function version_29(PDO $pdo)
-{
-    $pdo->exec('ALTER TABLE config ADD COLUMN fever_token INTEGER DEFAULT "'.substr(Helper\generate_token(), 0, 8).'"');
-}
-
-function version_28(PDO $pdo)
-{
-    $pdo->exec('ALTER TABLE feeds ADD COLUMN rtl INTEGER DEFAULT 0');
-}
-
-function version_27(PDO $pdo)
-{
-    $pdo->exec('ALTER TABLE config ADD COLUMN items_display_mode TEXT DEFAULT "summaries"');
-}
-
-function version_26(PDO $pdo)
-{
-    $pdo->exec('ALTER TABLE config ADD COLUMN bookmarklet_token TEXT DEFAULT "'.Helper\generate_token().'"');
-}
-
-function version_25(PDO $pdo)
-{
-    $pdo->exec(
-        'CREATE TABLE remember_me (
-            id INTEGER PRIMARY KEY,
-            username TEXT,
-            ip TEXT,
-            user_agent TEXT,
-            token TEXT,
-            sequence TEXT,
-            expiration INTEGER,
-            date_creation INTEGER
-        )'
-    );
-}
-
-function version_24(PDO $pdo)
-{
-    $pdo->exec("ALTER TABLE config ADD COLUMN auto_update_url TEXT DEFAULT 'https://github.com/fguillot/miniflux/archive/master.zip'");
-}
-
-function version_23(PDO $pdo)
-{
-    $pdo->exec('ALTER TABLE items ADD COLUMN language TEXT');
-}
-
-function version_22(PDO $pdo)
-{
-    $pdo->exec("ALTER TABLE config ADD COLUMN timezone TEXT DEFAULT 'UTC'");
-}
-
-function version_21(PDO $pdo)
-{
-    $pdo->exec('ALTER TABLE items ADD COLUMN enclosure TEXT');
-    $pdo->exec('ALTER TABLE items ADD COLUMN enclosure_type TEXT');
-}
-
-function version_20(PDO $pdo)
-{
-    $pdo->exec('ALTER TABLE config ADD COLUMN redirect_nothing_to_read TEXT DEFAULT "feeds"');
-}
-
-function version_19(PDO $pdo)
-{
-    $rq = $pdo->prepare('SELECT autoflush FROM config');
-    $rq->execute();
-    $value = (int) $rq->fetchColumn();
-
-    // Change default value of autoflush to 15 days to avoid very large database
-    if ($value <= 0) {
-        $rq = $pdo->prepare('UPDATE config SET autoflush=?');
-        $rq->execute(array(15));
-    }
-}
-
-function version_18(PDO $pdo)
-{
-    $pdo->exec('ALTER TABLE feeds ADD COLUMN parsing_error INTEGER DEFAULT 0');
-}
-
-function version_17(PDO $pdo)
-{
-    $pdo->exec('ALTER TABLE config ADD COLUMN items_sorting_direction TEXT DEFAULT "desc"');
-}
-
-function version_16(PDO $pdo)
-{
-    $pdo->exec('ALTER TABLE config ADD COLUMN auth_google_token TEXT DEFAULT ""');
-    $pdo->exec('ALTER TABLE config ADD COLUMN auth_mozilla_token TEXT DEFAULT ""');
-}
-
-function version_15(PDO $pdo)
-{
-    $pdo->exec('ALTER TABLE feeds ADD COLUMN download_content INTEGER DEFAULT 0');
-}
-
-function version_14(PDO $pdo)
-{
-    $pdo->exec('ALTER TABLE config ADD COLUMN feed_token TEXT DEFAULT "'.Helper\generate_token().'"');
-}
-
-function version_13(PDO $pdo)
-{
-    $pdo->exec('ALTER TABLE feeds ADD COLUMN enabled INTEGER DEFAULT 1');
-}
-
-function version_12(PDO $pdo)
-{
-    $pdo->exec('ALTER TABLE config ADD COLUMN api_token TEXT DEFAULT "'.Helper\generate_token().'"');
-}
-
-function version_11(PDO $pdo)
-{
-    $rq = $pdo->prepare('
-        SELECT
-        items.id, items.url AS item_url, feeds.site_url
-        FROM items
-        LEFT JOIN feeds ON feeds.id=items.feed_id
-    ');
-
-    $rq->execute();
-
-    $items = $rq->fetchAll(PDO::FETCH_ASSOC);
-
-    foreach ($items as $item) {
-        if ($item['id'] !== $item['item_url']) {
-            $id = hash('crc32b', $item['id'].$item['site_url']);
-        } else {
-            $id = hash('crc32b', $item['item_url'].$item['site_url']);
-        }
-
-        $rq = $pdo->prepare('UPDATE items SET id=? WHERE id=?');
-        $rq->execute(array($id, $item['id']));
-    }
-}
-
-function version_10(PDO $pdo)
-{
-    $pdo->exec('ALTER TABLE config ADD COLUMN theme TEXT DEFAULT "original"');
-}
-
-function version_9(PDO $pdo)
-{
-    $pdo->exec('ALTER TABLE config ADD COLUMN items_per_page INTEGER DEFAULT 100');
-}
-
-function version_8(PDO $pdo)
-{
-    $pdo->exec('ALTER TABLE items ADD COLUMN bookmark INTEGER DEFAULT 0');
-}
-
-function version_7(PDO $pdo)
-{
-    $pdo->exec('ALTER TABLE config ADD COLUMN nocontent INTEGER DEFAULT 0');
-}
-
-function version_6(PDO $pdo)
-{
-    $pdo->exec('ALTER TABLE config ADD COLUMN autoflush INTEGER DEFAULT 0');
-}
-
-function version_5(PDO $pdo)
-{
-    $pdo->exec('ALTER TABLE feeds ADD COLUMN last_checked INTEGER');
-}
-
-function version_4(PDO $pdo)
-{
-    $pdo->exec('CREATE INDEX idx_status ON items(status)');
-}
-
-function version_3(PDO $pdo)
-{
-    $pdo->exec("ALTER TABLE config ADD COLUMN language TEXT DEFAULT 'en_US'");
-}
-
-function version_2(PDO $pdo)
-{
-    $pdo->exec('ALTER TABLE feeds ADD COLUMN last_modified TEXT');
-    $pdo->exec('ALTER TABLE feeds ADD COLUMN etag TEXT');
-}
+const VERSION = 1;
 
 function version_1(PDO $pdo)
 {
-    $pdo->exec("
-        CREATE TABLE config (
-            username TEXT DEFAULT 'admin',
-            password TEXT
-        )
-    ");
+    $pdo->exec('CREATE TABLE users (
+        id INTEGER PRIMARY KEY,
+        username TEXT UNIQUE,
+        password TEXT NOT NULL,
+        is_admin INTEGER DEFAULT 0,
+        last_login INTEGER,
+        api_token TEXT NOT NULL UNIQUE,
+        bookmarklet_token TEXT NOT NULL UNIQUE,
+        cronjob_token TEXT NOT NULL UNIQUE,
+        feed_token TEXT NOT NULL UNIQUE,
+        fever_token TEXT NOT NULL UNIQUE,
+        fever_api_key TEXT NOT NULL UNIQUE
+    )');
 
-    $pdo->exec("
-        INSERT INTO config
-        (password)
-        VALUES ('".\password_hash('admin', PASSWORD_BCRYPT)."')
-    ");
+    $pdo->exec('CREATE TABLE user_settings (
+        "user_id" INTEGER NOT NULL,
+        "key" TEXT NOT NULL,
+        "value" TEXT NOT NULL,
+        PRIMARY KEY("user_id", "key"),
+        FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
+    )');
 
-    $pdo->exec('
-        CREATE TABLE feeds (
-            id INTEGER PRIMARY KEY,
-            site_url TEXT,
-            feed_url TEXT UNIQUE,
-            title TEXT COLLATE NOCASE
-        )
+    $pdo->exec('CREATE TABLE feeds (
+        id INTEGER PRIMARY KEY,
+        user_id INTEGER NOT NULL,
+        feed_url TEXT NOT NULL,
+        site_url TEXT,
+        title TEXT COLLATE NOCASE,
+        last_checked INTEGER DEFAULT 0,
+        last_modified TEXT,
+        etag TEXT,
+        enabled INTEGER DEFAULT 1,
+        download_content INTEGER DEFAULT 0,
+        parsing_error INTEGER DEFAULT 0,
+        rtl INTEGER DEFAULT 0,
+        cloak_referrer INTEGER DEFAULT 0,
+        FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
+        UNIQUE(user_id, feed_url)
+    )');
+
+    $pdo->exec('CREATE TABLE items (
+        id INTEGER PRIMARY KEY,
+        user_id INTEGER NOT NULL,
+        feed_id INTEGER NOT NULL,
+        checksum TEXT NOT NULL,
+        status TEXT,
+        bookmark INTEGER DEFAULT 0,
+        url TEXT,
+        title TEXT COLLATE NOCASE,
+        author TEXT,
+        content TEXT,
+        updated INTEGER,
+        enclosure_url TEXT,
+        enclosure_type TEXT,
+        language TEXT,
+        rtl INTEGER DEFAULT 0,
+        FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
+        FOREIGN KEY(feed_id) REFERENCES feeds(id) ON DELETE CASCADE,
+        UNIQUE(feed_id, checksum)
+    )');
+
+    $pdo->exec('CREATE TABLE "groups" (
+        id INTEGER PRIMARY KEY,
+        user_id INTEGER NOT NULL,
+        title TEXT COLLATE NOCASE,
+        FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
+        UNIQUE(user_id, title)
+    )');
+
+    $pdo->exec('CREATE TABLE "feeds_groups" (
+        feed_id INTEGER  NOT NULL,
+        group_id INTEGER NOT NULL,
+        PRIMARY KEY(feed_id, group_id),
+        FOREIGN KEY(group_id) REFERENCES groups(id) ON DELETE CASCADE,
+        FOREIGN KEY(feed_id) REFERENCES feeds(id) ON DELETE CASCADE
+    )');
+
+    $pdo->exec('CREATE TABLE favicons (
+        id INTEGER PRIMARY KEY,
+        hash TEXT UNIQUE,
+        type TEXT
+    )');
+
+    $pdo->exec('CREATE TABLE "favicons_feeds" (
+        feed_id INTEGER NOT NULL,
+        favicon_id INTEGER NOT NULL,
+        PRIMARY KEY(feed_id, favicon_id),
+        FOREIGN KEY(favicon_id) REFERENCES favicons(id) ON DELETE CASCADE,
+        FOREIGN KEY(feed_id) REFERENCES feeds(id) ON DELETE CASCADE
+    )');
+
+    $pdo->exec('CREATE TABLE remember_me (
+        id INTEGER PRIMARY KEY,
+        user_id INTEGER NOT NULL,
+        ip TEXT,
+        user_agent TEXT,
+        token TEXT,
+        sequence TEXT,
+        expiration INTEGER,
+        date_creation INTEGER,
+        FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
+    )');
+
+    $fever_token = Helper\generate_token();
+    $rq = $pdo->prepare('
+      INSERT INTO users
+      (username, password, is_admin, api_token, bookmarklet_token, cronjob_token, feed_token, fever_token, fever_api_key)
+      VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
     ');
 
-    $pdo->exec('
-        CREATE TABLE items (
-            id TEXT PRIMARY KEY,
-            url TEXT,
-            title TEXT,
-            author TEXT,
-            content TEXT,
-            updated INTEGER,
-            status TEXT,
-            feed_id INTEGER,
-            FOREIGN KEY(feed_id) REFERENCES feeds(id) ON DELETE CASCADE
-        )
-    ');
+    $rq->execute(array(
+        'admin',
+        password_hash('admin', PASSWORD_BCRYPT),
+        '1',
+        Helper\generate_token(),
+        Helper\generate_token(),
+        Helper\generate_token(),
+        Helper\generate_token(),
+        $fever_token,
+        md5('admin:'.$fever_token),
+    ));
 }
diff --git a/app/templates/about.php b/app/templates/about.php
index dd84381..9263f8c 100644
--- a/app/templates/about.php
+++ b/app/templates/about.php
@@ -3,9 +3,12 @@