diff --git a/.flake8 b/.flake8
new file mode 100644
index 0000000..63d64f0
--- /dev/null
+++ b/.flake8
@@ -0,0 +1,9 @@
+# Taken directly from https://github.com/ambv/black/blob/master/.flake8
+[flake8]
+ignore = E203, E266, E501, W503, C901, D104, D100
+max-line-length = 88
+max-complexity = 18
+select = B,C,E,F,W,T4,B9
+per-file-ignores =
+    tests/**:D101,D102,D103
+docstring-convention = numpy
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
new file mode 100644
index 0000000..08f4e4f
--- /dev/null
+++ b/.github/CODEOWNERS
@@ -0,0 +1 @@
+* @betatim @xhochy
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..175844a
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,10 @@
+version: 2
+updates:
+  - package-ecosystem: github-actions
+    directory: /
+    schedule:
+      interval: weekly
+    groups:
+      dependencies:
+        patterns:
+          - "*"
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
new file mode 100644
index 0000000..b075c2a
--- /dev/null
+++ b/.github/workflows/ci.yaml
@@ -0,0 +1,30 @@
+name: Build and upload to PyPI
+on: 
+ push:
+ pull_request:
+ release:
+   types: [published]
+
+jobs:
+  build_artifacts:
+    name: Build artifacts
+    runs-on: ubuntu-latest
+    strategy:
+      fail-fast: true
+    steps:
+      - uses: actions/checkout@v4
+      - name: Fetch full git history
+        run: git fetch --prune --unshallow
+      - name: Set up Conda env
+        uses: mamba-org/setup-micromamba@0dea6379afdaffa5d528b3d1dabc45da37f443fc
+        with:
+          environment-file: environment.yml
+          cache-environment: true
+      - shell: bash -el {0}
+        run:
+          python -m build
+      - uses: pypa/gh-action-pypi-publish@v1.12.3
+        if: github.event_name == 'release'
+        with:
+          user: __token__
+          password: ${{ secrets.PYPI_RELEASE_TOKEN }}
diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml
new file mode 100644
index 0000000..8304ce1
--- /dev/null
+++ b/.github/workflows/pre-commit.yml
@@ -0,0 +1,32 @@
+name: CI
+on: [push, pull_request]
+
+# Automatically stop old builds on the same branch/PR
+concurrency:
+  group: ${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: true
+
+defaults:
+  run:
+    shell: bash -el {0}
+
+jobs:
+  pre-commit-checks:
+    name: "Linux - pre-commit checks - Python 3.10"
+    timeout-minutes: 30
+    runs-on: ubuntu-latest
+    env:
+      PRE_COMMIT_USE_MICROMAMBA: 1
+    steps:
+      - name: Checkout branch
+        uses: actions/checkout@v4
+      - name: Set up micromamba
+        uses: mamba-org/setup-micromamba@0dea6379afdaffa5d528b3d1dabc45da37f443fc
+      - name: Add micromamba to GITHUB_PATH
+        run: echo "${HOME}/micromamba-bin" >> "$GITHUB_PATH"
+      - name: Install Python 3.10
+        uses: actions/setup-python@v5
+        with:
+          python-version: "3.10"
+      - name: Run pre-commit checks
+        uses: pre-commit/action@v3.0.1
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..394e28d
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,35 @@
+repos:
+ - repo: https://github.com/Quantco/pre-commit-mirrors-black
+   rev: 23.7.0
+   hooks:
+     - id: black-conda
+       args:
+         - --safe
+         - --target-version=py38
+ - repo: https://github.com/Quantco/pre-commit-mirrors-flake8
+   rev: 6.1.0
+   hooks:
+    - id: flake8-conda
+ - repo: https://github.com/Quantco/pre-commit-mirrors-isort
+   rev: 5.12.0
+   hooks:
+    - id: isort-conda
+      additional_dependencies: [-c, conda-forge, toml=0.10.2]
+ - repo: https://github.com/Quantco/pre-commit-mirrors-mypy
+   rev: "1.5.1"
+   hooks:
+    - id: mypy-conda
+      additional_dependencies:
+        - -c
+        - conda-forge
+        - types-mock
+        - types-setuptools
+        - types-redis
+        - types-boto
+        - boto3-stubs
+ - repo: https://github.com/Quantco/pre-commit-mirrors-pyupgrade
+   rev: 3.10.1
+   hooks:
+    - id: pyupgrade-conda
+      args:
+        - --py38-plus
diff --git a/README.md b/README.md
index 554bfb0..c6a7641 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
 # VS Code on Binder
 
 [![PyPI](https://img.shields.io/pypi/v/jupyter-vscode-proxy)](https://pypi.org/project/jupyter-vscode-proxy/)
-[![Install with conda](https://anaconda.org/conda-forge/jupyter-vscode-proxy/badges/installer/conda.svg)](https://github.com/conda-forge/jupyter-vscode-proxy-feedstock)
+[![Install with conda](https://anaconda.org/conda-forge/jupyter-vscode-proxy/badges/version.svg)](https://github.com/conda-forge/jupyter-vscode-proxy-feedstock)
 
 VS Code on Binder, because sometimes you need a real editor.
 
diff --git a/environment.yml b/environment.yml
index bc2c718..7ea9cee 100644
--- a/environment.yml
+++ b/environment.yml
@@ -1,6 +1,8 @@
+name: vscode-binder
 channels:
   - conda-forge
 dependencies:
   - numpy
+  - python-build
   - jupyter-server-proxy
   - code-server >=3.2
diff --git a/jupyter_vscode_proxy/__init__.py b/jupyter_vscode_proxy/__init__.py
index bc78f26..eb5ae43 100644
--- a/jupyter_vscode_proxy/__init__.py
+++ b/jupyter_vscode_proxy/__init__.py
@@ -1,46 +1,75 @@
 import os
 import shutil
+from typing import Any, Callable, Dict, List
 
 
-def setup_vscode():
-    def _get_vscode_cmd(port):
-        executable = "code-server"
+def _get_inner_vscode_cmd() -> List[str]:
+    return [
+        "code-server",
+        "--auth",
+        "none",
+        "--disable-telemetry",
+    ]
+
+
+def _get_inner_openvscode_cmd() -> List[str]:
+    return [
+        "openvscode-server",
+        "--without-connection-token",
+        "--telemetry-level",
+        "off",
+    ]
+
+
+_CODE_EXECUTABLE_INNER_CMD_MAP: Dict[str, Callable] = {
+    "code-server": _get_inner_vscode_cmd,
+    "openvscode-server": _get_inner_openvscode_cmd,
+}
+
+
+def _get_cmd_factory(executable: str) -> Callable:
+    if executable not in _CODE_EXECUTABLE_INNER_CMD_MAP:
+        raise KeyError(
+            f"'{executable}' is not one of {_CODE_EXECUTABLE_INNER_CMD_MAP.keys()}."
+        )
+
+    get_inner_cmd = _CODE_EXECUTABLE_INNER_CMD_MAP[executable]
+
+    def _get_cmd(port: int) -> List[str]:
         if not shutil.which(executable):
-            raise FileNotFoundError("Can not find code-server in PATH")
-        
+            raise FileNotFoundError(f"Can not find {executable} in PATH")
+
         # Start vscode in CODE_WORKINGDIR env variable if set
         # If not, start in 'current directory', which is $REPO_DIR in mybinder
         # but /home/jovyan (or equivalent) in JupyterHubs
         working_dir = os.getenv("CODE_WORKINGDIR", ".")
 
         extensions_dir = os.getenv("CODE_EXTENSIONSDIR", None)
-        extra_extensions_dir = os.getenv("CODE_EXTRA_EXTENSIONSDIR", None)
 
-        cmd = [
-            executable,
-            "--auth",
-            "none",
-            "--disable-telemetry",
-            "--port=" + str(port),
-        ]
+        cmd = get_inner_cmd()
+
+        cmd.append("--port=" + str(port))
 
         if extensions_dir:
             cmd += ["--extensions-dir", extensions_dir]
 
-        if extra_extensions_dir:
-            cmd += ["--extra-extensions-dir", extra_extensions_dir]
-
         cmd.append(working_dir)
         return cmd
 
+    return _get_cmd
+
+
+def setup_vscode() -> Dict[str, Any]:
+    executable = os.environ.get("CODE_EXECUTABLE", "code-server")
+    icon = "code-server.svg" if executable == "code-server" else "vscode.svg"
     return {
-        "command": _get_vscode_cmd,
-        "timeout": 20,
+        "command": _get_cmd_factory(executable),
+        "timeout": 300,
         "new_browser_tab": True,
         "launcher_entry": {
             "title": "VS Code",
             "icon_path": os.path.join(
-                os.path.dirname(os.path.abspath(__file__)), "icons", "vscode.svg"
+                os.path.dirname(os.path.abspath(__file__)), "icons", icon
             ),
         },
     }
diff --git a/jupyter_vscode_proxy/icons/code-server.svg b/jupyter_vscode_proxy/icons/code-server.svg
new file mode 100644
index 0000000..85b701d
--- /dev/null
+++ b/jupyter_vscode_proxy/icons/code-server.svg
@@ -0,0 +1,9 @@
+<svg width="64" viewBox="0 0 2250 2250" version="1.1" xmlns="http://www.w3.org/2000/svg" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
+<g class="jp-icon0" fill="#000" style="fill-rule:nonzero;">
+  <path d="M1991.66,1034.72c-38.493,0 -64.144,-22.57 -64.144,-68.897l-0,-266.084c-0,-169.867 -69.982,-263.709 -250.762,-263.709l-83.976,0l-0,179.368l25.661,0c71.144,0 104.967,39.201 104.967,109.285l0,235.201c0,102.156 30.324,143.733 96.806,165.114c-66.482,20.196 -96.806,62.958 -96.806,165.114l0,174.621c0,48.7 0,96.216 -12.829,144.917c-12.829,45.141 -33.823,87.903 -62.98,124.726c-16.329,21.386 -34.991,39.202 -55.981,55.835l-0,23.755l83.971,-0c180.781,-0 250.763,-93.843 250.763,-263.709l-0,-266.084c-0,-47.516 24.485,-68.897 64.144,-68.897l47.822,-0l-0,-179.37l-46.656,-0l0,-1.186Z"/>
+  <path d="M1420.16,706.904l-258.923,0c-5.833,0 -10.495,-4.752 -10.495,-10.691l-0,-20.192c-0,-5.941 4.662,-10.692 10.495,-10.692l260.089,0c5.83,0 10.495,4.751 10.495,10.692l0,20.192c0,5.939 -5.833,10.691 -11.661,10.691Z" />
+  <path d="M1464.48,963.474l-188.942,0c-5.833,0 -10.501,-4.754 -10.501,-10.693l0,-20.192c0,-5.938 4.668,-10.691 10.501,-10.691l188.942,-0c5.833,-0 10.495,4.753 10.495,10.691l-0,20.192c-0,4.754 -4.662,10.693 -10.495,10.693Z"/>
+  <path d="M1539.12,835.188l-377.885,0c-5.833,0 -10.495,-4.75 -10.495,-10.689l-0,-20.196c-0,-5.939 4.662,-10.69 10.495,-10.69l376.719,0c5.833,0 10.499,4.751 10.499,10.69l-0,20.196c-0,4.75 -3.5,10.689 -9.333,10.689Z"/>
+  <path d="M861.493,765.074c25.658,0 51.319,2.376 75.811,8.316l0,-48.705c0,-68.897 34.989,-109.285 104.971,-109.285l25.658,0l-0,-179.368l-83.977,0c-180.781,0 -250.758,93.842 -250.758,263.709l0,87.901c40.819,-14.252 83.977,-22.568 128.295,-22.568Z"/>
+  <path d="M1618.44,1411.25c-18.662,-150.861 -132.962,-276.776 -279.919,-305.285c-40.818,-8.314 -81.642,-9.504 -121.295,-2.376c-1.166,-0 -1.166,-1.189 -2.332,-1.189c-64.148,-136.605 -201.772,-226.884 -351.063,-226.884c-149.289,-0 -285.747,87.905 -351.062,224.51c-1.166,-0 -1.166,1.188 -2.332,1.188c-41.987,-4.753 -83.975,-2.379 -125.963,8.314c-144.623,35.634 -254.257,159.175 -274.085,308.847c-2.332,15.441 -3.499,30.883 -3.499,45.141c0,45.136 30.325,86.713 74.645,92.652c54.817,8.317 102.636,-34.448 101.469,-89.089c0,-8.317 0,-17.821 1.167,-26.134c9.331,-76.025 66.48,-140.168 141.123,-157.99c23.328,-5.939 46.654,-7.124 68.814,-3.559c71.146,9.502 141.124,-27.324 171.449,-91.467c22.162,-47.516 57.151,-89.094 103.804,-111.664c51.314,-24.946 109.633,-28.506 163.286,-9.499c55.979,20.192 97.966,62.954 123.627,116.409c26.824,52.27 39.653,89.093 96.805,96.221c23.325,3.559 88.639,2.374 113.132,1.185c47.82,0 95.64,16.631 129.463,51.079c22.156,23.757 38.485,53.455 45.486,86.715c10.495,53.455 -2.334,106.908 -33.825,147.296c-22.162,28.509 -52.485,49.89 -86.308,59.394c-16.329,4.754 -32.657,5.939 -48.986,5.939l-257.757,0c-51.314,0 -92.138,-41.573 -92.138,-93.842l0,-348.049c0,-14.251 -11.661,-26.13 -25.658,-26.13l-36.156,0c-71.148,1.185 -128.295,81.964 -128.295,167.488l-0,312.415c-0,92.652 73.476,167.488 164.451,167.488c0,0 404.714,-1.19 410.544,-1.19c93.304,-9.503 179.614,-58.204 237.927,-133.04c58.319,-72.46 85.142,-167.492 73.481,-264.894Z"/>
+</g></svg>
\ No newline at end of file
diff --git a/postBuild b/postBuild
index ff03503..542f613 100644
--- a/postBuild
+++ b/postBuild
@@ -1,10 +1,5 @@
 #!/bin/bash
 
-# Enable the proxy extension in notebook and lab
-jupyter serverextension enable --py jupyter_server_proxy
-jupyter labextension install @jupyterlab/server-proxy
-jupyter lab build
-
 code-server --install-extension ms-python.python
 
 # Install the VS code proxy
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..32f5676
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,53 @@
+[build-system]
+requires = ["setuptools", "setuptools-scm", "wheel"]
+
+[tool.setuptools_scm]
+version_scheme = "post-release"
+
+[project]
+name = "jupyter_vscode_proxy"
+description = "VS Code extension for Jupyter"
+readme = "README.md"
+dynamic = ["version"]
+authors = [
+  {name = "Tim Head", email = "noreply@example.com"},
+]
+classifiers = [
+  "Programming Language :: Python :: 3",
+  "Programming Language :: Python :: 3.8",
+  "Programming Language :: Python :: 3.9",
+  "Programming Language :: Python :: 3.10",
+  "Programming Language :: Python :: 3.11",
+]
+requires-python = ">=3.8"
+license = {text = "BSD-3-clause"}
+
+[project.urls]
+repository = "https://github.com/betatim/vscode-binder"
+
+[project.entry-points.jupyter_serverproxy_servers]
+vscode = "jupyter_vscode_proxy:setup_vscode"
+
+[tool.black]
+exclude = '''
+/(
+    \.eggs
+  | \.git
+  | \.venv
+  | build
+  | dist
+)/
+'''
+
+[tool.isort]
+profile = "black"
+line_length = 88
+known_first_party = "{{ project_slug }}"
+skip_glob = '\.eggs/*,\.git/*,\.venv/*,build/*,dist/*'
+default_section = 'THIRDPARTY'
+
+[tool.mypy]
+python_version = '3.8'
+ignore_missing_imports = true
+no_implicit_optional = true
+check_untyped_defs = true
diff --git a/setup.cfg b/setup.cfg
deleted file mode 100644
index 8183238..0000000
--- a/setup.cfg
+++ /dev/null
@@ -1,2 +0,0 @@
-[metadata]
-license_files = LICENSE
diff --git a/setup.py b/setup.py
deleted file mode 100644
index f51d1b7..0000000
--- a/setup.py
+++ /dev/null
@@ -1,31 +0,0 @@
-import setuptools
-
-
-with open("README.md", encoding="utf8") as f:
-    readme = f.read()
-
-
-setuptools.setup(
-    name="jupyter-vscode-proxy",
-    version="0.1",
-    url="https://github.com/betatim/vscode-binder",
-    author="Tim Head",
-    license="BSD",
-    description="VS Code extension for Jupyter",
-    long_description=readme,
-    long_description_content_type="text/markdown",
-    packages=setuptools.find_packages(),
-    keywords=["Jupyter", "vscode", "vs code", "editor"],
-    classifiers=["Framework :: Jupyter"],
-    install_requires=[
-        'jupyter-server-proxy'
-    ],
-    entry_points={
-        "jupyter_serverproxy_servers": ["vscode = jupyter_vscode_proxy:setup_vscode",]
-    },
-    package_data={"jupyter_vscode_proxy": ["icons/*"]},
-    project_urls={
-        "Source": "https://github.com/betatim/vscode-binder/",
-        "Tracker": "https://github.com/betatim/vscode-binder/issues",
-    },
-)