Repository: mlco2/codecarbon (language: python)

Issue:
Api routers testing
Init Postman Collection for testing purposes
Fix / refacto



Discussion / hints:

Fix patch:
diff --git a/.flake8 b/.flake8
index f2d7ac86a..b036933c0 100644
--- a/.flake8
+++ b/.flake8
@@ -3,5 +3,5 @@
 
 [flake8]
 select = C,E,F,W,B
-ignore = E203, E501, W503
+ignore = E203, E501, W503, B008
 copyright-check = True
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index de1ca32c1..10b56e719 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -1,4 +1,4 @@
-name: build
+name: build package & server
 
 on: [push, pull_request]
 
@@ -19,5 +19,22 @@ jobs:
       run: |
         python -m pip install --upgrade pip
         pip install tox tox-gh-actions
-    - name: Test with tox
+    - name: Test package with tox
       run: tox -vv
+
+  build_server:
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v2
+    - name: Set up Python 3.7.10
+      uses: actions/setup-python@v2
+      with:
+        python-version: '3.7'
+    - name: Install dependencies
+      run: |
+        python -m pip install --upgrade pip
+        pip install tox tox-gh-actions
+    - name: Unit tests on api with tox
+      run: |
+        cd carbonserver/
+        tox -e unit -vv
diff --git a/.gitignore b/.gitignore
index 3854fe4bc..ee7f4167f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -111,3 +111,6 @@ docs/_build/
 # OS generated files
 .DS_Store
 .DS_Store?
+
+# local database
+code_carbon.db
\ No newline at end of file
diff --git a/.isort.cfg b/.isort.cfg
index daa9a5a0c..6977d3e15 100644
--- a/.isort.cfg
+++ b/.isort.cfg
@@ -1,3 +1,3 @@
 [settings]
 profile=black
-multi_line_output=3
+multi_line_output=3
\ No newline at end of file
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 353157de9..1e78e1e3d 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -14,4 +14,4 @@ repos:
       hooks:
           - id: flake8
             args: ["--config=.flake8"]
-            additional_dependencies: ['flake8-bugbear==21.4.3']
+            additional_dependencies: ['flake8-bugbear==21.4.3']
\ No newline at end of file
diff --git a/README.md b/README.md
index d73532070..0ff9777e6 100644
--- a/README.md
+++ b/README.md
@@ -148,8 +148,8 @@ python examples/mnist_decorator.py
 This will create a `.csv` file with information about the energy that you used to carry out the classification task, and an estimate of the CO<sub>2</sub> that you generated, complete with comparisons to common modes of transportation to give you a better idea of the order of magnitude of your emissions.
 
 
-
 # Built-in Visualization Tool :heart_eyes:
+
 To track the evolution of your CO<sub>2</sub> emissions or just to see some nice graphs and charts, you can use the visualization tool that we created.
 As input, it takes a .csv file in the format generated by the Emissions Tracker, and it generates a visualization of the emissions incurred.
 
