diff --git a/updates_notifier/entry.py b/updates_notifier/entry.py
new file mode 100644
index 0000000000000000000000000000000000000000..57481f4457ba2bea4c55a9b13403134df24004ac
--- /dev/null
+++ b/updates_notifier/entry.py
@@ -0,0 +1,10 @@
+from dataclasses import dataclass
+
+
+@dataclass
+class Entry:
+    feed_id: int
+    tag_id: str
+    name: str
+    version: str
+    url: str
diff --git a/updates_notifier/fetchers/__init__.py b/updates_notifier/fetchers/__init__.py
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..a6b34eb1964de533fa1e10e292b7e9f2c9ef8a63 100644
--- a/updates_notifier/fetchers/__init__.py
+++ b/updates_notifier/fetchers/__init__.py
@@ -0,0 +1 @@
+from .project_fetcher import ProjectFetcher
diff --git a/updates_notifier/fetchers/github_fetcher.py b/updates_notifier/fetchers/github_fetcher.py
new file mode 100644
index 0000000000000000000000000000000000000000..0b572fdf7fa85d81ee0863af48611e46485b9ff1
--- /dev/null
+++ b/updates_notifier/fetchers/github_fetcher.py
@@ -0,0 +1,43 @@
+import logging
+import requests
+from typing import List
+
+from ..entry import Entry
+from .project_fetcher import ProjectFetcher
+
+logger = logging.getLogger(__name__)
+
+
+class GithubFetcher(ProjectFetcher):
+    def __init__(self, feed_id: int, name: str, repository: str):
+        super().__init__(feed_id, name)
+
+        self.repository = repository
+
+    def _get_releases_url(self) -> str:
+        return f"https://api.github.com/repos/{self.repository}/releases"
+
+    def fetch_entries(self) -> List[Entry]:
+        req = requests.get(self._get_releases_url())
+        if req.status_code != requests.codes.ok:
+            logger.error(
+                "Failed to get %s at %s, server returned %s.",
+                self.name,
+                self.repository,
+                req.status_code,
+            )
+            return []
+
+        entries: List[Entry] = []
+
+        try:
+            for remote_entry in req.json():
+                tag_id = remote_entry["id"]
+                tag_name = remote_entry["tag_name"]
+                url = remote_entry["html_url"]
+                entries.append(Entry(self.feed_id, tag_id, self.name, tag_name, url))
+        except requests.exceptions.JSONDecodeError as err:
+            logger.error(
+                "Failed to parse entries of project %s: %s", self.name, str(err)
+            )
+        return entries
diff --git a/updates_notifier/fetchers/project_fetcher.py b/updates_notifier/fetchers/project_fetcher.py
new file mode 100644
index 0000000000000000000000000000000000000000..56cf664b8a58f1ee1ee6615e1ba378732023af40
--- /dev/null
+++ b/updates_notifier/fetchers/project_fetcher.py
@@ -0,0 +1,14 @@
+from abc import ABCMeta, abstractmethod
+from typing import List
+
+from ..entry import Entry
+
+
+class ProjectFetcher(metaclass=ABCMeta):
+    def __init__(self, feed_id: int, name: str):
+        self.feed_id = feed_id
+        self.name = name
+
+    @abstractmethod
+    def fetch_entries(self) -> List[Entry]:
+        pass