diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,5 @@
+# Revision history for aws-academy-grade-exporter
+
+## 0.1.0.0 -- YYYY-mm-dd
+
+* First version. Released on an unsuspecting world.
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2025 Alexander Goussas
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/aws-academy-grade-exporter.cabal b/aws-academy-grade-exporter.cabal
new file mode 100644
--- /dev/null
+++ b/aws-academy-grade-exporter.cabal
@@ -0,0 +1,56 @@
+cabal-version:   3.0
+name:            aws-academy-grade-exporter
+version:         0.1.0.0
+synopsis:        Export grades from AWS Academy to different formats
+description:
+  A CLI tool for exporting grades from AWS Academy courses to various formats and sinks.
+
+homepage:        https://github.com/aloussase/aws-academy-grade-exporter-hs
+license:         MIT
+license-file:    LICENSE
+author:          Alexander Goussas
+maintainer:      goussasalexander@gmail.com
+
+-- copyright:
+category:        Text
+build-type:      Simple
+extra-doc-files: CHANGELOG.md
+
+-- extra-source-files:
+
+source-repository head
+  type:     git
+  location:
+    https://github.com/disel-espol/aws-academy-grades-exporter-hs
+
+common warnings
+  ghc-options: -Wall
+
+executable aws-academy-grade-exporter
+  import:             warnings
+  main-is:            Main.hs
+  other-modules:
+    Exporter
+    Options
+    Pg
+    Row
+
+  -- other-extensions:
+  build-depends:
+    , aeson                 >=2.2.3     && <2.3
+    , base                  ^>=4.17.2.1
+    , bytestring            >=0.11.5    && <0.12
+    , cassava               >=0.5.3     && <0.6
+    , optparse-applicative  >=0.18.1    && <0.19
+    , postgresql-simple     >=0.7.0     && <0.8
+    , req                   >=3.13.4    && <3.14
+    , text                  >=2.0.2     && <2.1
+    , vector                >=0.13.2    && <0.14
+
+  hs-source-dirs:     src
+  default-language:   Haskell2010
+  default-extensions:
+    DeriveAnyClass
+    DeriveGeneric
+    DerivingStrategies
+    OverloadedStrings
diff --git a/src/Exporter.hs b/src/Exporter.hs
new file mode 100644
--- /dev/null
+++ b/src/Exporter.hs
@@ -0,0 +1,21 @@
+module Exporter where
+
+import           Control.Exception
+import qualified Pg
+import           Row
+
+data ExporterHandle = ExporterHandle
+  { ehExport :: [Row] -> IO ()
+  }
+
+
+stdoutExporter :: ExporterHandle
+stdoutExporter = ExporterHandle print
+
+postgresExporter :: ExporterHandle
+postgresExporter = ExporterHandle $ \rows -> do
+  bracket
+    (Pg.connect)
+    (Pg.disconnect)
+    (\conn -> Pg.insertRows conn rows)
+
diff --git a/src/Main.hs b/src/Main.hs
new file mode 100644
--- /dev/null
+++ b/src/Main.hs
@@ -0,0 +1,26 @@
+module Main where
+
+import qualified Data.ByteString.Lazy.Char8 as BL8
+import           Data.Csv
+import qualified Data.Vector                as V
+import           Exporter
+import qualified Options
+import           Row
+import           System.IO                  (readFile')
+
+readRows :: FilePath -> IO [Row]
+readRows fileName = do
+  contents <- readFile' fileName
+  case decode HasHeader $ BL8.pack contents of
+    Left err -> error err
+    Right v  -> return $ V.toList v
+
+doExport :: Options.Dest -> [Row] -> IO ()
+doExport Options.Postgres rows = Exporter.ehExport postgresExporter $ rows
+doExport Options.Stdout rows   = Exporter.ehExport stdoutExporter $ rows
+
+main :: IO ()
+main = do
+  opts <- Options.parseOpts
+  rows <- readRows (Options.optFile opts)
+  doExport (Options.optDest opts) rows
diff --git a/src/Options.hs b/src/Options.hs
new file mode 100644
--- /dev/null
+++ b/src/Options.hs
@@ -0,0 +1,42 @@
+module Options (parseOpts, Opt (..), Dest (..)) where
+
+import           Options.Applicative
+
+data Dest = Postgres | Stdout
+  deriving Show
+
+data Opt = MkOpt
+  { optDest :: Dest
+  , optFile :: FilePath
+  }
+  deriving Show
+
+opt :: Parser Opt
+opt = MkOpt
+  <$> option readDest
+      ( long "destination"
+      <> short 'd'
+      <> help "Where to export the data"
+      )
+  <*> strOption
+      ( long "file"
+      <> short 'f'
+      <> help "The file to be processed"
+      )
+
+  where
+    readDest = eitherReader $ \s ->
+      case s of
+        "postgres" -> pure Postgres
+        "stdout"   -> pure Stdout
+        _          -> Left "Invalid exporter"
+
+parseOpts :: IO Opt
+parseOpts = execParser opts
+
+opts :: ParserInfo Opt
+opts = info (opt <**> helper)
+      ( fullDesc
+      <> progDesc "Export grade from AWS Academy to various different formats"
+      <> header "aws-academy-grade-exporter - a tool for exporting grades from AWS Academy to various formats"
+      )
diff --git a/src/Pg.hs b/src/Pg.hs
new file mode 100644
--- /dev/null
+++ b/src/Pg.hs
@@ -0,0 +1,57 @@
+module Pg where
+
+import           Control.Monad              (forM, void)
+import           Data.Aeson                 (FromJSON)
+import qualified Data.ByteString.Char8      as BL8
+import           Data.List                  (isSuffixOf)
+import qualified Data.Text                  as T
+import           Database.PostgreSQL.Simple
+import           GHC.Generics               (Generic)
+import           Network.HTTP.Req
+import           Row
+import           System.Environment
+
+connect :: IO Connection
+connect = do
+  connStr <- getEnv "DB_URL"
+  conn <- connectPostgreSQL $ BL8.pack connStr
+  return conn
+
+data StudentData = MkStudentData
+  { nombres   :: !String
+  , apellidos :: !String
+  }
+  deriving stock (Show, Generic)
+  deriving anyclass (FromJSON)
+
+insertRows :: Connection -> [Row] -> IO  ()
+insertRows conn rows = do
+  void $ executeMany
+    conn
+    "insert into grades values (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"
+    rows
+
+  students <- forM rows $ \row -> do
+    -- TODO: Should do this in parallel.
+    let studentEmail = getStudentEmail row
+
+    runReq defaultHttpConfig $ do
+      r <- req
+        GET
+        (https "wsarchivos.espol.edu.ec" /: "api" /: "consultas" /: "persona" /: (T.pack studentEmail))
+        NoReqBody
+        jsonResponse
+        mempty
+
+      return $ head $ map (\sd -> (studentEmail, nombres sd, apellidos sd)) (responseBody r :: [StudentData])
+
+  void $ executeMany conn "insert into students values (?,?,?)" (students :: [(String, String, String)])
+
+getStudentEmail :: Row -> String
+getStudentEmail row
+  | not (null $ rLoginID row) = rLoginID row
+  | "@espol.edu.ec" `isSuffixOf` rStudent row = rStudent row
+  | otherwise = rStudent row ++ "@espol.edu.ec"
+
+disconnect :: Connection -> IO ()
+disconnect conn = close conn
diff --git a/src/Row.hs b/src/Row.hs
new file mode 100644
--- /dev/null
+++ b/src/Row.hs
@@ -0,0 +1,60 @@
+module Row where
+
+
+import           Data.Csv
+import           Database.PostgreSQL.Simple.ToRow
+import           GHC.Generics                     (Generic)
+
+data Row = MkRow
+  { rStudent                         :: !String
+  , rId                              :: !Int
+  , rLoginID                         :: !String
+  , rSection                         :: !String
+  , rModule1KC                       :: !(Maybe Double)
+  , rModule2KC                       :: !(Maybe Double)
+  , rModule3KC                       :: !(Maybe Double)
+  , rModule4KC                       :: !(Maybe Double)
+  , rModule5KC                       :: !(Maybe Double)
+  , rModule6KC                       :: !(Maybe Double)
+  , rModule7KC                       :: !(Maybe Double)
+  , rModule8KC                       :: !(Maybe Double)
+  , rModule9KC                       :: !(Maybe Double)
+  , rModule10KC                      :: !(Maybe Double)
+  , rCourseAssesment                 :: !(Maybe Double)
+  , rLab1                            :: !(Maybe Double)
+  , rLab2                            :: !(Maybe Double)
+  , rLab3                            :: !(Maybe Double)
+  , rActivityAwsLambda               :: !(Maybe Double)
+  , rActivityElasticBeanstalk        :: !(Maybe Double)
+  , rLab4                            :: !(Maybe Double)
+  , rLab5                            :: !(Maybe Double)
+  , rLab6                            :: !(Maybe Double)
+  , rAssignmentsCurrentPoints        :: !(Maybe Double)
+  , rAssignmentsFinalPoints          :: !(Maybe Double)
+  , rAssignmentsCurrentScore         :: !(Maybe Double)
+  , rAssignmentsUnpostedCurrentScore :: !(Maybe Double)
+  , rAssignmentsFinalScore           :: !(Maybe Double)
+  , rAssignmentsUnpostedFinalScore   :: !(Maybe Double)
+  , rKCCurrentPoints                 :: !(Maybe Double)
+  , rKCFinalPoints                   :: !(Maybe Double)
+  , rKCCurrentScore                  :: !(Maybe Double)
+  , rKCUnpostedCurrentScore          :: !(Maybe Double)
+  , rKCFinalScore                    :: !(Maybe Double)
+  , rKCUnpostedFinalScore            :: !(Maybe Double)
+  , rLabsCurrentPoints               :: !(Maybe Double)
+  , rLabsFinalPoints                 :: !(Maybe Double)
+  , rLabsCurrentScore                :: !(Maybe Double)
+  , rLabsUnpostedCurrentScore        :: !(Maybe Double)
+  , rLabsFinalScore                  :: !(Maybe Double)
+  , rLabsUnpostedFinalScore          :: !(Maybe Double)
+  , rCurrentPoints                   :: !(Maybe Double)
+  , rFinalPoints                     :: !(Maybe Double)
+  , rCurrentScore                    :: !(Maybe Double)
+  , rUnpostedCurrentScore            :: !(Maybe Double)
+  , rFinalScore                      :: !(Maybe Double)
+  , rUnpostedFinalScore              :: !(Maybe Double)
+  }
+  deriving stock (Show, Generic)
+  deriving anyclass (ToRow)
+
+instance FromRecord Row
