diff --git a/.gitignore b/.gitignore
index aaf75f6fb704d482d549769501041a1736f90a63..aba790683b141a685f8664edb61febe4b63565ee 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,3 +22,5 @@ record.txt
 .envrc
 coverage.xml
 test_results.xml
+junit-coverage.xml
+.tox/
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 845b719f2378d396a8b75296a68d1e32c840e7c7..ce78bd8caadc61ae6a09f10d9dd40c906045ef49 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1 +1,67 @@
-include: 'https://gitlab.idiap.ch/bob/bob.devtools/raw/master/bob/devtools/data/gitlab-ci/single-package.yaml'
+variables:
+  PYTHONUNBUFFERED: "1"
+  XDG_CACHE_HOME: "${CI_PROJECT_DIR}/cache"
+
+stages:
+  - qa
+  - test
+  - doc
+
+qa:
+  stage: qa
+  image: python:3.10-slim
+  tags:
+    - bob
+    - docker
+  before_script:
+    - pip install pre-commit
+  script:
+    - pre-commit run --all-files --show-diff-on-failure --verbose
+  cache:
+      paths:
+        - ${XDG_CACHE_HOME}/pre-commit
+        - ${XDG_CACHE_HOME}/pip
+  rules:
+    - exists:
+      - .pre-commit-config.yaml
+
+test:
+  stage: test
+  image: python:${PYTHON_VERSION}-slim
+  parallel:
+    matrix:
+      - PYTHON_VERSION: ["3.9", "3.10"]
+  cache:
+      paths:
+        - ${XDG_CACHE_HOME}/pip
+  before_script:
+    - pip install '.[test]'
+  script:
+    - pytest --verbose --cov exposed --cov-report term-missing --cov-report html:html/coverage --cov-report xml:coverage.xml --junitxml=junit-coverage.xml --pyargs exposed.tests
+  coverage: '/(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/'
+  artifacts:
+    paths:
+      - html
+    reports:
+      junit: test_results.xml
+      coverage_report:
+        coverage_format: cobertura
+        path: coverage.xml
+
+doc:
+  stage: doc
+  image: python:${PYTHON_VERSION}-slim
+  cache:
+      paths:
+        - ${XDG_CACHE_HOME}/pip
+  before_script:
+    - pip install '.[doc]'
+  script:
+    - sphinx-build -aEW doc html/sphinx
+    - sphinx-build -aEb doctest doc html/doctest
+  artifacts:
+    paths:
+      - html
+  rules:
+    - exists:
+      - doc
diff --git a/MANIFEST.in b/MANIFEST.in
index 1582fb2f59d396a9000775113143b1071991087a..99858f73d3e7f21d06aab034bc9e58e48ccb9ac2 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,3 +1,3 @@
-include LICENSE README.md version.txt
+include LICENSE README.rst
 recursive-include doc *.rst *.txt *.py *.ico *.png
-recursive-include tests *.py *.cfg
+recursive-include src/exposed/tests/data *.cfg
diff --git a/conda/meta.yaml b/conda/meta.yaml
index 7e73fee0e82e713c00de312cabee17b1c733df56..f131eebcb204da77830b31a08738b65ab54db9ce 100644
--- a/conda/meta.yaml
+++ b/conda/meta.yaml
@@ -1,61 +1,44 @@
-{% set name = 'exposed' %}
-{% set project_dir = environ.get('RECIPE_DIR') + '/..' %}
+{% set project_data = load_file_data(RECIPE_DIR + '/../pyproject.toml') %}
 
 package:
-  name: {{ name }}
-  version: {{ environ.get('BOB_PACKAGE_VERSION', '0.0.1') }}
+  name: {{ project_data['project']['name'] }}
+  version: {{ project_data['project']['version'] }}
+
+source:
+  path: ..
 
 build:
+  noarch: python
   number: {{ environ.get('BOB_BUILD_NUMBER', 0) }}
   run_exports:
