diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000000000000000000000000000000000000..2ec5b34d15a65156b1ff41451d54c2e77a9e9d33
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,88 @@
+[tool.isort]
+profile = "black"
+line_length = 120
+skip = ["venv", ".tox", "gso/migrations"]
+known_third_party = ["pydantic", "migrations"]
+known_first_party = ["test"]
+
+[tool.black]
+line-length = 120
+target-version = ["py310"]
+
+[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"
+
+[tool.ruff]
+exclude = [
+    ".git",
+    ".*_cache",
+    ".tox",
+    "*.egg-info",
+    "__pycache__",
+    "htmlcov",
+    "venv",
+]
+ignore = [
+    "C417",
+    "D100",
+    "D101",
+    "D102",
+    "D103",
+    "D104",
+    "D105",
+    "D106",
+    "D107",
+    "D202",
+    "E501",
+    "N806",
+    "B905",
+    "N805",
+    "B904",
+    "N803",
+    "N801",
+    "N815",
+    "N802"
+]
+line-length = 120
+select = [
+    "B",
+    "C",
+    "D",
+    "E",
+    "F",
+    "I",
+    "N",
+    "RET",
+    "S",
+    "T",
+    "W",
+]
+target-version = "py310"
+
+[tool.ruff.flake8-tidy-imports]
+ban-relative-imports = "all"
+
+[tool.ruff.per-file-ignores]
+"test/*" = ["S101", "B033", "N816", "N802"]
+
+[tool.ruff.isort]
+known-third-party = ["pydantic"]
+known-first-party = ["migrations", "test"]
diff --git a/requirements.txt b/requirements.txt
index 0e982e1ee54d59dc0d981cc9b1f833f501d1eed1..2ba2124901876fa9d668c8d9f59e453c8d03771e 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -8,3 +8,4 @@ black
 isort
 flake8
 mypy
+ruff
diff --git a/tox.ini b/tox.ini
index dd02509de947f75dab25e672ed28a3801dc3b28d..2d4a26e64267682720a9316185439fac62b26fbb 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,10 +1,24 @@
 [flake8]
-exclude = venv,.tox, migrations
+ignore = D100,D101,D102,D103,D104,D105,D106,D107,D202,E501,RST301,RST304,W503,E203,C417,T202
+; extend-ignore = E203
+exclude = .git,.*_cache,.eggs,*.egg-info,__pycache__,venv,.tox,gso/migrations
+enable-extensions = G
+select = B,C,D,E,F,G,I,N,S,T,W,B902,B903,R
+max-line-length = 120
+ban-relative-imports = true
+per-file-ignores =
+	# Allow first argument to be cls instead of self for pydantic validators
+	gso/*: B902
+	test/*: S101
 
 [testenv]
 deps =
     coverage
     flake8
+    black
+    mypy
+    ruff
+    isort
     -r requirements.txt
 
 commands =
@@ -14,4 +28,8 @@ commands =
     coverage html
     # coverage report --fail-under 80
     coverage report
+    isort -c .
+    ruff .
+    black --check .
+    mypy .
     flake8