diff --git a/.gitignore b/.gitignore
index f7275bbbd035b827023cbae18954c0703b200c34..3752bf77bcdd358a92d82f898f7bc60a0e17b614 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,15 @@
-venv/
+__pycache__/
+*.egg-info
+.coverage*
+coverage.xml
+.vscode
+venv
+oss-params.json
+.mypy_cache
+.pytest_cache
+.ruff_cache
+.tox
+build/
+.idea
+.venv
+.env
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..a825c2db1f8b768cee467c4c074bfb1b50fdac17
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,12 @@
+repos:
+  - repo: https://github.com/astral-sh/ruff-pre-commit
+    # Ruff version.
+    rev: v0.1.15
+    hooks:
+      # Run the linter.
+      - id: ruff
+        args:
+          - --fix
+          - --preview
+          - --ignore=PLR0917,PLR0914
+          - --extend-exclude=test/*
diff --git a/Changelog.md b/Changelog.md
new file mode 100644
index 0000000000000000000000000000000000000000..ee20d1f269f81ef193ecf61febbeb9810dae5bc6
--- /dev/null
+++ b/Changelog.md
@@ -0,0 +1,4 @@
+# Changelog
+
+## [0.1] - 2025-03-26
+- Initial version
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..c17bbc331235ee6e8a72cd1918482b0f032f0659
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,30 @@
+FROM python:3.12.7-alpine
+WORKDIR /app
+
+# Set environment variables for predictable Python behavior and UTF-8 encoding
+ENV PYTHONUNBUFFERED=1 \
+    PYTHONDONTWRITEBYTECODE=1 \
+    LANG=C.UTF-8 \
+    LC_ALL=C.UTF-8
+
+ARG ARTIFACT_VERSION
+
+RUN apk add --no-cache gcc libc-dev libffi-dev && \
+    addgroup -S appgroup && adduser -S appuser -G appgroup -h /app
+
+RUN pip install --no-cache-dir \
+    --pre \
+    --trusted-host 150.254.211.2 \
+    --extra-index-url https://150.254.211.2/artifactory/api/pypi/geant-swd-pypi/simple \
+    --target /app \
+    geant-capacity-planner==${ARTIFACT_VERSION}
+
+# Copy the shell scripts and ensure scripts do not have Windows line endings and make them executable
+COPY start-app.sh start-worker.sh start-scheduler.sh /app/
+RUN sed -i 's/\r$//' start-app.sh start-worker.sh start-scheduler.sh && \
+    chmod +x start-app.sh start-worker.sh start-scheduler.sh
+
+RUN chown -R appuser:appgroup /app
+USER appuser
+EXPOSE 8080
+ENTRYPOINT ["/app/start-app.sh"]
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..b47daa7307ad235eedb47d2abb39d71692ed709f
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2025 GÉANT Vereniging
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000000000000000000000000000000000000..542d08af9b2015ead823142f7ab44e7bfe86d86f
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,117 @@
+[tool.mypy]
+exclude = ["venv"]
+ignore_missing_imports = true
+disallow_untyped_calls = true
+disallow_untyped_defs = true
+disallow_incomplete_defs = true
+disallow_untyped_decorators = true
+no_implicit_optional = true
+strict_optional = true
+namespace_packages = true
+warn_unused_ignores = true
+warn_redundant_casts = true
+warn_no_return = true
+warn_unreachable = true
+implicit_reexport = false
+strict_equality = true
+show_error_codes = true
+show_column_numbers = true
+# Suppress "note: By default the bodies of untyped functions are not checked"
+disable_error_code = "annotation-unchecked"
+# Forbid the use of a generic "type: ignore" without specifying the exact error that is ignored
+enable_error_code = "ignore-without-code"
+
+[tool.ruff]
+extend-exclude = [
+    "htmlcov",
+    "docs",
+]
+target-version = "py312"
+line-length = 120
+
+[tool.ruff.lint]
+ignore = [
+    "C901",
+    "COM812",
+    "D203",
+    "D213",
+    "ISC001",
+    "N805",
+    "PLC2801",
+    "PLR0913",
+    "PLR0904",
+    "PLW1514",
+]
+select = [
+    "A",
+    "ARG",
+    "B",
+    "BLE",
+    "C",
+    "COM",
+    "C4",
+    "C90",
+    "D",
+    "DTZ",
+    "E",
+    "EM",
+    "ERA",
+    "F",
+    "FA",
+    "FBT",
+    "FLY",
+    "FURB",
+    "G",
+    "I",
+    "ICN",
+    "INP",
+    "ISC",
+    "LOG",
+    "N",
+    "PERF",
+    "PGH",
+    "PIE",
+    "PL",
+    "PT",
+    "PTH",
+    "PYI",
+    "Q",
+    "RET",
+    "R",
+    "RET",
+    "RSE",
+    "RUF",
+    "S",
+    "SIM",
+    "SLF",
+    "T",
+    "T20",
+    "TID",
+    "TRY",
+    "UP",
+    "W",
+    "YTT"
+]
+
+[tool.ruff.lint.pydocstyle]
+convention = "google"
+
+[tool.ruff.lint.flake8-tidy-imports]
+ban-relative-imports = "all"
+
+[tool.ruff.lint.per-file-ignores]
+"setup.py" = ["D100"]
+
+[tool.ruff.lint.isort]
+known-third-party = ["pydantic"]
+known-first-party = ["test", "docs"]
+
+[tool.pytest.ini_options]
+markers = [
+    "noautofixt"
+]
+filterwarnings = [
+    "ignore",
+    "default:::capacity-planner",
+]
+asyncio_default_fixture_loop_scope = "function"
diff --git a/requirements.txt b/requirements.txt
index 1d4f386a11fd36464d1ce39960fc0fa540f5af0b..d3be9240cd5947e1dc9d7e958a65383313fd0313 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -12,3 +12,7 @@ six==1.17.0
 tabulate==0.9.0
 tzdata==2025.1
 urllib3==2.3.0
+
+#  Development requirements
+ruff==0.11.2
+mypy==1.15.0
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000000000000000000000000000000000000..4933ffc12f944676563290b01f655b3268eb9351
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,30 @@
+"""Setup script for the GÉANT Capacity Planner."""
+
+from setuptools import find_packages, setup
+
+setup(
+    name="geant-capacity-planner",
+    version="0.1",
+    author="GÉANT Vereniging",
+    author_email="goat@geant.org",
+    description="GÉANT Capacity Planner",
+    url="https://gitlab.software.geant.org/karel.vanklink/daniel",
+    packages=find_packages(),
+    install_requires=[
+        "certifi==2024.12.14",
+        "charset-normalizer == 3.4.1",
+        "idna==3.10",
+        "networkx==3.4.2",
+        "numpy==2.2.2",
+        "pandas==2.2.3",
+        "python-dateutil==2.9.0.post0",
+        "pytz==2025.1",
+        "requests==2.32.3",
+        "setuptools==75.8.0",
+        "six==1.17.0",
+        "tabulate==0.9.0",
+        "tzdata==2025.1",
+        "urllib3==2.3.0",
+    ],
+    include_package_data=True,
+)
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000000000000000000000000000000000000..85de47d9d67482c75ab81a7f0723035d1d98d6ce
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,23 @@
+[tox]
+envlist = py312
+
+[pytest]
+markers = "workflow,noautofixt"
+filterwarnings = "ignore,default:::capacity-planner"
+
+[testenv]
+passenv = SKIP_ALL_TESTS
+setenv =
+    TESTING=true
+deps =
+    pytest-cov
+    -r requirements.txt
+
+commands =
+    ruff check --respect-gitignore --preview .
+    ruff format --respect-gitignore --preview --check .
+    mypy .
+    sh -c 'if [ $SKIP_ALL_TESTS = 1 ]; then echo "Skipping coverage report"; else pytest --cov=gso --cov-report=xml --cov-report=html --cov-fail-under=90 -n auto {posargs}; fi'
+
+allowlist_externals =
+    sh