-    - {{ pin_subpackage(name) }}
+    - {{ pin_subpackage(project_data['project']['name']) }}
   script:
-    - cd {{ project_dir }}
-    {% if environ.get('BUILD_EGG') %}
-    - "{{ PYTHON }} setup.py sdist --formats=zip"
-    {% endif %}
-    - "{{ PYTHON }} -m pip install . -vv"
-    # installs the documentation source, readme to share/doc so it is available
-    # during test time
-    - install -d "${PREFIX}/share/doc/{{ name }}"
-    - cp -R README.rst doc "${PREFIX}/share/doc/{{ name }}/"
+    - "{{ PYTHON }} -m pip install {{ SRC_DIR }} -vv"
 
 requirements:
   host:
-    - python {{ python }}
-    - setuptools {{ setuptools }}
+    - python >=3.9
     - pip {{ pip }}
     - click >=8
     - click {{ click }}
     - tomli {{ tomli }}
     - tomli-w {{ tomli_w }}
   run:
-    - python
+    - python >=3.9
     - {{ pin_compatible('click') }}
     - {{ pin_compatible('tomli') }}
     - {{ pin_compatible('tomli-w') }}
 
 test:
   imports:
-    - {{ name }}
+    - {{ project_data['project']['name'] }}
   commands:
-    - pytest --verbose --cov {{ name }} --cov-report term-missing --cov-report html:{{ project_dir }}/sphinx/coverage --cov-report xml:{{ project_dir }}/coverage.xml --junitxml={{ project_dir }}/test_results.xml {{ project_dir }}/tests
-    - sphinx-build -aEW {{ project_dir }}/doc {{ project_dir }}/sphinx
-    - sphinx-build -aEb doctest {{ project_dir }}/doc {{ project_dir }}/sphinx
-    - conda inspect linkages -p $PREFIX {{ name }}  # [not win]
-    - conda inspect objects -p $PREFIX {{ name }}  # [osx]
+    - pytest -sv --pyargs {{ project_data['project']['name'] }}.tests
   requires:
     - pytest {{ pytest }}
-    - pytest-cov {{ pytest_cov }}
-    - coverage {{ coverage }}
-    - sphinx {{ sphinx }}
-    - sphinx_rtd_theme {{ sphinx_rtd_theme }}
-    - sphinx-autodoc-typehints {{ sphinx_autodoc_typehints }}
-    - sphinxcontrib-programoutput {{ sphinxcontrib_programoutput }}
-    - font-ttf-dejavu-sans-mono {{ font_ttf_dejavu_sans_mono }}
 
 about:
-  home: https://www.idiap.ch/software/bob/
-  license: BSD 3-Clause
-  summary: Configuration Support for Python Packages and CLIs
+  home: {{ project_data['project']['urls']['homepage'] }}
+  summary: {{ project_data['project']['description'] }}
+  license: {{ project_data['project']['license']['text'] }}
   license_family: BSD
diff --git a/pyproject.toml b/pyproject.toml
index ab102ccc9385f98d803216ea766a2eb6bcf63d93..977d55d0c74da58a64ea4ae2fd6e20690ebcf730 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,7 +1,71 @@
 [build-system]
-    requires = ["setuptools>=51.0.0"]
+    requires = ["setuptools>=61.0.0"]
     build-backend = "setuptools.build_meta"
 