diff --git a/act/Dockerfile b/act/Dockerfile
new file mode 100644
index 000000000..be5b863bd
--- /dev/null
+++ b/act/Dockerfile
@@ -0,0 +1,13 @@
+FROM ubuntu:20.04
+
+RUN apt-get update \
+    && apt update -y \
+    && apt-get upgrade -y \
+    && DEBIAN_FRONTEND=noninteractive apt-get install -y \
+    build-essential \
+    curl \
+    nodejs \
+    npm \
+    sudo \
+
+    && rm -rf /var/lib/apt/lists/*
diff --git a/api/Images/CodecarbonDB.jpg b/api/Images/CodecarbonDB.jpg
new file mode 100644
index 000000000..95d9e2256
Binary files /dev/null and b/api/Images/CodecarbonDB.jpg differ
diff --git a/carbonserver/Images/CodecarbonDB.jpg b/carbonserver/Images/CodecarbonDB.jpg
new file mode 100644
index 000000000..a5f21b0ba
Binary files /dev/null and b/carbonserver/Images/CodecarbonDB.jpg differ
diff --git a/carbonserver/README.md b/carbonserver/README.md
new file mode 100644
index 000000000..4d1b97266
--- /dev/null
+++ b/carbonserver/README.md
@@ -0,0 +1,11 @@
+![](https://github.com/mlco2/codecarbon/blob/api/api/Images/CodecarbonDB.jpg)
+
+
+Documentation :
+
+- Routers handling: https://fastapi.tiangolo.com/tutorial/bigger-applications/
+- Security: https://fastapi.tiangolo.com/tutorial/security/
+- SQL: https://fastapi.tiangolo.com/tutorial/sql-databases/
+- Alembic : https://youtu.be/36yw8VC3KU8
+- Deploy a full stack FastAPI / Vue.js app : https://github.com/tiangolo/full-stack-fastapi-postgresql
+- FastAPI + PG on docker : https://testdriven.io/blog/fastapi-docker-traefik
diff --git a/carbonserver/__init__.py b/carbonserver/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/carbonserver/carbonserver/__init__.py b/carbonserver/carbonserver/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/carbonserver/carbonserver/api/__init__.py b/carbonserver/carbonserver/api/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/carbonserver/carbonserver/api/dependencies.py b/carbonserver/carbonserver/api/dependencies.py
new file mode 100644
index 000000000..19841c018
--- /dev/null
+++ b/carbonserver/carbonserver/api/dependencies.py
@@ -0,0 +1,28 @@
+from typing import Optional
+
+from fastapi import Header, HTTPException
+
+from carbonserver.database.database import SessionLocal
+
+
+async def get_token_header(
+    x_token: Optional[str] = Header(
+        "fake-super-secret-token", convert_underscores=False
+    )
+):
+    if x_token != "fake-super-secret-token":
+        raise HTTPException(status_code=400, detail="X-Token header invalid")
+
+
+async def get_query_token(token: Optional[str] = "jessica"):
+    if token != "jessica":
+        raise HTTPException(status_code=400, detail="No Jessica token provided")
+
+
+# Dependency
+def get_db():
+    db = SessionLocal()
+    try:
+        yield db
+    finally:
+        db.close()
diff --git a/carbonserver/carbonserver/api/domain/__init__.py b/carbonserver/carbonserver/api/domain/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/carbonserver/carbonserver/api/domain/emissions.py b/carbonserver/carbonserver/api/domain/emissions.py
new file mode 100644
index 000000000..9b7155be4
--- /dev/null
+++ b/carbonserver/carbonserver/api/domain/emissions.py
@@ -0,0 +1,18 @@
+import abc
+from typing import List
+
+from carbonserver.api import schemas
+
+
+class Emissions(abc.ABC):
+    @abc.abstractmethod
+    def add_emission(self, emission: schemas.EmissionCreate):
+        raise NotImplementedError
+
+    @abc.abstractmethod
+    def get_one_emission(self, emission_id) -> schemas.Emission:
+        raise NotImplementedError
+
+    @abc.abstractmethod
+    def get_emissions_from_run(self, run_id) -> List[schemas.Emission]:
+        raise NotImplementedError
diff --git a/carbonserver/carbonserver/api/domain/experiments.py b/carbonserver/carbonserver/api/domain/experiments.py
new file mode 100644
index 000000000..cdadf2f3b
--- /dev/null
+++ b/carbonserver/carbonserver/api/domain/experiments.py
@@ -0,0 +1,18 @@
+import abc
+from typing import List
+
+from carbonserver.api import schemas
+
+
+class Experiments(abc.ABC):
+    @abc.abstractmethod
+    def add_experiment(self, experiment: schemas.ExperimentCreate):
+        raise NotImplementedError
+
+    @abc.abstractmethod
+    def get_one_experiment(self, experiment_id):
+        raise NotImplementedError
+
+    @abc.abstractmethod
+    def get_experiments_from_project(self, project_id) -> List[schemas.Experiment]:
+        raise NotImplementedError
diff --git a/carbonserver/carbonserver/api/domain/organizations.py b/carbonserver/carbonserver/api/domain/organizations.py
new file mode 100644
index 000000000..76579eac3
--- /dev/null
+++ b/carbonserver/carbonserver/api/domain/organizations.py
@@ -0,0 +1,17 @@
+import abc
+
+from carbonserver.api import schemas
+
+
+class Organizations(abc.ABC):
+    @abc.abstractmethod
+    def add_organization(self, organization: schemas.OrganizationCreate):
+        raise NotImplementedError
+
+    @abc.abstractmethod
+    def get_one_organization(self, organization_id):
+        raise NotImplementedError
+
+    @abc.abstractmethod
+    def get_team_from_organizations(self, organization_id):
+        raise NotImplementedError
diff --git a/carbonserver/carbonserver/api/domain/projects.py b/carbonserver/carbonserver/api/domain/projects.py
new file mode 100644
index 000000000..d84305a7b
--- /dev/null
+++ b/carbonserver/carbonserver/api/domain/projects.py
@@ -0,0 +1,13 @@
+import abc
+
+from carbonserver.api import schemas
+
+
+class Projects(abc.ABC):
+    @abc.abstractmethod
+    def add_project(self, project: schemas.ProjectCreate):
+        raise NotImplementedError
+
+    @abc.abstractmethod
+    def get_one_project(self, project_id):
+        raise NotImplementedError
diff --git a/carbonserver/carbonserver/api/domain/runs.py b/carbonserver/carbonserver/api/domain/runs.py
new file mode 100644
index 000000000..3e85fcc52
--- /dev/null
+++ b/carbonserver/carbonserver/api/domain/runs.py
@@ -0,0 +1,9 @@
+import abc
+
+from carbonserver.api import schemas
+
+
+class Runs(abc.ABC):
+    @abc.abstractmethod
+    def add_run(self, run: schemas.RunCreate):
+        raise NotImplementedError
diff --git a/carbonserver/carbonserver/api/domain/teams.py b/carbonserver/carbonserver/api/domain/teams.py
new file mode 100644
index 000000000..f7847b8a0
--- /dev/null
+++ b/carbonserver/carbonserver/api/domain/teams.py
@@ -0,0 +1,17 @@
+import abc
+
+from carbonserver.api import schemas
+
+
+class Teams(abc.ABC):
+    @abc.abstractmethod
+    def add_team(self, team: schemas.TeamCreate):
+        raise NotImplementedError
+
+    @abc.abstractmethod
+    def get_one_team(self, team_id):
+        raise NotImplementedError
+
+    @abc.abstractmethod
+    def get_projects_from_team(self, team_id):
+        raise NotImplementedError
diff --git a/carbonserver/carbonserver/api/domain/users.py b/carbonserver/carbonserver/api/domain/users.py
new file mode 100644
index 000000000..35a9a6fb4
--- /dev/null
+++ b/carbonserver/carbonserver/api/domain/users.py
@@ -0,0 +1,17 @@
+import abc
+
+from carbonserver.api import schemas
+
+
+class Users(abc.ABC):
+    @abc.abstractmethod
+    def create_user(self, user: schemas.UserCreate):
+        raise NotImplementedError
+
+    @abc.abstractmethod
+    def get_user_by_id(self, user_id: int):
+        raise NotImplementedError
+
+    @abc.abstractmethod
+    def list_users(self):
+        raise NotImplementedError
diff --git a/carbonserver/carbonserver/api/infra/repositories/__init__.py b/carbonserver/carbonserver/api/infra/repositories/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/carbonserver/carbonserver/api/infra/repositories/repository_emissions.py b/carbonserver/carbonserver/api/infra/repositories/repository_emissions.py
new file mode 100644
index 000000000..7e7fe541b
--- /dev/null
+++ b/carbonserver/carbonserver/api/infra/repositories/repository_emissions.py
@@ -0,0 +1,138 @@
+from typing import List
+
+from sqlalchemy.orm import Session
+
+from carbonserver.api import schemas
+from carbonserver.api.domain.emissions import Emissions
+from carbonserver.database import models
+
+"""
+The emissions are stored in the database by this repository class.
+The emission repository is implemented to facilitate tests & the switch of database backend.
+It relies on an abstract repository which exposes an interface of signatures shared by all repository implementations.
+
+"""
+
+
+class SqlAlchemyRepository(Emissions):
+    def __init__(self, db: Session):
+        self.db = db
+
+    @staticmethod
+    def get_db_to_class(emission: models.Emission) -> schemas.Emission:
+        """Convert a models.Emission to a schemas.Emission
+
+        :emission: An Emission in SQLAlchemy format.
+        :returns: An Emission in pyDantic BaseModel format.
+        :rtype: schemas.Emission
+        """
+        return schemas.Emission(
+            id=emission.id,
+            timestamp=emission.timestamp,
+            duration=emission.duration,
+            emissions=emission.emissions,
+            energy_consumed=emission.energy_consumed,
+            run_id=emission.run_id,
+        )
+
+    def add_emission(self, emission: schemas.EmissionCreate):
+        """Save an emission to the database.
+
+        :emission: An Emission in pyDantic BaseModel format.
+        """
+        db_emission = models.Emission(
+            timestamp=emission.timestamp,
+            duration=emission.duration,
+            emissions=emission.emissions,
+            energy_consumed=emission.energy_consumed,
+            run_id=emission.run_id,
+        )
+        self.db.add(db_emission)
+        self.db.commit()
+
+    def get_one_emission(self, emission_id) -> schemas.Emission:
+        """Find the emission in database and return it
+
+        :emission_id: The id of the emission to retreive.
+        :returns: An Emission in pyDantic BaseModel format.
+        :rtype: schemas.Emission
+        """
+        e = (
+            self.db.query(models.Emission)
+            .filter(models.Emission.id == emission_id)
+            .first()
+        )
+        if e is None:
+            return None
+        else:
+            return self.get_db_to_class(e)
+
+    def get_emissions_from_run(self, run_id) -> List[schemas.Emission]:
+        """Find the emissions from an run in database and return it
+
+        :run_id: The id of the run to retreive emissions from.
+        :returns: An Emission in pyDantic BaseModel format.
+        :rtype: List[schemas.Emission]
+        """
+        res = self.db.query(models.Emission).filter(models.Emission.run_id == run_id)
+        if res.first() is None:
+            return []
+        else:
+            # Convert the table of models.Emission to a table of schemas.Emission
+            emissions = []
+            for e in res:
+                emission = self.get_db_to_class(e)
+                emissions.append(emission)
+            return emissions
+
+
+class InMemoryRepository(Emissions):
+    def __init__(self):
+        self.emissions: List = []
+        self.id: int = 0
+
+    def add_emission(self, emission: schemas.EmissionCreate):
+        self.emissions.append(
+            models.Emission(
+                id=self.id + 1,
+                timestamp=emission.timestamp,
+                duration=emission.duration,
+                emissions=emission.emissions,
+                energy_consumed=emission.energy_consumed,
+                run_id=emission.run_id,
+            )
+        )
+
+    def get_one_emission(self, emission_id) -> schemas.Emission:
+        first_emission = self.emissions[0]
+        return schemas.Emission(
+            id=first_emission.id,
+            timestamp=first_emission.timestamp,
+            duration=first_emission.duration,
+            emissions=first_emission.emissions,
+            energy_consumed=first_emission.energy_consumed,
+            run_id=first_emission.run_id,
+        )
+
+    def get_emissions_from_run(self, run_id) -> List[schemas.Emission]:
+        print(len(self.emissions))
+        stored_emissions = [
+            stored_emission
+            for stored_emission in self.emissions
+            if stored_emission.run_id == run_id
+        ]
+        emissions: List[schemas.Emission] = []
+        for emission in stored_emissions:
+            emissions.append(self.get_db_to_class(emission))
+        return emissions
+
+    @staticmethod
+    def get_db_to_class(emission: models.Emission) -> schemas.Emission:
+        return schemas.Emission(
+            id=emission.id,
+            timestamp=emission.timestamp,
+            duration=emission.duration,
+            emissions=emission.emissions,
+            energy_consumed=emission.energy_consumed,
+            run_id=emission.run_id,
+        )
diff --git a/carbonserver/carbonserver/api/infra/repositories/repository_experiments.py b/carbonserver/carbonserver/api/infra/repositories/repository_experiments.py
new file mode 100644
index 000000000..ed5d38a79
--- /dev/null
+++ b/carbonserver/carbonserver/api/infra/repositories/repository_experiments.py
@@ -0,0 +1,180 @@
+from typing import List
+
+from sqlalchemy.orm import Session
+
+from carbonserver.api import schemas
+from carbonserver.api.domain.experiments import Experiments
+from carbonserver.database import models
+
+"""
+Here there is all the method to manipulate the experiment data
+"""
+
+
+class SqlAlchemyRepository(Experiments):
+    def __init__(self, db: Session):
+        self.db = db
+
+    def add_experiment(self, experiment: schemas.ExperimentCreate):
+        # TODO : save experiment in database and get her ID
+        db_experiment = models.Experiment(
+            timestamp=experiment.timestamp,
+            name=experiment.name,
+            description=experiment.description,
+            country_name=experiment.country_name,
+            country_iso_code=experiment.country_iso_code,
+            region=experiment.region,
+            on_cloud=experiment.on_cloud,
+            cloud_provider=experiment.cloud_provider,
+            cloud_region=experiment.cloud_region,
+            # is_active=experiment.is_active,
+            # emission_id=experiment.emission_id,
+            project_id=experiment.project_id,
+        )
+        self.db.add(db_experiment)
+        self.db.commit()
+        self.db.refresh(db_experiment)
+        return db_experiment
+
+    def get_one_experiment(self, experiment_id):
+        """Find the experiment in database and return it
+
+        :experiment_id: The id of the experiment to retreive.
+        :returns: An Experiment in pyDantic BaseModel format.
+        :rtype: schemas.Experiment
+        """
+        e = (
+            self.db.query(models.Experiment)
+            .filter(models.Experiment.id == experiment_id)
+            .first()
+        )
+        if e is None:
+            return None
+        else:
+            return self.get_db_to_class(e)
+
+    def get_experiments_from_project(self, project_id) -> List[schemas.Experiment]:
+        """Find the experiment from an emission in database and return it
+
+        :project_id: The id of the project to retreive experiment from.
+        :returns: An Experiment in pyDantic BaseModel format.
+        :rtype: List[schemas.Experiment]
+        """
+        res = self.db.query(models.Experiment).filter(
+            models.Experiment.project_id == project_id
+        )
+        if res.first() is None:
+            return []
+        else:
+            # Convert the table of models.Emission to a table of schemas.Emission
+            experiments = []
+            for e in res:
+                experiment = self.get_db_to_class(e)
+                experiments.append(experiment)
+            return experiments
+
+    @staticmethod
+    def get_db_to_class(experiment: models.Experiment) -> schemas.Experiment:
+        """Convert a models.Experiment to a schemas.Experiment
+
+        :experiment: An Experiment in SQLAlchemy format.
+        :returns: An Experiment in pyDantic BaseModel format.
+        :rtype: schemas.Experiment
+        """
+        return schemas.Experiment(
+            id=experiment.id,
+            timestamp=experiment.timestamp,
+            name=experiment.name,
+            description=experiment.description,
+            # isactive=experiment.is_active,
+            country_name=experiment.country_name,
+            country_iso_code=experiment.country_iso_code,
+            region=experiment.region,
+            on_cloud=experiment.on_cloud,
+            cloud_provider=experiment.cloud_provider,
+            cloud_region=experiment.cloud_region,
+            project_id=experiment.project_id,
+        )
+
+
+class InMemoryRepository(Experiments):
+    def __init__(self):
+        self.experiments: List = []
+        self.id: int = 0
+
+    def add_experiment(self, experiment: schemas.ExperimentCreate):
+        self.experiments.append(
+            models.Experiment(
+                id=self.id + 1,
+                timestamp=experiment.timestamp,
+                name=experiment.name,
+                description=experiment.description,
+                country_name=experiment.country_name,
+                country_iso_code=experiment.country_iso_code,
+                region=experiment.region,
+                on_cloud=experiment.on_cloud,
+                cloud_provider=experiment.cloud_provider,
+                cloud_region=experiment.cloud_region,
+                # is_active=experiment.is_active,
+                # emission_id=experiment.emission_id,
+                project_id=experiment.project_id,
+            )
+        )
+
+    def get_one_experiment(self, experiment_id: int) -> schemas.Experiment:
+        experiment = self.experiments[0]
+        return schemas.Experiment(
+            id=experiment.id,
+            timestamp=experiment.timestamp,
+            name=experiment.name,
+            description=experiment.description,
+            country_name=experiment.country_name,
+            country_iso_code=experiment.country_iso_code,
+            region=experiment.region,
+            on_cloud=experiment.on_cloud,
+            cloud_provider=experiment.cloud_provider,
+            cloud_region=experiment.cloud_region,
+            # is_active=experiment.is_active,
+            # emission_id=experiment.emission_id,
+            project_id=experiment.project_id,
+        )
+
+    def get_experiments_from_project(self, project_id) -> List[schemas.Experiment]:
+        experiments = []
+        for experiment in self.experiments:
+            print(experiment)
+            if experiment.project_id == project_id:
+                experiments.append(
+                    schemas.Experiment(
+                        id=experiment.id,
+                        timestamp=experiment.timestamp,
+                        name=experiment.name,
+                        description=experiment.description,
+                        country_name=experiment.country_name,
+                        country_iso_code=experiment.country_iso_code,
+                        region=experiment.region,
+                        on_cloud=experiment.on_cloud,
+                        cloud_provider=experiment.cloud_provider,
+                        cloud_region=experiment.cloud_region,
+                        is_active=experiment.is_active,
+                        project_id=project_id,
+                    )
+                )
+        return experiments
+
+    @staticmethod
+    def get_db_to_class(experiment: models.Experiment) -> schemas.Experiment:
+        return schemas.Experiment(
+            id=experiment.id,
+            timestamp=experiment.timestamp,
+            name=experiment.name,
+            description=experiment.description,
+            isactive=experiment.energy_consumed,
+            country_name=experiment.country_name,
+            country_iso_code=experiment.country_iso_code,
+            region=experiment.region,
+            on_cloud=experiment.on_cloud,
+            cloud_provider=experiment.cloud_provider,
+            cloud_region=experiment.cloud_region,
+            project_id=experiment.project_id,
+        )
diff --git a/carbonserver/carbonserver/api/infra/repositories/repository_organizations.py b/carbonserver/carbonserver/api/infra/repositories/repository_organizations.py
new file mode 100644
index 000000000..9376c4e4c
--- /dev/null
+++ b/carbonserver/carbonserver/api/infra/repositories/repository_organizations.py
@@ -0,0 +1,99 @@
+from typing import List
+
+from sqlalchemy.orm import Session
+
+from carbonserver.api import schemas
+from carbonserver.api.domain.organizations import Organizations
+from carbonserver.database import models
+
+"""
+Here there is all the method to manipulate the organization data
+"""
+
+
+class SqlAlchemyRepository(Organizations):
+    def __init__(self, db: Session):
+        self.db = db
+
+    @staticmethod
+    def get_db_to_class(organization: models.Organization) -> schemas.Organization:
+        return schemas.Organization(
+            id=organization.id,
+            name=organization.name,
+            description=organization.description,
+        )
+
+    def add_organization(self, organization: schemas.OrganizationCreate):
+        # TODO : save Organization in database and get her ID
+        db_organization = models.Organization(
+            name=organization.name, description=organization.description
+        )
+        self.db.add(db_organization)
+        self.db.commit()
+        self.db.refresh(db_organization)
+        return db_organization
+
+    def get_one_organization(self, organization_id: str):
+        """Find the organization in database and return it
+
+        :organization_id: The id of the organization to retreive.
+        :returns: An Organization in pyDantic BaseModel format.
+        :rtype: schemas.Organization
+        """
+        e = (
+            self.db.query(models.Organization)
+            .filter(models.Organization.id == organization_id)
+            .first()
+        )
+        if e is None:
+            return None
+        else:
+            return self.get_db_to_class(e)
+
+    def get_team_from_organizations(self, organization_name: str):
+        # TODO : get Organization from team id in database
+        pass
+
+
+class InMemoryRepository(Organizations):
+    def __init__(self):
+        self.organizations: List = []
+        self.id: int = 0
+
+    def get_db_to_class(
+        self, organization: models.Organization
+    ) -> schemas.Organization:
+        return schemas.Organization(
+            id=organization.id,
+            name=organization.name,
+            description=organization.description,
+        )
+
+    def add_organization(self, organization: schemas.OrganizationCreate):
+        self.organizations.append(
+            models.Experiment(
+                id=self.id + 1,
+                name=organization.name,
+                description=organization.description,
+            )
+        )
+
+    def get_one_organization(self, organization_name: str) -> schemas.Organization:
+        organization = self.organizations[0]
+        return schemas.Organization(
+            id=organization.id,
+            name=organization.name,
+            description=organization.description,
+        )
+
+    def get_team_from_organizations(self, organization_name: str):
+        organizations = []
+        for organization in self.organizations:
+            organizations.append(
+                schemas.Organization(
+                    id=organization.id,
+                    name=organization.name,
+                    description=organization.description,
+                )
+            )
+        return organizations
diff --git a/carbonserver/carbonserver/api/infra/repositories/repository_projects.py b/carbonserver/carbonserver/api/infra/repositories/repository_projects.py
new file mode 100644
index 000000000..522c2e2df
--- /dev/null
+++ b/carbonserver/carbonserver/api/infra/repositories/repository_projects.py
@@ -0,0 +1,98 @@
+from typing import List
+
+from sqlalchemy.orm import Session
+
+from carbonserver.api import schemas
+from carbonserver.api.domain.projects import Projects
+from carbonserver.database import models
+
+"""
+Here there is all the method to manipulate the project data
+"""
+
+
+class SqlAlchemyRepository(Projects):
+    def __init__(self, db: Session):
+        self.db = db
+
+    def add_project(self, project: schemas.ProjectCreate):
+        # TODO : save Project in database and get her ID
+        db_project = models.Project(
+            name=project.name,
+            description=project.description,
+            team_id=project.team_id,
+        )
+        self.db.add(db_project)
+        self.db.commit()
+        self.db.refresh(db_project)
+        return db_project
+
+    def get_one_project(self, project_id):
+        """Find the projet in database and return it
+
+        :project_id: The id of the experiment to retreive.
+        :returns: An Project in pyDantic BaseModel format.
+        :rtype: schemas.Project
+        """
+        e = (
+            self.db.query(models.Project)
+            .filter(models.Project.id == project_id)
+            .first()
+        )
+        if e is None:
+            return None
+        else:
+            return self.get_db_to_class(e)
+
+        # TODO : find the Project in database and return it
+        # pass
+
+    @staticmethod
+    def get_db_to_class(project: models.Project) -> schemas.Project:
+        """Convert a models.Project to a schemas.Project
+
+        :project: An Project in SQLAlchemy format.
+        :returns: An Project in pyDantic BaseModel format.
+        :rtype: schemas.Project
+        """
+        return schemas.Project(
+            id=project.id,
+            name=project.name,
+            description=project.description,
+            # isactive=experiment.is_active,
+            team_id=project.team_id,
+        )
+
+
+class InMemoryRepository(Projects):
+    def __init__(self):
+        self.projects: List = []
+        self.id: int = 0
+
+    def add_project(self, project: schemas.ProjectCreate):
+        self.projects.append(
+            models.Project(
+                id=self.id + 1,
+                name=project.name,
+                description=project.description,
+                team_id=project.team_id,
+            )
+        )
+
+    def get_one_project(self, project_id) -> schemas.Project:
+        first_project = self.projects[0]
+        return schemas.Project(
+            id=first_project.id,
+            name=first_project.name,
+            description=first_project.description,
+            team_id=first_project.team_id,
+        )
+
+    @staticmethod
+    def get_db_to_class(self, project: models.Project) -> schemas.Project:
+        return schemas.Project(
+            id=project.id,
+            name=project.name,
+            description=project.description,
+            team_id=project.team_id,
+        )
diff --git a/carbonserver/carbonserver/api/infra/repositories/repository_runs.py b/carbonserver/carbonserver/api/infra/repositories/repository_runs.py
new file mode 100644
index 000000000..c3b4d8110
--- /dev/null
+++ b/carbonserver/carbonserver/api/infra/repositories/repository_runs.py
@@ -0,0 +1,40 @@
+from sqlalchemy.orm import Session
+
+from carbonserver.api import schemas
+from carbonserver.api.domain.runs import Runs
+from carbonserver.database import models
+
+"""
+Here there is all the method to manipulate the experiment data
+"""
+
+
+class SqlAlchemyRepository(Runs):
+    def __init__(self, db: Session):
+        self.db = db
+
+    def add_run(self, run: schemas.RunCreate):
+        """Save an Run to the database.
+        :run: An Run in pyDantic BaseModel format.
+        """
+        db_run = models.Run(
+            timestamp=run.timestamp,
+            experiment_id=run.experiment_id,
+        )
+        self.db.add(db_run)
+        self.db.commit()
+
+    @staticmethod
+    def get_db_to_class(self, run: models.Run) -> schemas.Run:
+        """Convert a models.Run to a schemas.Run
+
+        :emission: An Run in SQLAlchemy format.
+        :returns: An Run in pyDantic BaseModel format.
+        :rtype: schemas.Run
+        """
+        return schemas.Run(
+            id=run.id,
+            timestamp=run.timestamp,
+            emission_id=run.emission_id,
+            experiment_id=run.experiment_id,
+        )
diff --git a/carbonserver/carbonserver/api/infra/repositories/repository_teams.py b/carbonserver/carbonserver/api/infra/repositories/repository_teams.py
new file mode 100644
index 000000000..f08d62e90
--- /dev/null
+++ b/carbonserver/carbonserver/api/infra/repositories/repository_teams.py
@@ -0,0 +1,92 @@
+from typing import List
+
+from sqlalchemy.orm import Session
+
+from carbonserver.api import schemas
+from carbonserver.api.domain.teams import Teams
+from carbonserver.database import models
+
+"""
+Here there is all the method to manipulate the project data
+"""
+
+
+class SqlAlchemyRepository(Teams):
+    def __init__(self, db: Session):
+        self.db = db
+
+    def add_team(self, team: schemas.TeamCreate):
+        # TODO : save Team in database and get her ID
+        db_team = models.Team(
+            name=team.name,
+            description=team.description,
+            organization_id=team.organization_id,
+        )
+        self.db.add(db_team)
+        self.db.commit()
+        self.db.refresh(db_team)
+        return db_team
+
+    def get_one_team(self, team_id):
+        """Find the team in database and return it
+
+        :team_id: The id of the team to retreive.
+        :returns: An Team in pyDantic BaseModel format.
+        :rtype: schemas.Team
+        """
+        e = self.db.query(models.Team).filter(models.Team.id == team_id).first()
+        if e is None:
+            return None
+        else:
+            return self.get_db_to_class(e)
+
+    def get_projects_from_team(self, team_id):
+        # TODO : get Projects from Project id in database
+        pass
+
+    @staticmethod
+    def get_db_to_class(team: models.Team) -> schemas.Team:
+        return schemas.Team(
+            id=team.id,
+            name=team.name,
+            description=team.description,
+            organization_id=team.organization_id,
+        )
+
+
+class InMemoryRepository(Teams):
+    def __init__(self):
+        self.teams: List = []
+        self.id: int = 0
+
+    def add_team(self, team: schemas.TeamCreate):
+        self.teams.append(
+            models.Team(
+                id=self.id + 1,
+                name=team.name,
+                description=team.description,
+                organization_id=team.organization_id,
+            )
+        )
+
+    def get_one_team(self, team_id) -> schemas.Team:
+        first_team = self.teams[0]
+        return schemas.Team(
+            id=first_team.id,
+            name=first_team.name,
+            description=first_team.description,
+            organization_id=first_team.organization_id,
+        )
+
+    def get_projects_from_team(self, team_id):
+        # TODO : get Projects from Project id in database
+        pass
+
+    @staticmethod
+    def get_db_to_class(team: models.Team) -> schemas.Team:
+        return schemas.Team(
+            id=team.id,
+            name=team.name,
+            description=team.description,
+            organization_id=team.organization_id,
+        )
diff --git a/carbonserver/carbonserver/api/infra/repositories/repository_users.py b/carbonserver/carbonserver/api/infra/repositories/repository_users.py
new file mode 100644
index 000000000..dce05c564
--- /dev/null
+++ b/carbonserver/carbonserver/api/infra/repositories/repository_users.py
@@ -0,0 +1,106 @@
+import secrets
+from typing import List
+
+from sqlalchemy.orm import Session
+
+from carbonserver.api import schemas
+from carbonserver.api.domain.users import Users
+from carbonserver.database import models
+
+
+class SqlAlchemyRepository(Users):
+    def __init__(self, db: Session):
+        self.db = db
+
+    def create_user(self, user: schemas.UserCreate):
+        db_user = models.User(
+            user_id=user.name,
+            name=user.name,
+            email=user.email,
+            hashed_password=user.password,
+            api_key=_api_key_generator(),
+            is_active=True,
+        )
+
+        self.db.add(db_user)
+        self.db.commit()
+        self.db.refresh(db_user)
+        return db_user
+
+    def get_user_by_id(self, user_id):
+        """Find an user in database and return it
+
+        :user_id: The id of the user to retrieve.
+        :returns: An User in pyDantic BaseModel format.
+        :rtype: schemas.User
+        """
+        e = self.db.query(models.User).filter(models.User.id == user_id).first()
+        if e is None:
+            return None
+        else:
+            return self.get_db_to_class(e)
+
+    def list_users(self):
+        e = self.db.query(models.User)
+        if e is None:
+            return None
+        else:
+            users = []
+            for user in e:
+                users.append(self.get_db_to_class(user))
+            return users
+
+    @staticmethod
+    def get_db_to_class(user: models.User) -> schemas.User:
+        return schemas.User(
+            id=user.user_id,
+            name=user.name,
+            email=user.email,
+            hashed_password=user.hashed_password,
+            api_key=user.api_key,
+            is_active=user.is_active,
+        )
+
+
+class InMemoryRepository(Users):
+    def __init__(self):
+        self.users: List[Users] = []
+        self.id: int = 0
+        self.inactive_users: List = []
+
+    def create_user(self, user: schemas.UserCreate) -> models.User:
+        self.id += 1
+        self.users.append(
+            models.User(
+                user_id=self.id,
+                name=user.name,
+                email=user.email,
+                hashed_password=user.password,
+                api_key=_api_key_generator(),
+                is_active=True,
+            )
+        )
+        return self.users[self.id - 1]
+
+    def get_user_by_id(self, user_id: int):
+        user = [user for user in self.users if user.user_id == user_id][0]
+        return user
+
+    def list_users(self):
+        return self.users
+
+    @staticmethod
+    def get_db_to_class(user: models.User) -> schemas.User:
+        return schemas.User(
+            id=user.user_id,
+            name=user.name,
+            email=user.email,
+            hashed_password=user.hashed_password,
+            api_key=user.api_key,
+            is_active=user.is_active,
+        )
+
+
+def _api_key_generator():
+    generated_key = secrets.token_urlsafe(16)
+    return generated_key
diff --git a/carbonserver/carbonserver/api/routers/__init__.py b/carbonserver/carbonserver/api/routers/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/carbonserver/carbonserver/api/routers/emissions.py b/carbonserver/carbonserver/api/routers/emissions.py
new file mode 100644
index 000000000..e34c04fb2
--- /dev/null
+++ b/carbonserver/carbonserver/api/routers/emissions.py
@@ -0,0 +1,45 @@
+from fastapi import APIRouter, Depends, HTTPException, Path
+from sqlalchemy.orm import Session
+
+from carbonserver.api.dependencies import get_db, get_token_header
+from carbonserver.api.infra.repositories.repository_emissions import (
+    SqlAlchemyRepository,
+)
+from carbonserver.api.schemas import EmissionCreate
+
+router = APIRouter(
+    dependencies=[Depends(get_token_header)],
+)
+
+
+@router.put("/emission", tags=["emissions"], status_code=201)
+def add_emission(emission: EmissionCreate, db: Session = Depends(get_db)):
+    repository_emissions = SqlAlchemyRepository(db)
+    repository_emissions.add_emission(emission)
+
+
+@router.get("/emission/{emission_id}", tags=["emissions"])
+async def read_emission(
+    emission_id: str = Path(..., title="The ID of the emission to get"),
+    db: Session = Depends(get_db),
+):
+    repository_emissions = SqlAlchemyRepository(db)
+    emission = repository_emissions.get_one_emission(emission_id)
+    if emission is None:
+        raise HTTPException(status_code=404, detail="Emission not found")
+    return emission
+
+
+@router.get("/emissions/{run_id}", tags=["emissions"])
+async def read_experiment_emissions(
+    run_id: str = Path(..., title="The ID of the experiment to get"),
+    db: Session = Depends(get_db),
+):
+    repository_emissions = SqlAlchemyRepository(db)
+    experiment_emissions = repository_emissions.get_emissions_from_run(run_id)
+    if len(experiment_emissions) == 0:
+        raise HTTPException(
+            status_code=404,
+            detail=f"Emission not found for run_id {run_id}",
+        )
+    return experiment_emissions
diff --git a/carbonserver/carbonserver/api/routers/experiments.py b/carbonserver/carbonserver/api/routers/experiments.py
new file mode 100644
index 000000000..3e964be78
--- /dev/null
+++ b/carbonserver/carbonserver/api/routers/experiments.py
@@ -0,0 +1,51 @@
+from fastapi import APIRouter, Depends, HTTPException, Path
+from sqlalchemy.orm import Session
+
+from carbonserver.api.dependencies import get_db, get_token_header
+from carbonserver.api.infra.repositories.repository_experiments import (
+    SqlAlchemyRepository,
+)
+from carbonserver.api.schemas import ExperimentCreate
+
+router = APIRouter(
+    dependencies=[Depends(get_token_header)],
+)
+
+
+@router.put("/experiment", tags=["experiments"], status_code=201)
+def add_experiment(experiment: ExperimentCreate, db: Session = Depends(get_db)):
+    # crud_experiments.save_experiment(db, experiment)
+    repository_experiment = SqlAlchemyRepository(db)
+    repository_experiment.add_experiment(experiment)
+
+    # raise HTTPException(status_code=404, detail="Not Found")
+
+
+@router.get("/experiment/{experiment_id}", tags=["experiments"])
+async def read_experiment(
+    experiment_id: str = Path(..., title="The ID of the experiment to get"),
+    db: Session = Depends(get_db),
+):
+    repository_experiments = SqlAlchemyRepository(db)
+    experiment = repository_experiments.get_one_experiment(experiment_id)
+    if experiment is None:
+        raise HTTPException(status_code=404, detail="Experiment not found")
+    return experiment
+
+    # experiment = crud_experiments.get_one_experiment(experiment_id)
+    # if experiment_id is False:
+    #     raise HTTPException(status_code=404, detail="Item not found")
+    # return experiment
+    # raise HTTPException(status_code=501, detail="Not Implemented")
+
+
+@router.get("/experiments/{experiment_id}", tags=["experiments"])
+async def read_experiment_experiments(
+    experiment_id: str = Path(..., title="The ID of the experiment to get")
+):
+
+    # experiment_experiments = crud_experiments.get_experiments_from_experiment(
+    #     experiment_id
+    # )
+    # return experiment_experiments
+    raise HTTPException(status_code=501, detail="Not Implemented")
diff --git a/carbonserver/carbonserver/api/routers/organizations.py b/carbonserver/carbonserver/api/routers/organizations.py
new file mode 100644
index 000000000..5e988b5ed
--- /dev/null
+++ b/carbonserver/carbonserver/api/routers/organizations.py
@@ -0,0 +1,45 @@
+from fastapi import APIRouter, Depends, HTTPException, Path
+from sqlalchemy.orm import Session
+
+from carbonserver.api.dependencies import get_db, get_token_header
+from carbonserver.api.infra.repositories.repository_organizations import (
+    SqlAlchemyRepository,
+)
+from carbonserver.api.schemas import OrganizationCreate
+
+router = APIRouter(
+    dependencies=[Depends(get_token_header)],
+)
+
+
+organizations_temp_db = []
+
+
+@router.put("/organization", tags=["organizations"])
+def add_organization(organization: OrganizationCreate, db: Session = Depends(get_db)):
+    repository_organizations = SqlAlchemyRepository(db)
+    repository_organizations.add_organization(organization)
+    # TODO : return the id of the organization
+
+
+@router.get("/organization/{organization_id}", tags=["organizations"])
+async def read_organization(
+    organization_id: str = Path(..., title="The ID of the organization to get"),
+    db: Session = Depends(get_db),
+):
+    repository_organizations = SqlAlchemyRepository(db)
+    organization = repository_organizations.get_one_organization(organization_id)
+    if organization is None:
+        raise HTTPException(status_code=404, detail="Experiment not found")
+    return organization
+
+
+@router.get("/organizations/{organization_id}", tags=["organizations"])
+async def read_teams_organizations(
+    team_id: str = Path(..., title="The ID of the team to get")
+):
+    # team_organizations = crud_organizations.get_Team_from_Organizations(team_id)
+    # # Remove next line when DB work
+    # team_organizations = organizations_temp_db
+    # return team_organizations
+    raise HTTPException(status_code=501, detail="Not Implemented")
diff --git a/carbonserver/carbonserver/api/routers/projects.py b/carbonserver/carbonserver/api/routers/projects.py
new file mode 100644
index 000000000..429bc5f7f
--- /dev/null
+++ b/carbonserver/carbonserver/api/routers/projects.py
@@ -0,0 +1,42 @@
+from fastapi import APIRouter, Depends, HTTPException, Path
+from sqlalchemy.orm import Session
+
+from carbonserver.api.dependencies import get_db, get_token_header
+from carbonserver.api.infra.repositories.repository_projects import SqlAlchemyRepository
+from carbonserver.api.schemas import ProjectCreate
+
+router = APIRouter(
+    dependencies=[Depends(get_token_header)],
+)
+
+
+projects_temp_db = []
+
+
+@router.put("/project", tags=["projects"])
+def add_project(project: ProjectCreate, db: Session = Depends(get_db)):
+    repository_projects = SqlAlchemyRepository(db)
+    repository_projects.add_project(project)
+
+
+@router.get("/project/{project_id}", tags=["projects"])
+async def read_project(
+    project_id: str = Path(..., title="The ID of the project to get"),
+    db: Session = Depends(get_db),
+):
+    repository_projects = SqlAlchemyRepository(db)
+    project = repository_projects.get_one_project(project_id)
+    if project is None:
+        raise HTTPException(status_code=404, detail="Experiment not found")
+    return project
+
+
+@router.get("/projects/{project_id}", tags=["projects"])
+async def read_experiment_projects(
+    experiment_id: str = Path(..., title="The ID of the experiment to get")
+):
+    # experiment_projects = crud_projects.get_projects_from_experiment(experiment_id)
+    # # Remove next line when DB work
+    # experiment_projects = projects_temp_db
+    # return experiment_projects
+    raise HTTPException(status_code=501, detail="Not Implemented")
diff --git a/carbonserver/carbonserver/api/routers/runs.py b/carbonserver/carbonserver/api/routers/runs.py
new file mode 100644
index 000000000..79865a18f
--- /dev/null
+++ b/carbonserver/carbonserver/api/routers/runs.py
@@ -0,0 +1,17 @@
+from fastapi import APIRouter, Depends
+from sqlalchemy.orm import Session
+
+from carbonserver.api.dependencies import get_db, get_token_header
+from carbonserver.api.infra.repositories.repository_runs import SqlAlchemyRepository
+from carbonserver.api.schemas import RunCreate
+
+router = APIRouter(
+    dependencies=[Depends(get_token_header)],
+)
+
+
+@router.put("/run", tags=["runs"], status_code=201)
+def add_run(run: RunCreate, db: Session = Depends(get_db)):
+    repository_runs = SqlAlchemyRepository(db)
+    repository_runs.add_run(run)
+    # TODO : Return Error when experiment does not exist
diff --git a/carbonserver/carbonserver/api/routers/teams.py b/carbonserver/carbonserver/api/routers/teams.py
new file mode 100644
index 000000000..2ee745388
--- /dev/null
+++ b/carbonserver/carbonserver/api/routers/teams.py
@@ -0,0 +1,42 @@
+from fastapi import APIRouter, Depends, HTTPException, Path
+from sqlalchemy.orm import Session
+
+from carbonserver.api.dependencies import get_db, get_token_header
+from carbonserver.api.infra.repositories.repository_teams import SqlAlchemyRepository
+from carbonserver.api.schemas import TeamCreate
+
+router = APIRouter(
+    dependencies=[Depends(get_token_header)],
+)
+
+
+teams_temp_db = []
+
+
+@router.put("/team", tags=["teams"])
+def add_team(team: TeamCreate, db: Session = Depends(get_db)):
+    repository_teams = SqlAlchemyRepository(db)
+    repository_teams.add_team(team)
+
+
+@router.get("/team/{team_id}", tags=["teams"])
+async def read_team(
+    team_id: str = Path(..., title="The ID of the team to get"),
+    db: Session = Depends(get_db),
+):
+    repository_teams = SqlAlchemyRepository(db)
+    team = repository_teams.get_one_team(team_id)
+    if team is None:
+        raise HTTPException(status_code=404, detail="Experiment not found")
+    return team
+
+
+@router.get("/teams/{team_id}", tags=["teams"])
+async def read_project_teams(
+    project_id: str = Path(..., title="The ID of the project to get")
+):
+    # project_teams = crud_teams.get_Project_from_Teams(team_id)
+    # Remove next line when DB work
+    # project_teams = teams_temp_db
+    # return project_teams
+    raise HTTPException(status_code=501, detail="Not Implemented")
diff --git a/carbonserver/carbonserver/api/routers/users.py b/carbonserver/carbonserver/api/routers/users.py
new file mode 100644
index 000000000..3b1c5fddd
--- /dev/null
+++ b/carbonserver/carbonserver/api/routers/users.py
@@ -0,0 +1,31 @@
+from fastapi import APIRouter, Depends
+from requests import Session
+
+from carbonserver.api.dependencies import get_db, get_token_header
+from carbonserver.api.infra.repositories.repository_users import SqlAlchemyRepository
+from carbonserver.api.schemas import UserCreate
+
+router = APIRouter(
+    dependencies=[Depends(get_token_header)],
+)
+
+
+@router.put("/users/", tags=["users"], status_code=201)
+def create_user(user: UserCreate, db: Session = Depends(get_db)):
+    repository_users = SqlAlchemyRepository(db)
+    created_user = repository_users.create_user(user)
+    return created_user
+
+
+@router.get("/users/", tags=["users"], status_code=200)
+def list_users(db: Session = Depends(get_db)):
+    repository_users = SqlAlchemyRepository(db)
+    users = repository_users.list_users()
+    return users
+
+
+@router.get("/user/", tags=["users"], status_code=200)
+def get_user_by_id(user_id: int, db: Session = Depends(get_db)):
+    repository_users = SqlAlchemyRepository(db)
+    users = repository_users.get_user_by_id(user_id)
+    return users
diff --git a/carbonserver/carbonserver/api/schemas.py b/carbonserver/carbonserver/api/schemas.py
new file mode 100644
index 000000000..5aa557642
--- /dev/null
+++ b/carbonserver/carbonserver/api/schemas.py
@@ -0,0 +1,207 @@
+"""
+https://fastapi.tiangolo.com/tutorial/sql-databases/
+
+To avoid confusion between the SQLAlchemy models and the Pydantic models, we will have the file models.py with the SQLAlchemy models, and the file schemas.py with the Pydantic models.
+
+These Pydantic models define more or less a "schema" (a valid data shape).
+
+So this will help us avoiding confusion while using both.
+"""
+
+from datetime import datetime
+from typing import List, Optional
+
+from pydantic import BaseModel, EmailStr, Field, SecretStr
+
+
+class EmissionBase(BaseModel):
+    timestamp: datetime
+    run_id: str
+    duration: int = Field(
+        ..., gt=0, description="The duration must be greater than zero"
+    )
+    emissions: float = Field(
+        ..., gt=0, description="The emissions must be greater than zero"
+    )
+    energy_consumed: float = Field(
+        ..., gt=0, description="The energy_consumed must be greater than zero"
+    )
+
+    class Config:
+        schema_extra = {
+            "example": {
+                "timestamp": "2021-04-04T08:43:00+02:00",
+                "run_id": "40088f1a-d28e-4980-8d80-bf5600056a14",
+                "duration": 98745,
+                "emissions": 1.548444,
+                "energy_consumed": 57.21874,
+            }
+        }
+
+
+class EmissionCreate(EmissionBase):
+    pass
+
+
+class Emission(EmissionBase):
+    id: str
+
+
+# Run
+class RunBase(BaseModel):
+    timestamp: datetime
+    experiment_id: str
+
+    class Config:
+        schema_extra = {
+            "example": {
+                "timestamp": "2021-04-04T08:43:00+02:00",
+                "experiment_id": "1",
+            }
+        }
+
+
+class RunCreate(RunBase):
+    pass
+
+
+class Run(RunBase):
+    id: str
+
+
+# Experiment
+class ExperimentBase(BaseModel):
+    timestamp: datetime
+    name: str
+    description: str
+    # is_active: bool
+    country_name: Optional[str] = None
+    country_iso_code: Optional[str] = None
+    region: Optional[str] = None
+    on_cloud: bool
+    cloud_provider: Optional[str] = None
+    cloud_region: Optional[str] = None
+    project_id: str
+
+    class Config:
+        # orm_mode = True
+        schema_extra = {
+            "example": {
+                "name": "Run on AWS",
+                "description": "AWS API for Code Carbon",
+                # "is_active": True,
+                "timestamp": "2021-04-04T08:43:00+02:00",
+                "country_name": "France",
+                "country_iso_code": "FRA",
+                "region": "france",
+                "on_cloud": True,
+                "cloud_provider": "aws",
+                "cloud_region": "eu-west-1a",
+                "project_id": "1",
+            }
+        }
+
+
+class ExperimentCreate(ExperimentBase):
+    pass
+
+
+class Experiment(ExperimentBase):
+    id: str
+    emissions: List[Emission] = []
+
+
+# Project
+class ProjectBase(BaseModel):
+    name: str
+    description: str
+    team_id: str
+
+    class Config:
+        schema_extra = {
+            "example": {
+                "name": "API Code Carbon",
+                "description": "API for Code Carbon",
+                "team_id": "1",
+            }
+        }
+
+
+class ProjectCreate(ProjectBase):
+    pass
+
+
+class Project(ProjectBase):
+    id: str
+    experiments: List[Experiment] = []
+
+
+# Team
+class TeamBase(BaseModel):
+    name: str
+    description: str
+    organization_id: str
+
+    class Config:
+        schema_extra = {
+            "example": {
+                "name": "Data For Good",
+                "description": "Data For Good France",
+                "organization_id": "1",
+            }
+        }
+
+
+class TeamCreate(TeamBase):
+    pass
+
+
+class Team(TeamBase):
+    id: str
+    projects: List[Project] = []
+
+
+# Organization
+class OrganizationBase(BaseModel):
+    name: str
+    description: str
+
+    class Config:
+        schema_extra = {
+            "example": {
+                "name": "Code Carbon",
+                "description": "Save the world, one run at a time.",
+            }
+        }
+
+
+class OrganizationCreate(OrganizationBase):
+    pass
+
+
+class Organization(OrganizationBase):
+    id: str
+    teams: List[Team] = []
+
+
+# User
+class UserBase(BaseModel):
+    email: str
+
+
+class UserCreate(UserBase):
+    name: str
+    email: EmailStr
+    password: SecretStr
+
+
+class User(UserBase):
+    id: str
+    name: str
+    email: EmailStr
+    password: SecretStr
+    api_key: str
+    is_active: Optional[bool]
+
+    class Config:
+        orm_mode = True
diff --git a/carbonserver/carbonserver/config.py b/carbonserver/carbonserver/config.py
new file mode 100644
index 000000000..95e2b718b
--- /dev/null
+++ b/carbonserver/carbonserver/config.py
@@ -0,0 +1,11 @@
+from pydantic import BaseSettings, Field
+
+
+class Settings(BaseSettings):
+    db_url: str = Field(
+        "postgresql://codecarbon-user:supersecret@localhost:5432/codecarbon_db",
+        env="DATABASE_URL",
+    )
+
+
+settings = Settings()
diff --git a/carbonserver/carbonserver/database/__init__.py b/carbonserver/carbonserver/database/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/carbonserver/carbonserver/database/alembic.ini b/carbonserver/carbonserver/database/alembic.ini
new file mode 100644
index 000000000..3ad210d27
--- /dev/null
+++ b/carbonserver/carbonserver/database/alembic.ini
@@ -0,0 +1,89 @@
+# A generic, single database configuration.
+
+[alembic]
+# path to migration scripts
+script_location = alembic
+
+# template used to generate migration files
+# file_template = %%(rev)s_%%(slug)s
+
+# sys.path path, will be prepended to sys.path if present.
+# defaults to the current working directory.
+prepend_sys_path = .
+
+# timezone to use when rendering the date
+# within the migration file as well as the filename.
+# string value is passed to dateutil.tz.gettz()
+# leave blank for localtime
+# timezone =
+
+# max length of characters to apply to the
+# "slug" field
+# truncate_slug_length = 40
+
+# set to 'true' to run the environment during
+# the 'revision' command, regardless of autogenerate
+# revision_environment = false
+
+# set to 'true' to allow .pyc and .pyo files without
+# a source .py file to be detected as revisions in the
+# versions/ directory
+# sourceless = false
+
+# version location specification; this defaults
+# to alembic/versions.  When using multiple version
+# directories, initial revisions must be specified with --version-path
+# version_locations = %(here)s/bar %(here)s/bat alembic/versions
+
+# the output encoding used when revision files
+# are written from script.py.mako
+# output_encoding = utf-8
+
+sqlalchemy.url = postgresql://codecarbon-user:supersecret@postgres:5432/codecarbon_db
+
+
+[post_write_hooks]
+# post_write_hooks defines scripts or Python functions that are run
+# on newly generated revision scripts.  See the documentation for further
+# detail and examples
+
+# format using "black" - use the console_scripts runner, against the "black" entrypoint
+# hooks = black
+# black.type = console_scripts
+# black.entrypoint = black
+# black.options = -l 79 REVISION_SCRIPT_FILENAME
+
+# Logging configuration
+[loggers]
+keys = root,sqlalchemy,alembic
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = WARN
+handlers = console
+qualname =
+
+[logger_sqlalchemy]
+level = WARN
+handlers =
+qualname = sqlalchemy.engine
+
+[logger_alembic]
+level = INFO
+handlers =
+qualname = alembic
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(levelname)-5.5s [%(name)s] %(message)s
+datefmt = %H:%M:%S
diff --git a/carbonserver/carbonserver/database/alembic/README.md b/carbonserver/carbonserver/database/alembic/README.md
new file mode 100644
index 000000000..a9f8b0ba1
--- /dev/null
+++ b/carbonserver/carbonserver/database/alembic/README.md
@@ -0,0 +1,49 @@
+# How to manage the API database
+
+We use Alembic with SQL Alchemy for database (DB) management.
+
+https://www.kimsereylam.com/sqlalchemy/2019/10/18/get-started-with-alembic.html
+
+https://alembic.sqlalchemy.org/en/latest/tutorial.html#the-migration-environment
+
+
+## Configure the database
+
+Put the url of the database in rhe config file:
+
+For SQLite:
+```
+sqlalchemy.url = sqlite:///./code_carbon.db
+```
+
+For Postgres:
+```
+sqlalchemy.url = postgresql://codecarbon-user:supersecret@localhost:5432/codecarbon_db
+```
+
+# Init the DB
+```
+alembic upgrade head
+```
+
+# Get infos
+```
+alembic current
+```
+
+```
+alembic history --verbose
+```
+
+Test :
+```
+cd codecarbon/carbonserver/carbonserver/database
+alembic upgrade head --sql
+```
+
+# Rollback
+```
+alembic downgrade base
+```
+
+
diff --git a/carbonserver/carbonserver/database/alembic/env.py b/carbonserver/carbonserver/database/alembic/env.py
new file mode 100644
index 000000000..72d73f1cc
--- /dev/null
+++ b/carbonserver/carbonserver/database/alembic/env.py
@@ -0,0 +1,73 @@
+from logging.config import fileConfig
+
+from alembic import context
+from sqlalchemy import engine_from_config, pool
+
+# this is the Alembic Config object, which provides
+# access to the values within the .ini file in use.
+config = context.config
+
+# Interpret the config file for Python logging.
+# This line sets up loggers basically.
+fileConfig(config.config_file_name)
+
+# add your model's MetaData object here
+# for 'autogenerate' support
+# from myapp import mymodel
+# target_metadata = mymodel.Base.metadata
+target_metadata = None
+
+# other values from the config, defined by the needs of env.py,
+# can be acquired:
+# my_important_option = config.get_main_option("my_important_option")
+# ... etc.
+
+
+def run_migrations_offline():
+    """Run migrations in 'offline' mode.
+
+    This configures the context with just a URL
+    and not an Engine, though an Engine is acceptable
+    here as well.  By skipping the Engine creation
+    we don't even need a DBAPI to be available.
+
+    Calls to context.execute() here emit the given string to the
+    script output.
+
+    """
+    url = config.get_main_option("sqlalchemy.url")
+    context.configure(
+        url=url,
+        target_metadata=target_metadata,
+        literal_binds=True,
+        dialect_opts={"paramstyle": "named"},
+    )
+
+    with context.begin_transaction():
+        context.run_migrations()
+
+
+def run_migrations_online():
+    """Run migrations in 'online' mode.
+
+    In this scenario we need to create an Engine
+    and associate a connection with the context.
+
+    """
+    connectable = engine_from_config(
+        config.get_section(config.config_ini_section),
+        prefix="sqlalchemy.",
+        poolclass=pool.NullPool,
+    )
+
+    with connectable.connect() as connection:
+        context.configure(connection=connection, target_metadata=target_metadata)
+
+        with context.begin_transaction():
+            context.run_migrations()
+
+
+if context.is_offline_mode():
+    run_migrations_offline()
+else:
+    run_migrations_online()
diff --git a/carbonserver/carbonserver/database/alembic/script.py.mako b/carbonserver/carbonserver/database/alembic/script.py.mako
new file mode 100644
index 000000000..2c0156303
--- /dev/null
+++ b/carbonserver/carbonserver/database/alembic/script.py.mako
@@ -0,0 +1,24 @@
+"""${message}
+
+Revision ID: ${up_revision}
+Revises: ${down_revision | comma,n}
+Create Date: ${create_date}
+
+"""
+from alembic import op
+import sqlalchemy as sa
+${imports if imports else ""}
+
+# revision identifiers, used by Alembic.
+revision = ${repr(up_revision)}
+down_revision = ${repr(down_revision)}
+branch_labels = ${repr(branch_labels)}
+depends_on = ${repr(depends_on)}
+
+
+def upgrade():
+    ${upgrades if upgrades else "pass"}
+
+
+def downgrade():
+    ${downgrades if downgrades else "pass"}
diff --git a/carbonserver/carbonserver/database/alembic/versions/5abae4eb2079_create_tables.py b/carbonserver/carbonserver/database/alembic/versions/5abae4eb2079_create_tables.py
new file mode 100644
index 000000000..3ca10db2a
--- /dev/null
+++ b/carbonserver/carbonserver/database/alembic/versions/5abae4eb2079_create_tables.py
@@ -0,0 +1,139 @@
+"""create_tables
+
+Revision ID: 5abae4eb2079
+Revises: Tables creation
+Create Date: 2021-05-18 22:21:49.659708
+
+"""
+import uuid
+
+import sqlalchemy as sa
+from alembic import op
+from sqlalchemy.dialects.postgresql import UUID
+
+revision = "5abae4eb2079"
+down_revision = None
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    """
+    Create all tables
+    """
+    op.create_table(
+        "emissions",
+        sa.Column(
+            "id", UUID(as_uuid=True), primary_key=True, index=True, default=uuid.uuid4
+        ),
+        sa.Column("timestamp", sa.DateTime),
+        sa.Column("duration", sa.Float),
+        sa.Column("emissions", sa.Float),
+        sa.Column("energy_consumed", sa.Float),
+        sa.Column("run_id", UUID),
+    )
+
+    op.create_table(
+        "runs",
+        sa.Column(
+            "id", UUID(as_uuid=True), primary_key=True, index=True, default=uuid.uuid4
+        ),
+        sa.Column("timestamp", sa.DateTime),
+        sa.Column("experiment_id", UUID),
+    )
+
+    op.create_foreign_key("fk_emissions_runs", "emissions", "runs", ["run_id"], ["id"])
+
+    op.create_table(
+        "experiments",
+        sa.Column(
+            "id", UUID(as_uuid=True), primary_key=True, index=True, default=uuid.uuid4
+        ),
+        sa.Column("timestamp", sa.DateTime),
+        sa.Column("name", sa.String),
+        sa.Column("description", sa.String),
+        sa.Column("country_name", sa.String),
+        sa.Column("country_iso_code", sa.String),
+        sa.Column("region", sa.String),
+        sa.Column("on_cloud", sa.Boolean, default=False),
+        sa.Column("cloud_provider", sa.String),
+        sa.Column("cloud_region", sa.String),
+        sa.Column("project_id", UUID),
+    )
+
+    op.create_foreign_key(
+        "fk_runs_experiments", "runs", "experiments", ["experiment_id"], ["id"]
+    )
+
+    op.create_table(
+        "projects",
+        sa.Column(
+            "id", UUID(as_uuid=True), primary_key=True, index=True, default=uuid.uuid4
+        ),
+        sa.Column("name", sa.String),
+        sa.Column("description", sa.String),
+        sa.Column("team_id", UUID),
+    )
+
+    op.create_foreign_key(
+        "fk_experiments_projects", "experiments", "projects", ["project_id"], ["id"]
+    )
+
+    op.create_table(
+        "teams",
+        sa.Column(
+            "id", UUID(as_uuid=True), primary_key=True, index=True, default=uuid.uuid4
+        ),
+        sa.Column("name", sa.String),
+        sa.Column("description", sa.String),
+        sa.Column("organization_id", UUID),
+    )
+
+    op.create_foreign_key("fk_projects_teams", "projects", "teams", ["team_id"], ["id"])
+
+    op.create_table(
+        "organizations",
+        sa.Column(
+            "id", UUID(as_uuid=True), primary_key=True, index=True, default=uuid.uuid4
+        ),
+        sa.Column("name", sa.String),
+        sa.Column("description", sa.String),
+    )
+
+    op.create_foreign_key(
+        "fk_teams_organizations", "teams", "organizations", ["organization_id"], ["id"]
+    )
+
+    op.create_table(
+        "users",
+        sa.Column(
+            "id", UUID(as_uuid=True), primary_key=True, index=True, default=uuid.uuid4
+        ),
+        sa.Column("name", sa.String),
+        sa.Column("api_key", sa.String),
+        sa.Column("email", sa.String, unique=True, index=True),
+        sa.Column("hashed_password", sa.String),
+        sa.Column("is_active", sa.Boolean, default=True),
+        sa.Column("organization_id", UUID),
+    )
+
+    op.create_foreign_key(
+        "fk_users_organizations", "users", "organizations", ["organization_id"], ["id"]
+    )
+
+
+def downgrade():
+    """
+    Remove all tables
+    """
+    tables = [
+        "emissions",
+        "runs",
+        "experiments",
+        "projects",
+        "teams",
+        "organizations",
+        "users",
+    ]
+    for t in tables:
+        op.drop_table(t)
diff --git a/carbonserver/carbonserver/database/database.py b/carbonserver/carbonserver/database/database.py
new file mode 100644
index 000000000..a12625cc1
--- /dev/null
+++ b/carbonserver/carbonserver/database/database.py
@@ -0,0 +1,15 @@
+from sqlalchemy import create_engine
+from sqlalchemy.ext.declarative import declarative_base
+from sqlalchemy.orm import sessionmaker
+
+from carbonserver.config import settings
+
+engine_kwargs = {}
+if "sqlite" in settings.db_url:
+    engine_kwargs["connect_args"] = {"check_same_thread": False}
+engine = create_engine(settings.db_url, **engine_kwargs)
+SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
+
+# We will use the function declarative_base() that returns a class.
+# Later we will inherit from this class to create each of the database models or classes (the ORM models)
+Base = declarative_base()
diff --git a/carbonserver/carbonserver/database/models.py b/carbonserver/carbonserver/database/models.py
new file mode 100644
index 000000000..f9a86ed3e
--- /dev/null
+++ b/carbonserver/carbonserver/database/models.py
@@ -0,0 +1,86 @@
+import uuid
+
+from sqlalchemy import Boolean, Column, DateTime, Float, ForeignKey, Integer, String
+from sqlalchemy.dialects.postgresql import UUID
+from sqlalchemy.orm import relationship
+
+from carbonserver.database.database import Base
+
+
+class Emission(Base):
+    __tablename__ = "emissions"
+    id = Column(UUID(as_uuid=True), primary_key=True, index=True, default=uuid.uuid4)
+    timestamp = Column(DateTime)
+    duration = Column(Float)
+    emissions = Column(Float)
+    energy_consumed = Column(Float)
+    run_id = Column(String, ForeignKey("runs.id"))
+    run = relationship("Run", back_populates="emissions")
+
+
+class Run(Base):
+    __tablename__ = "runs"
+    id = Column(UUID(as_uuid=True), primary_key=True, index=True, default=uuid.uuid4)
+    timestamp = Column(DateTime)
+    experiment_id = Column(Integer, ForeignKey("experiments.id"))
+    experiment = relationship("Experiment", back_populates="runs")
+    emissions = relationship("Emission", back_populates="run")
+
+
+class Experiment(Base):
+    __tablename__ = "experiments"
+    id = Column(UUID(as_uuid=True), primary_key=True, index=True, default=uuid.uuid4)
+    timestamp = Column(DateTime)
+    name = Column(String)
+    description = Column(String)
+    is_active = Column(Boolean, default=True)
+    country_name = Column(String)
+    country_iso_code = Column(String)
+    region = Column(String)
+    on_cloud = Column(Boolean, default=False)
+    cloud_provider = Column(String)
+    cloud_region = Column(String)
+    project_id = Column(Integer, ForeignKey("projects.id"))
+    project = relationship("Project", back_populates="experiments")
+    runs = relationship("Run", back_populates="experiment")
+
+
+class Project(Base):
+    __tablename__ = "projects"
+    id = Column(UUID(as_uuid=True), primary_key=True, index=True, default=uuid.uuid4)
+    name = Column(String)
+    description = Column(String)
+    team_id = Column(Integer, ForeignKey("teams.id"))
+    experiments = relationship("Experiment", back_populates="project")
+    team = relationship("Team", back_populates="projects")
+
+
+class Team(Base):
+    __tablename__ = "teams"
+    id = Column(UUID(as_uuid=True), primary_key=True, index=True, default=uuid.uuid4)
+    name = Column(String)
+    description = Column(String)
+    organization_id = Column(Integer, ForeignKey("organizations.id"))
+    projects = relationship("Project", back_populates="team")
+    organization = relationship("Organization", back_populates="teams")
+
+
+class Organization(Base):
+    __tablename__ = "organizations"
+    id = Column(UUID(as_uuid=True), primary_key=True, index=True, default=uuid.uuid4)
+    name = Column(String)
+    description = Column(String)
+    teams = relationship("Team", back_populates="organization")
+
+
+class User(Base):
+    __tablename__ = "users"
+    user_id = Column(
+        UUID(as_uuid=True), primary_key=True, index=True, default=uuid.uuid4
+    )
+    name = Column(String)
+    email = Column(String, unique=True, index=True)
+    hashed_password = Column(String)
+    api_key = Column(String)
+    is_active = Column(Boolean, default=True)
+    # TODO: Associate user with his entities
diff --git a/carbonserver/docker/.dockerignore b/carbonserver/docker/.dockerignore
new file mode 100644
index 000000000..1fbd580d6
--- /dev/null
+++ b/carbonserver/docker/.dockerignore
@@ -0,0 +1,7 @@
+.*
+!.coveragerc
+!.env
+!.pylintrc
+venv
+env
+/carbonserver/.tox
\ No newline at end of file
diff --git a/carbonserver/docker/Dockerfile b/carbonserver/docker/Dockerfile
new file mode 100644
index 000000000..89bd5cf48
--- /dev/null
+++ b/carbonserver/docker/Dockerfile
@@ -0,0 +1,29 @@
+# Dockerfile
+
+# Use Ubuntu to install 3 versions of Python for testing
+# For production, you could use python:3.8-slim
+FROM ubuntu:20.04
+
+# set work directory
+WORKDIR /carbonserver
+
+# set env variables
+ENV PYTHONDONTWRITEBYTECODE 1
+ENV PYTHONUNBUFFERED 1
+
+# install dependencies
+RUN apt-get update && apt-get upgrade -y && \
+ apt-get install -y software-properties-common && \
+ add-apt-repository ppa:deadsnakes/ppa -y && \
+ apt-get update && \
+ apt-get install -y gcc libpq-dev python3.7 python3.6 python3-pip
+
+COPY requirements-dev.txt .
+RUN pip install -r requirements-dev.txt
+
+COPY docker/entrypoint.sh /opt
+RUN chmod a+x /opt/entrypoint.sh
+# copy project
+COPY . .
+EXPOSE 8000
+ENTRYPOINT ["/opt/entrypoint.sh"]
diff --git a/carbonserver/docker/README.md b/carbonserver/docker/README.md
new file mode 100644
index 000000000..fb5e577cc
--- /dev/null
+++ b/carbonserver/docker/README.md
@@ -0,0 +1,13 @@
+# Run Code Carbon API with Docker
+
+This is to start using `docker-compose` to run the carbonserver app.
+
+* Run the carbonserver app with `docker-compose`
+* Switch between sqlite or postgres (comment/uncomment in `docker-compose.yml` the database)
+```yaml
+    environment:
+      # DATABASE_URL: sqlite:///./code_carbon.db
+      DATABASE_URL: postgresql://${DATABASE_USER:-codecarbon-user}:${DATABASE_PASS:-supersecret}@postgres/${DATABASE_NAME:-codecarbon_db}
+```
+
+> Access the app on port `8008` instead of default `8000`, see `ports` in `docker-compose.yml`.
diff --git a/carbonserver/docker/entrypoint.sh b/carbonserver/docker/entrypoint.sh
new file mode 100644
index 000000000..b041023ea
--- /dev/null
+++ b/carbonserver/docker/entrypoint.sh
@@ -0,0 +1,14 @@
+#!/bin/bash
+echo "Waiting for database to start..."
+sleep 3
+echo "Preparing database..."
+cd /carbonserver/carbonserver/database
+python3 -m alembic upgrade head
+if [ $? -eq 0 ]; then
+    echo "Database ready"
+else
+    echo "---------------- ERROR initializing database --------------------------"
+    exit 1
+fi
+cd /carbonserver
+uvicorn --reload main:app --host 0.0.0.0
diff --git a/carbonserver/docker/pgadmin-servers.json b/carbonserver/docker/pgadmin-servers.json
new file mode 100644
index 000000000..1129b13cd
--- /dev/null
+++ b/carbonserver/docker/pgadmin-servers.json
@@ -0,0 +1,21 @@
+{
+    "Servers": {
+        "1": {
+            "Name": "CodeCarbon Docker",
+            "Group": "Servers",
+            "Host": "postgres_codecarbon",
+            "Port": 5432,
+            "MaintenanceDB": "postgres",
+            "Username": "codecarbon-user",
+            "PassFile": "/pgadmin4/pgpassfile",
+            "SSLMode": "prefer",
+            "SSLCert": "<STORAGE_DIR>/.postgresql/postgresql.crt",
+            "SSLKey": "<STORAGE_DIR>/.postgresql/postgresql.key",
+            "SSLCompression": 0,
+            "Timeout": 10,
+            "UseSSHTunnel": 0,
+            "TunnelPort": "22",
+            "TunnelAuthentication": 0
+        }
+    }
+}
\ No newline at end of file
diff --git a/carbonserver/docker/pgpassfile b/carbonserver/docker/pgpassfile
new file mode 100644
index 000000000..ec77d7bb8
--- /dev/null
+++ b/carbonserver/docker/pgpassfile
@@ -0,0 +1,1 @@
+postgres_codecarbon:5432:postgres:codecarbon-user:supersecret
\ No newline at end of file
diff --git a/carbonserver/docker/requirements-dev.txt b/carbonserver/docker/requirements-dev.txt
new file mode 100644
index 000000000..23cd7b828
--- /dev/null
+++ b/carbonserver/docker/requirements-dev.txt
@@ -0,0 +1,8 @@
+alembic
+fastapi
+pydantic[email]
+psycopg2
+requests
+sqlalchemy
+tox
+uvicorn[standard]
diff --git a/carbonserver/main.py b/carbonserver/main.py
new file mode 100644
index 000000000..b32975009
--- /dev/null
+++ b/carbonserver/main.py
@@ -0,0 +1,37 @@
+from fastapi import Depends, FastAPI
+
+from carbonserver.api.dependencies import get_query_token
+from carbonserver.api.routers import (
+    emissions,
+    experiments,
+    organizations,
+    projects,
+    runs,
+    teams,
+    users,
+)
+from carbonserver.database import models
+from carbonserver.database.database import engine
+
+models.Base.metadata.create_all(bind=engine)
+
+app = FastAPI(dependencies=[Depends(get_query_token)])
+
+
+app.include_router(emissions.router)
+app.include_router(runs.router)
+app.include_router(experiments.router)
+app.include_router(projects.router)
+app.include_router(teams.router)
+app.include_router(organizations.router)
+app.include_router(users.router)
+
+
+@app.get("/")
+def default():
+    return {"docs": "Please go to /docs"}
+
+
+@app.get("/status")
+def status():
+    return {"status": "OK"}
diff --git a/carbonserver/requirements-dev.txt b/carbonserver/requirements-dev.txt
new file mode 100644
index 000000000..249d80b9b
--- /dev/null
+++ b/carbonserver/requirements-dev.txt
@@ -0,0 +1,8 @@
+alembic
+fastapi
+pydantic[email]
+psycopg2-binary
+requests
+sqlalchemy
+tox
+uvicorn[standard]
diff --git a/carbonserver/setup.py b/carbonserver/setup.py
new file mode 100644
index 000000000..724d027ab
--- /dev/null
+++ b/carbonserver/setup.py
@@ -0,0 +1,41 @@
+import setuptools
+
+with open("README.md", "r") as f:
+    long_description = f.read()
+
+DEPENDENCIES = [
+    "uvicorn[standard] >= 0.13.0, < 0.14.0",
+    "fastapi >= 0.65.0, < 0.66.0",
+    "sqlalchemy",
+    "psycopg2-binary",
+    "pydantic[email]",
+]
+
+TEST_DEPENDENCIES = [
+    "mock",
+    "pytest",
+    "requests",
+    "responses",
+    "tox",
+    "alembic",
+]
+
+
+setuptools.setup(
+    name="carbonserver",
+    version="0.1.0",
+    author="BCG GAMMA, Comet.ml, Haverford College, MILA, DataForGood France",
+    long_description=long_description,
+    long_description_content_type="text/markdown",
+    packages=setuptools.find_packages(
+        exclude=["*.tests", "*.tests.*", "tests.*", "tests"]
+    ),
+    install_requires=DEPENDENCIES,
+    tests_require=TEST_DEPENDENCIES,
+    classifiers=[
+        "Natural Language :: English",
+        "Programming Language :: Python :: 3.7",
+    ],
+    python_requires=">=3.7",
+    entry_points={"console_scripts": ["carbonserver = carbonserver:main"]},
+)
diff --git a/carbonserver/tox.ini b/carbonserver/tox.ini
new file mode 100644
index 000000000..c6535b129
--- /dev/null
+++ b/carbonserver/tox.ini
@@ -0,0 +1,29 @@
+[tox]
+envlist = py37, py38
+
+[testenv:unit]
+deps =
+    pytest
+    -rrequirements-test.txt
+
+commands =
+    pip install -e .
+    python -m pytest -vv tests/infra
+
+
+recreate =
+    false
+
+
+[testenv:integration]
+deps =
+    pytest
+    -rrequirements-test.txt
+
+commands =
+    pip install -e .
+    python -m pytest -vv tests/routers
+
+
+recreate =
+    false
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 000000000..5b4479599
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,73 @@
+version: '3.7'
+services:
+
+  # codecarbon_package:
+  #   build:
+  #     context: ./codecarbon/
+  #     dockerfile: ./docker/Dockerfile
+  #   networks:
+  #     - codecarbon_net
+  #   volumes:
+  #     - ./codecarbon:/opt/codecarbon
+  #   depends_on:
+  #     - codecarbon_api
+  #   restart: unless-stopped
+
+  carbonserver:
+    build:
+      context: ./carbonserver/
+      dockerfile: ./docker/Dockerfile
+    #command: cd /carbonserver/carbonserver/database && python3 -m alembic upgrade head && cd ../.. && uvicorn --reload main:app --host 0.0.0.0
+    volumes:
+      - ./carbonserver:/carbonserver
+      # Avoid problems with local tox
+      - /carbonserver/.tox
+    ports:
+      - 8008:8000
+    environment:
+      # DATABASE_URL: sqlite:///./code_carbon.db
+      DATABASE_URL: postgresql://${DATABASE_USER:-codecarbon-user}:${DATABASE_PASS:-supersecret}@postgres/${DATABASE_NAME:-codecarbon_db}
+    networks:
+      - codecarbon_net
+
+  postgres:
+    container_name: ${DATABASE_HOST:-postgres_codecarbon}
+    environment:
+      HOSTNAME: ${DATABASE_HOST:-postgres_codecarbon}
+      POSTGRES_DB: ${DATABASE_NAME:-codecarbon_db}
+      POSTGRES_PASSWORD: ${DATABASE_PASS:-supersecret}
+      POSTGRES_USER: ${DATABASE_USER:-codecarbon-user}
+    image: postgres:12
+    networks:
+      - codecarbon_net
+    ports:
+    - 5480:5432
+    restart: unless-stopped
+    volumes:
+    - postgres_data:/var/lib/postgresql/data:rw
+
+  pgadmin:
+    container_name: pgadmin_codecarbon
+    image: dpage/pgadmin4
+    environment:
+      PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL:-test@test.com}
+      PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD:-test}
+    volumes:
+       - pgadmin:/root/.pgadmin
+       - ./carbonserver/docker/pgpassfile:/pgadmin4/pgpassfile
+       - ./carbonserver/docker/pgadmin-servers.json:/pgadmin4/servers.json
+    ports:
+      - "${PGADMIN_PORT:-5080}:80"
+    networks:
+      - codecarbon_net
+    restart: unless-stopped
+
+volumes:
+  postgres_data:
+    name: postgres_codecarbon_data
+  pgadmin:
+    name: pgadmin_codecarbon_data
+
+networks:
+  codecarbon_net:
+    name: codecarbon_network
diff --git a/docker/.dockerignore b/docker/.dockerignore
new file mode 100644
index 000000000..98358b577
--- /dev/null
+++ b/docker/.dockerignore
@@ -0,0 +1,6 @@
+.*
+!.coveragerc
+!.env
+!.pylintrc
+venv
+env
\ No newline at end of file
diff --git a/docker/Dockerfile b/docker/Dockerfile
new file mode 100644
index 000000000..365bdcecb
--- /dev/null
+++ b/docker/Dockerfile
@@ -0,0 +1,10 @@
+FROM python:3.8
+WORKDIR /opt
+
+WORKDIR /opt/codecarbon
+# Tips : relative path is from root project folder as we use context in docker-compose
+#COPY ./docker/.env.docker .env
+COPY setup.py .
+RUN python3 setup.py install
+COPY . .
+# TODO : install codecarbon package ?
\ No newline at end of file
diff --git a/docker/README.md b/docker/README.md
new file mode 100644
index 000000000..06fd3d740
--- /dev/null
+++ b/docker/README.md
@@ -0,0 +1,58 @@
+# Usage of CodeCarbon with Docker
+
+## Pre-requisis
+
+Clone the project
+```sh
+git clone https://github.com/mlco2/codecarbon/codecarbon.git
+```
+
+Prepare configuration :
+```sh
+cd codecarbon
+cp docker/docker.env .env
+```
+
+
+## Run
+
+```sh
+docker-compose up -d
+```
+This will run :
+ - Postgresql on localhost:5480
+ - PGAdmin 4 on [http://localhost:5080](http://localhost:5080) login : *test@test.com* , password : *test* (Dans PGAdmin, use the host *postgres_codecarbon* and port *5432* for in docker network access)
+ - CodeCarbon API on [http://localhost:5080](http://localhost:5080)
+ 
+
+## Stop
+
+```sh
+docker-compose down
+```
+
+Postgres database and PGAdmin configuration are saved in separate volumes.
+
+## View logs
+
+```sh
+docker logs codecarbon-api_codecarbon_api_1
+```
+
+## Execute tests
+```sh
+docker exec codecarbon-api_codecarbon_api_1 tox
+```
+
+## Force build
+```sh
+docker-compose up --build
+```
+
+
+## Cleaning
+Delete all files :
+```sh
+docker-compose down -v
+docker image rm codecarbon-api
+```
\ No newline at end of file
diff --git a/docker/docker.env b/docker/docker.env
new file mode 100644
index 000000000..bf5deef66
--- /dev/null
+++ b/docker/docker.env
@@ -0,0 +1,2 @@
+# Warning : docker-compose does not support comment at the end of the line !!!
+API_KEY=798-798-798-798
diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh
new file mode 100755
index 000000000..db607e02d
--- /dev/null
+++ b/docker/entrypoint.sh
@@ -0,0 +1,2 @@
+#!/bin/bash
+# TODO : Run a Python code to use codecarbon package and call the API
\ No newline at end of file


Test patch:
diff --git a/carbonserver/requirements-test.txt b/carbonserver/requirements-test.txt
new file mode 100644
index 000000000..249d80b9b
--- /dev/null
+++ b/carbonserver/requirements-test.txt
@@ -0,0 +1,8 @@
+alembic
+fastapi
+pydantic[email]
+psycopg2-binary
+requests
+sqlalchemy
+tox
+uvicorn[standard]
diff --git a/carbonserver/tests/TESTING.md b/carbonserver/tests/TESTING.md
new file mode 100644
index 000000000..6b28bdad9
--- /dev/null
+++ b/carbonserver/tests/TESTING.md
@@ -0,0 +1,81 @@
+# Testing the carbon server 
+
+
+## Behaviors to test :
+
+
+### Domain logic
+To test the interface exposed by entities, in memory repositories can be used to isolate the domain logic from technical
+ implementation.
+Current entities tests are located in ```tests/infra/repositories```.
+
+
+### Infrastructure
+
+- Testing SqlRepositories : 
+    - Spawn a test database : 
+        - Use docker compose to launch a Postgres instance from project root
+        ```bash
+        docker compose up -d postgres pgadmin
+        ```
+        - Use [alembic](carbonserver/carbonserver/database/alembic/README.md) to inject last version of database schema
+        ```bash
+        alembic upgrade head
+        ```
+        
+- Http server setup in tests/api (TODO)
+- Authentication / user management (TODO)
+
+### Routers 
+To test a router, validation parameters are ensured by pydantic (available on the swagger documentation), 
+and logic is tested by interfaces.
+A Postman collection of requests is available: ```carbonserver/tests/postman/TestCollection.postman_collection.json```.
+
+### Integration
+- Database : in the CI, a prod-like database can be used to test features on real data (TODO)
+- Code Carbon package : Launch a train scenario (TODO)
+
+
+## Running the tests :
+
+### Install the test setup 
+
+In a virtual environment, install and build the api package :
+```bash
+git clone git@github.com:mlco2/codecarbon.git
+git checkout api
+cd carbonserver
+python -m setup install
+python -m setup build   # Verify the build on local environment
+pip install -rrequirements-test.txt # Install test dependencies
+```
+
+
+### Run tests
+```bash
+tox -e unit # Unit tests on api
+tox -e integration # Integration tests
+```
+
+
+To test the HTTP layer, you can also deploy a local instance :
+
+```bash
+cd carbonserver/
+uvicorn main:app --reload
+```
+
+Swagger documentation is available at http://localhost:8000/docs
+
+
+### Run locally the CI 
+
+
+To test the full build process, the Github Actions workflow can be executed locally with act ([install available here](https://raw.githubusercontent.com/nektos/act/master/install.sh)):
+```bash
+# Build patched dockerfile from project root
+docker build act -t local/ubuntu-builder:latest 
+
+# Run GA job from patched instance
+act -j build_server -P ubuntu-latest=local/ubuntu-builder:latest
+```
diff --git a/carbonserver/tests/infra/repository/test_in_memory_emissions_repository.py b/carbonserver/tests/infra/repository/test_in_memory_emissions_repository.py
new file mode 100644
index 000000000..c3846d433
--- /dev/null
+++ b/carbonserver/tests/infra/repository/test_in_memory_emissions_repository.py
@@ -0,0 +1,123 @@
+import pytest
+
+from carbonserver.api.infra.repositories.repository_emissions import InMemoryRepository
+from carbonserver.api.schemas import Emission as SchemaEmission
+from carbonserver.api.schemas import EmissionCreate
+from carbonserver.database.models import Emission as ModelEmission
+
+
+@pytest.fixture()
+def emissions_repository():
+    repo = InMemoryRepository()
+    return repo
+
+
+@pytest.fixture()
+def emission_fixture() -> EmissionCreate:
+    emission = EmissionCreate.parse_obj(
+        {
+            "timestamp": "2021-04-04T08:43:00+02:00",
+            "run_id": "40088f1a-d28e-4980-8d80-bf5600056a14",
+            "duration": 98745,
+            "emissions": 1.548444,
+            "energy_consumed": 57.21874,
+        }
+    )
+    return emission
+
+
+@pytest.fixture()
+def model_emission() -> ModelEmission:
+    model_emission = ModelEmission(
+        **{
+            "id": 1,
+            "timestamp": "2021-04-04T08:43:00+02:00",
+            "run_id": "40088f1a-d28e-4980-8d80-bf5600056a14",
+            "duration": 98745,
+            "emissions": 1.548444,
+            "energy_consumed": 57.21874,
+        }
+    )
+    return model_emission
+
+
+def test_emissions_repository_saves_correct_emission(
+    emissions_repository, model_emission
+):
+    emission = EmissionCreate.parse_obj(
+        {
+            "timestamp": "2021-04-04T08:43:00+02:00",
+            "run_id": "40088f1a-d28e-4980-8d80-bf5600056a14",
+            "duration": 98745,
+            "emissions": 1.548444,
+            "energy_consumed": 57.21874,
+        }
+    )
+    emissions_repository.add_emission(emission)
+    saved_emissions = emissions_repository.emissions
+
+    assert len(saved_emissions) == 1
+    assert saved_emissions[0].id == model_emission.id
+
+
+def test_emissions_repository_get_db_to_class_returns_correct_object_type(
+    emissions_repository, model_emission
+):
+    expected_schema_emission = SchemaEmission.parse_obj(
+        {
+            "id": 1,
+            "timestamp": "2021-04-04T08:43:00+02:00",
+            "run_id": "40088f1a-d28e-4980-8d80-bf5600056a14",
+            "duration": 98745,
+            "emissions": 1.548444,
+            "energy_consumed": 57.21874,
+        }
+    )
+
+    actual_schema_emission = emissions_repository.get_db_to_class(model_emission)
+
+    assert expected_schema_emission == actual_schema_emission
+
+
+def test_get_one_emission_returns_the_correct_emission_from_emission_id(
+    emissions_repository, emission_fixture
+):
+    emission_id = 1
+    expected_emission = SchemaEmission.parse_obj(
+        {
+            "id": 1,
+            "timestamp": "2021-04-04T08:43:00+02:00",
+            "run_id": "40088f1a-d28e-4980-8d80-bf5600056a14",
+            "duration": 98745,
+            "emissions": 1.548444,
+            "energy_consumed": 57.21874,
+        }
+    )
+    emissions_repository.add_emission(emission_fixture)
+
+    actual_emission = emissions_repository.get_one_emission(emission_id)
+
+    assert expected_emission == actual_emission
+
+
+def test_get_one_emission_returns_the_correct_emission_list_from_experiment_id(
+    emissions_repository, emission_fixture
+):
+    run_id = "40088f1a-d28e-4980-8d80-bf5600056a14"
+    expected_emissions = [
+        SchemaEmission.parse_obj(
+            {
+                "id": 1,
+                "timestamp": "2021-04-04T08:43:00+02:00",
+                "run_id": "40088f1a-d28e-4980-8d80-bf5600056a14",
+                "duration": 98745,
+                "emissions": 1.548444,
+                "energy_consumed": 57.21874,
+            }
+        )
+    ]
+    emissions_repository.add_emission(emission_fixture)
+
+    actual_emissions = emissions_repository.get_emissions_from_run(run_id)
+
+    assert expected_emissions == actual_emissions
diff --git a/carbonserver/tests/infra/repository/test_in_memory_experiments_repository.py b/carbonserver/tests/infra/repository/test_in_memory_experiments_repository.py
new file mode 100644
index 000000000..0343ef187
--- /dev/null
+++ b/carbonserver/tests/infra/repository/test_in_memory_experiments_repository.py
@@ -0,0 +1,134 @@
+import pytest
+
+from carbonserver.api.infra.repositories.repository_experiments import (
+    InMemoryRepository,
+)
+from carbonserver.api.schemas import Experiment as SchemaExperiment
+from carbonserver.api.schemas import ExperimentCreate
+from carbonserver.database.models import Experiment as ModelExperiment
+
+
+@pytest.fixture()
+def experiments_repository():
+    repo = InMemoryRepository()
+    return repo
+
+
+@pytest.fixture()
+def experiments_fixture() -> ExperimentCreate:
+    experiment = ExperimentCreate.parse_obj(
+        {
+            "timestamp": "2021-04-04T08:43:00+02:00",
+            "name": "experiment",
+            "description": "Test experiment",
+            "is_active": True,
+            "country_name": "France",
+            "country_iso_code": "FRA",
+            "region": "FRA",
+            "on_cloud": True,
+            "cloud_provider": "AWS",
+            "cloud_region": "eu-west-1a",
+            "project_id": 1,
+        }
+    )
+    return experiment
+
+
+@pytest.fixture()
+def model_experiment() -> ModelExperiment:
+    model_experiment = ModelExperiment(
+        **{
+            "timestamp": "2021-04-04T08:43:00+02:00",
+            "name": "experiment",
+            "description": "Test experiment",
+            "is_active": True,
+            "country_name": "France",
+            "country_iso_code": "FRA",
+            "region": "FRA",
+            "on_cloud": True,
+            "cloud_provider": "AWS",
+            "cloud_region": "eu-west-1a",
+            "project_id": "1",
+        }
+    )
+    return model_experiment
+
+
+def test_experiment_repository_saves_correct_experiment(
+    experiments_repository, model_experiment
+):
+    experiment = ExperimentCreate.parse_obj(
+        {
+            "timestamp": "2021-04-04T08:43:00+02:00",
+            "name": "experiment",
+            "description": "Test experiment",
+            "is_active": True,
+            "country_name": "France",
+            "country_iso_code": "FRA",
+            "region": "FRA",
+            "on_cloud": True,
+            "cloud_provider": "AWS",
+            "cloud_region": "eu-west-1a",
+            "project_id": "1",
+        }
+    )
+    experiments_repository.add_experiment(experiment)
+    saved_experiments = experiments_repository.experiments
+    assert len(saved_experiments) == 1
+    assert saved_experiments[0].id == 1
+
+
+def test_get_one_experiment_returns_the_correct_experiment_from_experiment_id(
+    experiments_repository, experiments_fixture
+):
+    experiment_id = "1"
+    expected_experiment = SchemaExperiment.parse_obj(
+        {
+            "id": "1",
+            "timestamp": "2021-04-04T08:43:00+02:00",
+            "name": "experiment",
+            "description": "Test experiment",
+            "is_active": True,
+            "country_name": "France",
+            "country_iso_code": "FRA",
+            "region": "FRA",
+            "on_cloud": True,
+            "cloud_provider": "AWS",
+            "cloud_region": "eu-west-1a",
+            "project_id": "1",
+        }
+    )
+    experiments_repository.add_experiment(experiments_fixture)
+
+    actual_experiment = experiments_repository.get_one_experiment(experiment_id)
+    print(actual_experiment)
+    assert expected_experiment == actual_experiment
+
+
+def test_get_one_experiment_returns_the_correct_experiment_list_from_experiment_id(
+    experiments_repository, experiments_fixture
+):
+    project_id = "3"
+    expected_emissions = [
+        SchemaExperiment.parse_obj(
+            {
+                "id": "133742",
+                "timestamp": "2021-04-04T08:43:00+02:00",
+                "name": "experiment",
+                "description": "Test experiment",
+                "is_active": True,
+                "country_name": "France",
+                "country_iso_code": "FRA",
+                "region": "FRA",
+                "on_cloud": True,
+                "cloud_provider": "AWS",
+                "cloud_region": "eu-west-1a",
+                "project_id": project_id,
+            }
+        )
+    ]
+    experiments_repository.add_experiment(expected_emissions[0])
+
+    actual_emissions = experiments_repository.get_experiments_from_project(project_id)
+    actual_emissions[0].id = expected_emissions[0].id
+    assert expected_emissions == actual_emissions
diff --git a/carbonserver/tests/infra/repository/test_in_memory_organizations_repository.py b/carbonserver/tests/infra/repository/test_in_memory_organizations_repository.py
new file mode 100644
index 000000000..4e99c6fc3
--- /dev/null
+++ b/carbonserver/tests/infra/repository/test_in_memory_organizations_repository.py
@@ -0,0 +1,75 @@
+import pytest
+
+from carbonserver.api.infra.repositories.repository_organizations import (
+    InMemoryRepository,
+)
+from carbonserver.api.schemas import Organization as SchemaOrganization
+from carbonserver.api.schemas import OrganizationCreate
+from carbonserver.database.models import Organization as ModelOrganization
+
+
+@pytest.fixture()
+def organizations_repository():
+    repo = InMemoryRepository()
+    return repo
+
+
+@pytest.fixture()
+def organizations_fixture() -> OrganizationCreate:
+    organization = OrganizationCreate.parse_obj(
+        {"name": "1", "description": "Test organization"}
+    )
+    return organization
+
+
+@pytest.fixture()
+def model_organization() -> ModelOrganization:
+    model_organization = ModelOrganization(
+        **{"name": "1", "description": "Test organization"}
+    )
+    return model_organization
+
+
+def test_organizations_repository_saves_correct_organization(
+    organizations_repository, model_organization
+):
+    organization = OrganizationCreate.parse_obj(
+        {"name": "1", "description": "Test organization"}
+    )
+    organizations_repository.add_organization(organization)
+    saved_experiments = organizations_repository.organizations
+    assert len(saved_experiments) == 1
+    assert saved_experiments[0].name == "1"
+
+
+def test_get_one_organization_returns_the_correct_organization_from_organization_name(
+    organizations_repository, organizations_fixture
+):
+    organization_name = "1"
+    expected_organization = SchemaOrganization.parse_obj(
+        {"id": 1, "name": "1", "description": "Test organization"}
+    )
+    organizations_repository.add_organization(organizations_fixture)
+
+    actual_organization = organizations_repository.get_one_organization(
+        organization_name
+    )
+    assert expected_organization == actual_organization
+
+
+def test_get_one_organization_returns_the_correct_organizations_list_from_organization_name(
+    organizations_repository, organizations_fixture
+):
+    organization_name = "1"
+    expected_organizations = [
+        SchemaOrganization.parse_obj(
+            {"id": 1, "name": "1", "description": "Test organization"}
+        )
+    ]
+    organizations_repository.add_organization(organizations_fixture)
+
+    actual_organizations = organizations_repository.get_team_from_organizations(
+        organization_name
+    )
+
+    assert expected_organizations == actual_organizations
diff --git a/carbonserver/tests/infra/repository/test_in_memory_users_repository.py b/carbonserver/tests/infra/repository/test_in_memory_users_repository.py
new file mode 100644
index 000000000..dbdf6ccf6
--- /dev/null
+++ b/carbonserver/tests/infra/repository/test_in_memory_users_repository.py
@@ -0,0 +1,83 @@
+import pytest
+
+from carbonserver.api.infra.repositories.repository_users import InMemoryRepository
+from carbonserver.api.schemas import UserCreate
+from carbonserver.database.models import User as ModelUser
+
+
+@pytest.fixture()
+def user_repository():
+    repo = InMemoryRepository()
+    return repo
+
+
+@pytest.fixture()
+def user_fixture() -> UserCreate:
+    user = UserCreate.parse_obj(
+        {
+            "name": "John McLane",
+            "email": "mclane@grubber.io",
+            "password": "john",
+        }
+    )
+    return user
+
+
+@pytest.fixture()
+def model_user() -> ModelUser:
+    model_user = ModelUser(
+        **{
+            "name": "John McLane",
+            "email": "mclane@grubber.io",
+            "hashed_password": "john",
+            "api_key": "key",
+        }
+    )
+    return model_user
+
+
+def test_create_user_saves_correct_user(user_repository):
+    user = UserCreate.parse_obj(
+        {"name": "John McLane", "email": "mclane@grubber.io", "password": "john"}
+    )
+
+    created_user = user_repository.create_user(user)
+
+    assert created_user.user_id == 1
+
+
+def test_list_users_returns_all_users(user_repository):
+    user1 = UserCreate.parse_obj(
+        {"name": "John McLane", "email": "mclane@grubber.io", "password": "john"}
+    )
+
+    user2 = UserCreate.parse_obj(
+        {"name": "Simon Gruber", "email": "Simon@grubber.io", "password": "john"}
+    )
+
+    user_repository.create_user(user1)
+    user_repository.create_user(user2)
+    expected_user_ids = [1, 2]
+
+    actual_users = user_repository.list_users()
+    actual_user_ids = [user.user_id for user in actual_users]
+    assert expected_user_ids == actual_user_ids
+
+
+def test_get_user_by_id_returns_correct_user(user_repository):
+    user1 = UserCreate.parse_obj(
+        {"name": "John McLane", "email": "mclane@grubber.io", "password": "john"}
+    )
+
+    user2 = UserCreate.parse_obj(
+        {"name": "Simon Gruber", "email": "Simon@grubber.io", "password": "john"}
+    )
+
+    user_repository.create_user(user1)
+    user_repository.create_user(user2)
+    expected_user_email = "Simon@grubber.io"
+    expected_user_id = 2
+
+    actual_user = user_repository.get_user_by_id(expected_user_id)
+
+    assert expected_user_email == actual_user.email
diff --git a/carbonserver/tests/postman/TestCollection.postman_collection.json b/carbonserver/tests/postman/TestCollection.postman_collection.json
new file mode 100644
index 000000000..ca76606af
--- /dev/null
+++ b/carbonserver/tests/postman/TestCollection.postman_collection.json
@@ -0,0 +1,393 @@
+{
+	"info": {
+		"_postman_id": "0f05af20-07c9-4ce5-a0d3-06ef21364095",
+		"name": "Test Collection",
+		"schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json"
+	},
+	"item": [
+		{
+			"name": "Add Emission",
+			"protocolProfileBehavior": {
+				"followRedirects": false,
+				"followOriginalHttpMethod": false,
+				"followAuthorizationHeader": false
+			},
+			"request": {
+				"method": "PUT",
+				"header": [
+					{
+						"key": "Content-Type",
+						"value": "application/json"
+					},
+					{
+						"key": "x_token\n",
+						"value": "fake-super-secret-token"
+					},
+					{
+						"key": "token",
+						"value": "jessica"
+					}
+				],
+				"body": {
+					"mode": "raw",
+					"raw": "{\"timestamp\":\"2021-04-04T08:43:00+02:00\",\"run_id\":\"40088f1a-d28e-4980-8d80-bf5600056a14\",\"duration\":98745,\"emissions\":1.548444,\"energy_consumed\":57.21874}"
+				},
+				"url": "http://localhost:8000/emission"
+			},
+			"response": []
+		},
+		{
+			"name": "Get emission by id",
+			"protocolProfileBehavior": {
+				"followRedirects": false,
+				"followOriginalHttpMethod": false,
+				"followAuthorizationHeader": false
+			},
+			"request": {
+				"method": "GET",
+				"header": [
+					{
+						"key": "Content-Type",
+						"value": "application/json"
+					},
+					{
+						"key": "x_token\n",
+						"value": "fake-super-secret-token"
+					},
+					{
+						"key": "token",
+						"value": "jessica"
+					}
+				],
+				"url": {
+					"raw": "http://localhost:8000/emission/1?id=1",
+					"protocol": "http",
+					"host": [
+						"localhost"
+					],
+					"port": "8000",
+					"path": [
+						"emission",
+						"1"
+					],
+					"query": [
+						{
+							"key": "id",
+							"value": "1"
+						}
+					]
+				},
+				"description": "Get saved emission objects by id."
+			},
+			"response": []
+		},
+		{
+			"name": "Get emissions by run id",
+			"protocolProfileBehavior": {
+				"followRedirects": false,
+				"followOriginalHttpMethod": false,
+				"followAuthorizationHeader": false
+			},
+			"request": {
+				"method": "GET",
+				"header": [
+					{
+						"key": "Content-Type",
+						"value": "application/json"
+					},
+					{
+						"key": "x_token\n",
+						"value": "fake-super-secret-token"
+					},
+					{
+						"key": "token",
+						"value": "jessica"
+					}
+				],
+				"url": {
+					"raw": "http://localhost:8000/emissions/40088f1a-d28e-4980-8d80-bf5600056a14?id=1",
+					"protocol": "http",
+					"host": [
+						"localhost"
+					],
+					"port": "8000",
+					"path": [
+						"emissions",
+						"40088f1a-d28e-4980-8d80-bf5600056a14"
+					],
+					"query": [
+						{
+							"key": "id",
+							"value": "1"
+						}
+					]
+				},
+				"description": "Get saved emission objects for example run id."
+			},
+			"response": []
+		},
+		{
+			"name": "Add Run",
+			"protocolProfileBehavior": {
+				"followRedirects": false,
+				"followOriginalHttpMethod": false,
+				"followAuthorizationHeader": false
+			},
+			"request": {
+				"method": "PUT",
+				"header": [
+					{
+						"key": "x_token\n",
+						"value": "fake-super-secret-token"
+					},
+					{
+						"key": "token",
+						"value": "jessica"
+					}
+				],
+				"body": {
+					"mode": "raw",
+					"raw": "{\n  \"timestamp\": \"2021-04-04T08:43:00+02:00\",\n  \"experiment_id\": \"1\"\n}"
+				},
+				"url": "http://localhost:8000/run"
+			},
+			"response": []
+		},
+		{
+			"name": "Add Experiment",
+			"protocolProfileBehavior": {
+				"followRedirects": false,
+				"followOriginalHttpMethod": false,
+				"followAuthorizationHeader": false
+			},
+			"request": {
+				"method": "PUT",
+				"header": [
+					{
+						"key": "x_token\n",
+						"value": "fake-super-secret-token"
+					},
+					{
+						"key": "token",
+						"value": "jessica"
+					}
+				],
+				"body": {
+					"mode": "raw",
+					"raw": "{\n  \"name\": \"Run on AWS\",\n  \"description\": \"AWS API for Code Carbon\",\n  \"timestamp\": \"2021-04-04T08:43:00+02:00\",\n  \"country_name\": \"France\",\n  \"country_iso_code\": \"FRA\",\n  \"region\": \"france\",\n  \"on_cloud\": true,\n  \"cloud_provider\": \"aws\",\n  \"cloud_region\": \"eu-west-1a\",\n  \"project_id\": \"1\"\n}"
+				},
+				"url": "http://localhost:8000/experiment"
+			},
+			"response": []
+		},
+		{
+			"name": "Get Experiment by id",
+			"protocolProfileBehavior": {
+				"followRedirects": false,
+				"followOriginalHttpMethod": false,
+				"followAuthorizationHeader": false
+			},
+			"request": {
+				"method": "GET",
+				"header": [
+					{
+						"key": "x_token\n",
+						"value": "fake-super-secret-token"
+					},
+					{
+						"key": "token",
+						"value": "jessica"
+					}
+				],
+				"url": "http://localhost:8000/experiment/1"
+			},
+			"response": []
+		},
+		{
+			"name": "Add Organization",
+			"protocolProfileBehavior": {
+				"followRedirects": false,
+				"followOriginalHttpMethod": false,
+				"followAuthorizationHeader": false
+			},
+			"request": {
+				"method": "PUT",
+				"header": [
+					{
+						"key": "Content-Type",
+						"value": "application/json"
+					},
+					{
+						"key": "x_token",
+						"value": "fake-super-secret-token"
+					},
+					{
+						"key": "token",
+						"value": "jessica"
+					}
+				],
+				"body": {
+					"mode": "raw",
+					"raw": "{\"name\":\"Code Carbon\",\"description\":\"Save the world, one run at a time.\"}"
+				},
+				"url": "http://localhost:8000/organization"
+			},
+			"response": []
+		},
+		{
+			"name": "Get Organization by Id",
+			"protocolProfileBehavior": {
+				"followRedirects": false,
+				"followOriginalHttpMethod": false,
+				"followAuthorizationHeader": false
+			},
+			"request": {
+				"method": "GET",
+				"header": [
+					{
+						"key": "Content-Type",
+						"value": "application/json"
+					},
+					{
+						"key": "x_token",
+						"value": "fake-super-secret-token"
+					},
+					{
+						"key": "token",
+						"value": "jessica"
+					}
+				],
+				"url": "http://localhost:8000/organization/1"
+			},
+			"response": []
+		},
+		{
+			"name": "Add Project",
+			"protocolProfileBehavior": {
+				"followRedirects": false,
+				"followOriginalHttpMethod": false,
+				"followAuthorizationHeader": false
+			},
+			"request": {
+				"method": "PUT",
+				"header": [
+					{
+						"key": "x_token",
+						"value": "fake-super-secret-token"
+					},
+					{
+						"key": "token",
+						"value": "jessica"
+					}
+				],
+				"body": {
+					"mode": "raw",
+					"raw": "{\n  \"name\": \"API Code Carbon\",\n  \"description\": \"API for Code Carbon\",\n  \"team_id\": \"1\"\n}\n"
+				},
+				"url": "http://localhost:8000/project"
+			},
+			"response": []
+		},
+		{
+			"name": "Get Project",
+			"protocolProfileBehavior": {
+				"followRedirects": false,
+				"followOriginalHttpMethod": false,
+				"followAuthorizationHeader": false
+			},
+			"request": {
+				"method": "GET",
+				"header": [
+					{
+						"key": "x_token",
+						"value": "fake-super-secret-token"
+					},
+					{
+						"key": "token",
+						"value": "jessica"
+					}
+				],
+				"url": "http://localhost:8000/project/1"
+			},
+			"response": []
+		},
+		{
+			"name": "Add Team",
+			"protocolProfileBehavior": {
+				"followRedirects": false,
+				"followOriginalHttpMethod": false,
+				"followAuthorizationHeader": false
+			},
+			"request": {
+				"method": "PUT",
+				"header": [
+					{
+						"key": "x_token",
+						"value": "fake-super-secret-token"
+					},
+					{
+						"key": "token",
+						"value": "jessica"
+					}
+				],
+				"body": {
+					"mode": "raw",
+					"raw": "{\n  \"name\": \"Data For Good\",\n  \"description\": \"Data For Good France\",\n  \"organization_id\": \"1\"\n}"
+				},
+				"url": "http://localhost:8000/team"
+			},
+			"response": []
+		},
+		{
+			"name": "Get Team",
+			"protocolProfileBehavior": {
+				"followRedirects": false,
+				"followOriginalHttpMethod": false,
+				"followAuthorizationHeader": false
+			},
+			"request": {
+				"method": "GET",
+				"header": [
+					{
+						"key": "x_token",
+						"value": "fake-super-secret-token"
+					},
+					{
+						"key": "token",
+						"value": "jessica"
+					}
+				],
+				"url": "http://localhost:8000/team/1"
+			},
+			"response": []
+		},
+		{
+			"name": "Add User",
+			"protocolProfileBehavior": {
+				"followRedirects": false,
+				"followOriginalHttpMethod": false,
+				"followAuthorizationHeader": false
+			},
+			"request": {
+				"method": "PUT",
+				"header": [],
+				"url": "C"
+			},
+			"response": []
+		},
+		{
+			"name": "Get User",
+			"protocolProfileBehavior": {
+				"followRedirects": false,
+				"followOriginalHttpMethod": false,
+				"followAuthorizationHeader": false
+			},
+			"request": {
+				"method": "GET",
+				"header": [],
+				"url": "C"
+			},
+			"response": []
+		}
+	]
+}
\ No newline at end of file
diff --git a/carbonserver/tests/routers/test_emissions.py b/carbonserver/tests/routers/test_emissions.py
new file mode 100644
index 000000000..bebf1b2b1
--- /dev/null
+++ b/carbonserver/tests/routers/test_emissions.py
@@ -0,0 +1,90 @@
+import pytest
+from fastapi import Depends, FastAPI
+from fastapi.testclient import TestClient
+
+from carbonserver.api.dependencies import get_query_token
+from carbonserver.api.routers import emissions
+
+"""
+Setting up a test client & a test server in specific states would be interesting.
+This setup may be extracted on a separate test container file to let developers ingest multiple / custom configurations.
+# Current state:
+ - No actual database is plugged on unit tests
+ - Client is emitting from same network as server
+ - This test class tests only API handling (logic is mocked)
+"""
+
+
+@pytest.fixture()
+def emissions_router():
+    app = FastAPI(dependencies=[Depends(get_query_token)])
+    app.include_router(emissions.router)
+    client = TestClient(app)
+    return app, client
+
+
+def test_post_emission_not_implemented(emissions_router):
+    app, client = emissions_router
+    response = client.post("/emission")
+    assert response.status_code == 405
+
+
+def test_put_emission_returns_success_with_correct_object(emissions_router):
+    app, client = emissions_router
+    response = client.put(
+        "/emission",
+        json={
+            "timestamp": "2021-04-04T08:43:00+02:00",
+            "run_id": "40088f1a-d28e-4980-8d80-bf5600056a14",
+            "duration": 98745,
+            "emissions": 1.548444,
+            "energy_consumed": 57.21874,
+        },
+    )
+    assert response.status_code == 201
+
+
+def test_put_emission_empty_returns_unprocessable(emissions_router):
+    app, client = emissions_router
+    response = client.put("/emission")
+    assert response.status_code == 422
+
+
+def test_read_emission_by_emission_id_returns_emission(emissions_router):
+    app, client = emissions_router
+    emission_id = "1"
+    response = client.get("/emission/{emission_id}".format(emission_id=emission_id))
+
+    assert response.status_code == 200
+    assert response.json() == {
+        "duration": 98745,
+        "emissions": 1.548444,
+        "energy_consumed": 57.21874,
+        "id": 1,
+        "run_id": "40088f1a-d28e-4980-8d80-bf5600056a14",
+        "timestamp": "2021-04-04T08:43:00",
+    }
+
+
+def test_read_emission_by_emission_id_returns_not_found_error(emissions_router):
+    app, client = emissions_router
+    emission_id = "64565sd4f5g6sd4f65g4"
+    response = client.get("/emission/{emission_id}".format(emission_id=emission_id))
+
+    assert response.status_code == 404
+
+
+def test_read_emissions_by_run_id_returns_not_found_error(emissions_router):
+    app, client = emissions_router
+    run_id = "1"
+    response = client.get("/emissions/{run_id}".format(run_id=run_id))
+
+    assert response.status_code == 404
+
+
+def test_read_emission_returns_experiment_id_returns_not_found_error(emissions_router):
+    app, client = emissions_router
+    run_id = "wxcb46554vb4651"
+    response = client.get("/emissions/{run_id}".format(run_id=run_id))
+
+    assert response.status_code == 404