+[project]
+name = "exposed"
+version = "1.0.0b0"
+description = "Configuration Support for Python Packages and CLIs"
+dynamic = ["readme"]
+license = {text = "BSD 3-Clause License"}
+authors = [
+  {name = "Andre Anjos"},
+  {email = "andre.anjos@idiap.ch"},
+]
+classifiers = [
+    "Framework :: Bob",
+    "Development Status :: 4 - Beta",
+    "Intended Audience :: Developers",
+    "License :: OSI Approved :: BSD License",
+    "Natural Language :: English",
+    "Programming Language :: Python :: 3",
+    "Topic :: Software Development :: Libraries :: Python Modules",
+]
+dependencies = [
+    "click>=8",
+    "tomli",
+    "tomli-w",
+]
+
+[project.urls]
+documentation = "https://www.idiap.ch/software/bob/docs/bob/exposed/master/"
+homepage = "https://pypi.org/project/exposed"
+repository = "https://gitlab.idiap.ch/bob/exposed"
+changelog = "https://gitlab.idiap.ch/bob/exposed/-/releases"
+
+[project.optional-dependencies]
+qa = ["pre-commit"]
+doc = [
+    "sphinx",
+    "sphinx_rtd_theme",
+    "sphinx-autodoc-typehints",
+    "sphinxcontrib-programoutput",
+    ]
+test = [
+    "pytest",
+    "pytest-cov",
+    "coverage",
+    ]
+
+[project.entry-points."exposed.test.config"]
+first = "exposed.tests.data.basic_config"
+first-a = "exposed.tests.data.basic_config:a"
+first-b = "exposed.tests.data.basic_config:b"
+second = "exposed.tests.data.second_config"
+second-b = "exposed.tests.data.second_config:b"
+second-c = "exposed.tests.data.second_config:c"
+complex = "exposed.tests.data.complex"
+complex-var = "exposed.tests.data.complex:cplx"
+verbose-config = "exposed.tests.data.verbose_config"
+error-config = "exposed.tests.data.doesnt_exist"
+
+[tool.setuptools]
+zip-safe = false
+
+[tool.setuptools.dynamic]
+version = {file = "version.txt"}
+readme = {file = "README.rst"}
+
 [tool.isort]
     profile = "black"
     line_length = 80
@@ -18,15 +82,3 @@
 addopts = [
     "--import-mode=importlib",
 ]
-
-[tool.tox]
-legacy_tox_ini = """
-[tox]
-envlist = py39,py310
-
-[testenv]
-deps =
-    pytest-cov
-commands =
-    pytest --verbose --cov exposed --cov-report term-missing --cov-report html:html/coverage --cov-report xml:coverage.xml --junitxml=junit-coverage.xml tests/
-"""
diff --git a/setup.cfg b/setup.cfg
deleted file mode 100644
index 8cc1a8e8c4a528f06e19ef75ffb924f18aca1ea9..0000000000000000000000000000000000000000
--- a/setup.cfg
+++ /dev/null
@@ -1,39 +0,0 @@
-[metadata]
-name = exposed
-version = file: version.txt
-description = Configuration Support for Python Packages and CLIs
-long_description = file: README.rst
-url = https://gitlab.idiap.ch/bob/exposed
-license = BSD 3-Clause License
-author = Andre Anjos
-author_email = andre.anjos@idiap.ch
-classifiers =
-    Framework :: Bob
-    Development Status :: 4 - Beta
-    Intended Audience :: Developers
-    License :: OSI Approved :: BSD License
-    Natural Language :: English
-    Programming Language :: Python :: 3
-    Topic :: Software Development :: Libraries :: Python Modules
-
-[options]
-zip_safe = True
-include_package_data = True
-packages = find:
-install_requires =
-    click>=8
-    tomli
-    tomli-w
-
-[options.entry_points]
-exposed.test.config =
-    first = tests.data.basic_config
-    first-a = tests.data.basic_config:a
-    first-b = tests.data.basic_config:b
-    second = tests.data.second_config
-    second-b = tests.data.second_config:b
-    second-c = tests.data.second_config:c
-    complex = tests.data.complex
-    complex-var = tests.data.complex:cplx
-    verbose-config = tests.data.verbose_config
-    error-config = tests.data.doesnt_exist
diff --git a/exposed/__init__.py b/src/exposed/__init__.py
similarity index 100%
rename from exposed/__init__.py
rename to src/exposed/__init__.py
diff --git a/exposed/click.py b/src/exposed/click.py
similarity index 100%
rename from exposed/click.py
rename to src/exposed/click.py
diff --git a/exposed/config.py b/src/exposed/config.py
similarity index 100%
rename from exposed/config.py
rename to src/exposed/config.py
diff --git a/exposed/logging.py b/src/exposed/logging.py
similarity index 100%
rename from exposed/logging.py
rename to src/exposed/logging.py
diff --git a/exposed/rc.py b/src/exposed/rc.py
similarity index 100%
rename from exposed/rc.py
rename to src/exposed/rc.py
diff --git a/tests/__init__.py b/src/exposed/tests/__init__.py
similarity index 100%
rename from tests/__init__.py
rename to src/exposed/tests/__init__.py
diff --git a/tests/conftest.py b/src/exposed/tests/conftest.py
similarity index 100%
rename from tests/conftest.py
rename to src/exposed/tests/conftest.py
diff --git a/tests/data/__init__.py b/src/exposed/tests/data/__init__.py
similarity index 100%
rename from tests/data/__init__.py
rename to src/exposed/tests/data/__init__.py
diff --git a/tests/data/basic_config.py b/src/exposed/tests/data/basic_config.py
similarity index 100%
rename from tests/data/basic_config.py
rename to src/exposed/tests/data/basic_config.py
diff --git a/tests/data/complex.py b/src/exposed/tests/data/complex.py
similarity index 100%
rename from tests/data/complex.py
rename to src/exposed/tests/data/complex.py
diff --git a/tests/data/oldjson.cfg b/src/exposed/tests/data/oldjson.cfg
similarity index 100%
rename from tests/data/oldjson.cfg
rename to src/exposed/tests/data/oldjson.cfg
diff --git a/tests/data/second_config.py b/src/exposed/tests/data/second_config.py
similarity index 100%
rename from tests/data/second_config.py
rename to src/exposed/tests/data/second_config.py
diff --git a/tests/data/test_dump_config.py b/src/exposed/tests/data/test_dump_config.py
similarity index 100%
rename from tests/data/test_dump_config.py
rename to src/exposed/tests/data/test_dump_config.py
diff --git a/tests/data/test_dump_config2.py b/src/exposed/tests/data/test_dump_config2.py
similarity index 100%
rename from tests/data/test_dump_config2.py
rename to src/exposed/tests/data/test_dump_config2.py
diff --git a/tests/data/userdefaults_ex1.cfg b/src/exposed/tests/data/userdefaults_ex1.cfg
similarity index 100%
rename from tests/data/userdefaults_ex1.cfg
rename to src/exposed/tests/data/userdefaults_ex1.cfg
diff --git a/tests/data/verbose_config.py b/src/exposed/tests/data/verbose_config.py
similarity index 100%
rename from tests/data/verbose_config.py
rename to src/exposed/tests/data/verbose_config.py
diff --git a/tests/test_click.py b/src/exposed/tests/test_click.py
similarity index 96%
rename from tests/test_click.py
rename to src/exposed/tests/test_click.py
index e43d219f02e2565364a8ed425951420574ee1f88..985af387bd8a74d66c1c458409053b78e48d9d90 100644
--- a/tests/test_click.py
+++ b/src/exposed/tests/test_click.py
@@ -266,7 +266,7 @@ def test_resource_option():
         assert a == 1
 
     runner = CliRunner()
-    result = runner.invoke(cli1, ["-a", "tests.data.basic_config"])
+    result = runner.invoke(cli1, ["-a", "exposed.tests.data.basic_config"])
     assert result.exit_code == 0
 
     # test usage without ConfigCommand and without entry_point_group
@@ -288,12 +288,12 @@ def test_resource_option():
         "-a",
         "--a",
         cls=ResourceOption,
-        string_exceptions=("tests.data.basic_config"),
+        string_exceptions=("exposed.tests.data.basic_config"),
         entry_point_group="exposed.test.config",
     )
     def cli3(a):
-        assert a == "tests.data.basic_config"
+        assert a == "exposed.tests.data.basic_config"
 
     runner = CliRunner()
-    result = runner.invoke(cli3, ["-a", "tests.data.basic_config"])
+    result = runner.invoke(cli3, ["-a", "exposed.tests.data.basic_config"])
     assert result.exit_code == 0
diff --git a/tests/test_config.py b/src/exposed/tests/test_config.py
similarity index 89%
rename from tests/test_config.py
rename to src/exposed/tests/test_config.py
index f66368336b3945f7e2680c4f55662f5d90c22a21..44b7412d01f1a7709a4e05ef3c16ca417eb21b95 100644
--- a/tests/test_config.py
+++ b/src/exposed/tests/test_config.py
@@ -48,9 +48,9 @@ def test_config_with_module():
 
     c = load(
         [
-            "tests.data.basic_config",
-            "tests.data.second_config",
-            "tests.data.complex",
+            "exposed.tests.data.basic_config",
+            "exposed.tests.data.second_config",
+            "exposed.tests.data.complex",
         ]
     )
     assert hasattr(c, "a") and c.a == 1
@@ -77,7 +77,11 @@ def test_config_with_entry_point_file_missing():
 def test_config_with_mixture(datadir):
 
     c = load(
-        [datadir / "basic_config.py", "tests.data.second_config", "complex"],
+        [
+            datadir / "basic_config.py",
+            "exposed.tests.data.second_config",
+            "complex",
+        ],
         entry_point_group="exposed.test.config",
     )
     assert hasattr(c, "a") and c.a == 1
@@ -93,14 +97,14 @@ def test_config_not_found(datadir):
 
 def test_config_load_attribute():
 
-    a = load(["tests.data.basic_config"], attribute_name="a")
+    a = load(["exposed.tests.data.basic_config"], attribute_name="a")
     assert a == 1
 
 
 def test_config_load_no_attribute():
 
     with pytest.raises(ImportError):
-        _ = load(["tests.data.basic_config"], attribute_name="wrong")
+        _ = load(["exposed.tests.data.basic_config"], attribute_name="wrong")
 
 
 @pytest.fixture
@@ -128,7 +132,7 @@ def test_config_click_config_list(cli_messages):
     runner = CliRunner()
     result = runner.invoke(cli, ["list"])
     assert result.exit_code == 0
-    assert result.output.startswith("module: tests.data")
+    assert result.output.startswith("module: exposed.tests.data")
     assert "(cannot be loaded; add another -v for details)" not in result.output
 
 
@@ -138,7 +142,7 @@ def test_config_click_config_list_v(cli_messages):
     runner = CliRunner()
     result = runner.invoke(cli, ["list", "-v"])
     assert result.exit_code == 0
-    assert result.output.startswith("module: tests.data")
+    assert result.output.startswith("module: exposed.tests.data")
     assert "(cannot be loaded; add another -v for details)" in result.output
     assert "[module] Example configuration module" in result.output
 
@@ -149,7 +153,7 @@ def test_config_click_config_list_vv(cli_messages):
     runner = CliRunner()
     result = runner.invoke(cli, ["list", "-vv"])
     assert result.exit_code == 0
-    assert result.output.startswith("module: tests.data")
+    assert result.output.startswith("module: exposed.tests.data")
     assert "(cannot be loaded; add another -v for details)" in result.output
     assert "[module] Example configuration module" in result.output
     assert "NameError" in messages.getvalue()
diff --git a/tests/test_logging.py b/src/exposed/tests/test_logging.py
similarity index 100%
rename from tests/test_logging.py
rename to src/exposed/tests/test_logging.py
diff --git a/tests/test_rc.py b/src/exposed/tests/test_rc.py
similarity index 100%
rename from tests/test_rc.py
rename to src/exposed/tests/test_rc.py
diff --git a/version.txt b/version.txt
deleted file mode 100644
index cdf299159fb0a20cae5dbf62cc74d6c6bb0bc424..0000000000000000000000000000000000000000
--- a/version.txt
+++ /dev/null
@@ -1 +0,0 @@
-1.0.0b0