diff --git a/.envrc b/.envrc
new file mode 100644
index 0000000000000000000000000000000000000000..3550a30f2de389e537ee40ca5e64a77dc185c79b
--- /dev/null
+++ b/.envrc
@@ -0,0 +1 @@
+use flake
diff --git a/.gitignore b/.gitignore
index 559b58626e1d6044e0cef1bff755f767570da0fd..dc07e918abc86d9c40b8848df93284c25483ae03 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,3 +18,9 @@ __pycache__
 
 # sphinx
 docs/_build/
+
+# direnv
+.direnv
+
+# nix
+result*
diff --git a/docs/badge/firmware-development.rst b/docs/badge/firmware-development.rst
index dc64da735e0c10fe121330327dd517447293099b..e19477095a17b2c15ba4bce4b76c45300bd0b3c8 100644
--- a/docs/badge/firmware-development.rst
+++ b/docs/badge/firmware-development.rst
@@ -34,7 +34,7 @@ If you've already cloned without ``--recursive`` you can update your submodules
 Dependencies
 ------------
 
-If you're using Nix(OS), just run ``nix-shell nix/shell.nix``.
+If you're using Nix(OS), just run ``nix-shell nix/shell.nix``. There is also a flake available for ``nix develop``.
 
 On other Linux-based distributions, you will have to manually install ESP-IDF alongside our custom patches (note that install.sh installs stuff to your $HOME so that you may want to use a container or nix):
 
diff --git a/flake.lock b/flake.lock
new file mode 100644
index 0000000000000000000000000000000000000000..be862d336ee7aab6fd8d22a2c75c604b73bd4060
--- /dev/null
+++ b/flake.lock
@@ -0,0 +1,44 @@
+{
+  "nodes": {
+    "flake-compat": {
+      "flake": false,
+      "locked": {
+        "lastModified": 1673956053,
+        "narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=",
+        "owner": "edolstra",
+        "repo": "flake-compat",
+        "rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9",
+        "type": "github"
+      },
+      "original": {
+        "owner": "edolstra",
+        "repo": "flake-compat",
+        "type": "github"
+      }
+    },
+    "nixpkgs": {
+      "locked": {
+        "lastModified": 1692128808,
+        "narHash": "sha256-Di1Zm/P042NuwThMiZNrtmaAjd4Tm2qBOKHX7xUOfMk=",
+        "owner": "NixOS",
+        "repo": "nixpkgs",
+        "rev": "4ed9856be002a730234a1a1ed9dcd9dd10cbdb40",
+        "type": "github"
+      },
+      "original": {
+        "owner": "NixOS",
+        "ref": "nixpkgs-unstable",
+        "repo": "nixpkgs",
+        "type": "github"
+      }
+    },
+    "root": {
+      "inputs": {
+        "flake-compat": "flake-compat",
+        "nixpkgs": "nixpkgs"
+      }
+    }
+  },
+  "root": "root",
+  "version": 7
+}
diff --git a/flake.nix b/flake.nix
new file mode 100644
index 0000000000000000000000000000000000000000..e206d713920bcf261445393bc6d02f2382e9e1fb
--- /dev/null
+++ b/flake.nix
@@ -0,0 +1,123 @@
+{
+  description = "flow3r badge flake";
+
+  inputs = {
+    nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
+    flake-compat = {
+      url = "github:edolstra/flake-compat";
+      flake = false;
+    };
+  };
+
+  outputs =
+    { self
+    , nixpkgs
+    , ...
+    }:
+    let
+      supportedSystems = [ /* "aarch64-linux" */ "x86_64-linux" ];
+      pkgsFor = system:
+        nixpkgs.legacyPackages.${system}.extend self.overlays.default;
+      forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
+      forAllPkgs = f: forAllSystems (system: f (pkgsFor system));
+
+      # All packages require to build/lint the project.
+      fwbuild = pkgs: with pkgs; [
+        gcc-xtensa-esp32s3-elf-bin
+        esp-idf
+        esp-llvm
+        esptool
+        run-clang-tidy
+
+        git
+        wget
+        gnumake
+        flex
+        bison
+        gperf
+        pkgconfig
+        gnutar
+        curl
+        bzip2
+        gcc
+        gnused
+        findutils
+        gnugrep
+
+        cmake
+        ninja
+
+        python3Packages.sphinx
+        python3Packages.sphinx_rtd_theme
+        python3Packages.black
+        mypy
+      ];
+      fwdev = pkgs: (fwbuild pkgs) ++ (with pkgs; [
+        openocd-esp32-bin
+        python3Packages.pygame
+        python3Packages.wasmer
+        python3Packages.wasmer-compiler-cranelift
+        emscripten
+        ncurses5
+        esp-gdb
+        mpremote
+      ]);
+    in
+    {
+      overlays.default = import ./nix/overlay;
+
+      packages = forAllPkgs (pkgs:
+        {
+          dockerImage = pkgs.dockerTools.buildImage {
+            name = "flow3r-build";
+            copyToRoot = pkgs.buildEnv {
+              name = "flow3r-build-root";
+              paths = with pkgs; [
+                # interactive shell
+                bashInteractive
+                coreutils-full
+                cacert
+              ] ++ fwbuild pkgs;
+              pathsToLink = [ "/bin" ];
+            };
+
+            runAsRoot = ''
+              #!${pkgs.runtimeShell}
+              mkdir -p /tmp
+            '';
+
+            config = {
+              Env = [
+                "PATH=/bin:${pkgs.esp-idf}/tools"
+                "PYTHONPATH=${pkgs.python3.pkgs.makePythonPath pkgs.esp-idf.propagatedBuildInputs}"
+                "IDF_PATH=${pkgs.esp-idf}"
+                "IDF_COMPONENT_MANAGER=0"
+                "TMPDIR=/tmp"
+                "NIX_SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"
+              ];
+            };
+          };
+        });
+
+      devShells = forAllPkgs (pkgs:
+        {
+          default = pkgs.mkShell {
+            name = "flow3r-shell";
+            buildInputs = (fwdev pkgs) ++ (with pkgs; [
+              micropython
+            ]);
+            shellHook = ''
+              # For esp.py openocd integration.
+              export OPENOCD_SCRIPTS="${pkgs.openocd-esp32-bin}/share/openocd/scripts"
+
+              # Some nice-to-have defaults.
+              export ESPPORT=/dev/ttyACM0
+              export OPENOCD_COMMANDS="-f board/esp32s3-builtin.cfg"
+
+              # openocd wants udev
+              export LD_LIBRARY_PATH="${pkgs.systemd}/lib:$LD_LIBRARY_PATH"
+            '';
+          };
+        });
+    };
+}
diff --git a/nix/docker-image.nix b/nix/docker-image.nix
deleted file mode 100644
index 37494c62a15d0927619a6b958fefd2ce8265d62e..0000000000000000000000000000000000000000
--- a/nix/docker-image.nix
+++ /dev/null
@@ -1,55 +0,0 @@
-with import ./pkgs.nix;
-
-pkgs.dockerTools.buildImage {
-  name = "registry.k0.hswaw.net/q3k/flow3r-build";
-  copyToRoot = pkgs.buildEnv {
-    name = "image-root";
-    paths = with pkgs; [
-      # interactive shell
-      bashInteractive
-      coreutils-full
-
-      # esp crud
-      esp-idf
-      esptool
-      run-clang-tidy
-      gcc-xtensa-esp32s3-elf-bin
-      # esp-llvm goes into PATH because it has conflicting binary names.
-
-      mypy
-
-      (python3.withPackages (ps: with ps; [
-        sphinx sphinx_rtd_theme
-        black
-        
-        # simulator deps
-        pygame wasmer
-        wasmer-compiler-cranelift
-      ]))
-
-      # random build tools
-      gcc gnused findutils gnugrep
-      git wget gnumake
-      cmake ninja pkgconfig
-      gnutar curl bzip2
-      cacert
-    ];
-    pathsToLink = [ "/bin" ];
-  };
-
-  runAsRoot = ''
-    #!${pkgs.runtimeShell}
-    mkdir -p /tmp
-  '';
-
-  config = {
-    Env = [
-      "PATH=/bin:${pkgs.esp-idf}/tools:${pkgs.esp-llvm}/bin"
-      "PYTHONPATH=${pkgs.python3.pkgs.makePythonPath pkgs.esp-idf.propagatedBuildInputs}"
-      "IDF_PATH=${pkgs.esp-idf}"
-      "IDF_COMPONENT_MANAGER=0"
-      "TMPDIR=/tmp"
-      "NIX_SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"
-    ];
-  };
-}
diff --git a/nix/overlay/default.nix b/nix/overlay/default.nix
index 93eb8413b298806c9990a0cef1aef7951cb74d1a..676537c0e939efa375dadb24f7f252785003943d 100644
--- a/nix/overlay/default.nix
+++ b/nix/overlay/default.nix
@@ -1,9 +1,9 @@
-(self: super: {
-  gcc-xtensa-esp32s3-elf-bin = super.callPackage ./esp32s3-toolchain-bin.nix {};
-  openocd-esp32-bin = super.callPackage ./openocd-esp32-bin.nix {};
-  esp-idf = super.callPackage ./esp-idf {};
-  esp-llvm = super.callPackage ./esp-llvm.nix {};
-  esp-gdb = super.callPackage ./esp-gdb.nix {};
-  run-clang-tidy = super.callPackage ./run-clang-tidy {};
-  mpremote = super.python310Packages.callPackage ./mpremote {};
+(final: prev: {
+  gcc-xtensa-esp32s3-elf-bin = final.callPackage ./esp32s3-toolchain-bin.nix {};
+  openocd-esp32-bin = final.callPackage ./openocd-esp32-bin.nix {};
+  esp-idf = final.callPackage ./esp-idf {};
+  esp-llvm = final.callPackage ./esp-llvm.nix {};
+  esp-gdb = final.callPackage ./esp-gdb.nix {};
+  run-clang-tidy = final.callPackage ./run-clang-tidy {};
+  mpremote = final.python310Packages.callPackage ./mpremote {};
 })
diff --git a/nix/overlay/esp-llvm.nix b/nix/overlay/esp-llvm.nix
index 7434cdb36af0aaf5a4093cf9ccfd188fb1eea506..49879640e638e6e18098d49e76c7dc9e11d56daf 100644
--- a/nix/overlay/esp-llvm.nix
+++ b/nix/overlay/esp-llvm.nix
@@ -45,6 +45,7 @@ stdenv.mkDerivation rec {
     description = "Espressif LLVM/Clang fork binary build";
     homepage = "https://docs.espressif.com/projects/esp-idf/en/stable/get-started/linux-setup.html";
     license = licenses.gpl3;
+    priority = -23;
   };
 }
 
diff --git a/nix/pkgs.nix b/nix/pkgs.nix
deleted file mode 100644
index d4fd1febf876d86dfa459a7828c4883c1fcae1f6..0000000000000000000000000000000000000000
--- a/nix/pkgs.nix
+++ /dev/null
@@ -1,40 +0,0 @@
-let
-  sources = import ./sources.nix;
-  nixpkgs = import sources.nixpkgs {
-    overlays = [
-      (import ./overlay)
-    ];
-  };
-
-in with nixpkgs; rec {
-  # nixpkgs passthrough
-  inherit (nixpkgs) pkgs lib;
-  # All packages require to build/lint the project.
-  fwbuild = [
-    gcc-xtensa-esp32s3-elf-bin
-    esp-idf
-    esp-llvm
-    esptool
-    run-clang-tidy
-
-    git wget gnumake
-    flex bison gperf pkgconfig
-
-    cmake ninja
-
-    python3Packages.sphinx
-    python3Packages.sphinx_rtd_theme
-    python3Packages.black
-    mypy
-  ];
-  fwdev = fwbuild ++ [
-    openocd-esp32-bin
-    python3Packages.pygame
-    python3Packages.wasmer
-    python3Packages.wasmer-compiler-cranelift
-    emscripten
-    ncurses5
-    esp-gdb
-    mpremote
-  ];
-}
diff --git a/nix/shell.nix b/nix/shell.nix
index 18177407b99571371e734161791278b669dc4361..9699c1c669d8a428fe5400245e414cc019805d50 100644
--- a/nix/shell.nix
+++ b/nix/shell.nix
@@ -1,16 +1,10 @@
-with import ./pkgs.nix;
-pkgs.mkShell {
-  name = "flow3r-shell";
-  buildInputs = fwdev;
-  shellHook = ''
-    # For esp.py openocd integration.
-    export OPENOCD_SCRIPTS="${pkgs.openocd-esp32-bin}/share/openocd/scripts"
-
-    # Some nice-to-have defaults.
-    export ESPPORT=/dev/ttyACM0
-    export OPENOCD_COMMANDS="-f board/esp32s3-builtin.cfg"
-
-    # openocd wants udev
-    export LD_LIBRARY_PATH="${pkgs.systemd}/lib:$LD_LIBRARY_PATH"
-  '';
-}
+(import
+  (
+    let lock = builtins.fromJSON (builtins.readFile ../flake.lock); in
+    fetchTarball {
+      url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz";
+      sha256 = lock.nodes.flake-compat.locked.narHash;
+    }
+  )
+  { src = ../.; }
+).shellNix
diff --git a/nix/sources.json b/nix/sources.json
deleted file mode 100644
index b64d0d8da00afe718bf313a7933565a9f0365d1e..0000000000000000000000000000000000000000
--- a/nix/sources.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
-    "nixpkgs": {
-        "branch": "master",
-        "description": "Nix Packages collection",
-        "homepage": "",
-        "owner": "NixOS",
-        "repo": "nixpkgs",
-        "rev": "68b3d3225096da1bc66ba8e65f69fa8d19248358",
-        "sha256": "0ccljssfq473pf5dhy1zcf7gb2adapgj9jcbkk1mmn9g025533wn",
-        "type": "tarball",
-        "url": "https://github.com/NixOS/nixpkgs/archive/68b3d3225096da1bc66ba8e65f69fa8d19248358.tar.gz",
-        "url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
-    }
-}
diff --git a/nix/sources.nix b/nix/sources.nix
deleted file mode 100644
index fe3dadf7ebb1da68bff868e5ca95a2eaae1ddc9c..0000000000000000000000000000000000000000
--- a/nix/sources.nix
+++ /dev/null
@@ -1,198 +0,0 @@
-# This file has been generated by Niv.
-
-let
-
-  #
-  # The fetchers. fetch_<type> fetches specs of type <type>.
-  #
-
-  fetch_file = pkgs: name: spec:
-    let
-      name' = sanitizeName name + "-src";
-    in
-    if spec.builtin or true then
-      builtins_fetchurl { inherit (spec) url sha256; name = name'; }
-    else
-      pkgs.fetchurl { inherit (spec) url sha256; name = name'; };
-
-  fetch_tarball = pkgs: name: spec:
-    let
-      name' = sanitizeName name + "-src";
-    in
-    if spec.builtin or true then
-      builtins_fetchTarball { name = name'; inherit (spec) url sha256; }
-    else
-      pkgs.fetchzip { name = name'; inherit (spec) url sha256; };
-
-  fetch_git = name: spec:
-    let
-      ref =
-        spec.ref or (
-          if spec ? branch then "refs/heads/${spec.branch}" else
-          if spec ? tag then "refs/tags/${spec.tag}" else
-          abort "In git source '${name}': Please specify `ref`, `tag` or `branch`!"
-        );
-      submodules = spec.submodules or false;
-      submoduleArg =
-        let
-          nixSupportsSubmodules = builtins.compareVersions builtins.nixVersion "2.4" >= 0;
-          emptyArgWithWarning =
-            if submodules
-            then
-              builtins.trace
-                (
-                  "The niv input \"${name}\" uses submodules "
-                  + "but your nix's (${builtins.nixVersion}) builtins.fetchGit "
-                  + "does not support them"
-                )
-                { }
-            else { };
-        in
-        if nixSupportsSubmodules
-        then { inherit submodules; }
-        else emptyArgWithWarning;
-    in
-    builtins.fetchGit
-      ({ url = spec.repo; inherit (spec) rev; inherit ref; } // submoduleArg);
-
-  fetch_local = spec: spec.path;
-
-  fetch_builtin-tarball = name: throw
-    ''[${name}] The niv type "builtin-tarball" is deprecated. You should instead use `builtin = true`.
-        $ niv modify ${name} -a type=tarball -a builtin=true'';
-
-  fetch_builtin-url = name: throw
-    ''[${name}] The niv type "builtin-url" will soon be deprecated. You should instead use `builtin = true`.
-        $ niv modify ${name} -a type=file -a builtin=true'';
-
-  #
-  # Various helpers
-  #
-
-  # https://github.com/NixOS/nixpkgs/pull/83241/files#diff-c6f540a4f3bfa4b0e8b6bafd4cd54e8bR695
-  sanitizeName = name:
-    (
-      concatMapStrings (s: if builtins.isList s then "-" else s)
-        (
-          builtins.split "[^[:alnum:]+._?=-]+"
-            ((x: builtins.elemAt (builtins.match "\\.*(.*)" x) 0) name)
-        )
-    );
-
-  # The set of packages used when specs are fetched using non-builtins.
-  mkPkgs = sources: system:
-    let
-      sourcesNixpkgs =
-        import (builtins_fetchTarball { inherit (sources.nixpkgs) url sha256; }) { inherit system; };
-      hasNixpkgsPath = builtins.any (x: x.prefix == "nixpkgs") builtins.nixPath;
-      hasThisAsNixpkgsPath = <nixpkgs> == ./.;
-    in
-    if builtins.hasAttr "nixpkgs" sources
-    then sourcesNixpkgs
-    else if hasNixpkgsPath && ! hasThisAsNixpkgsPath then
-      import <nixpkgs> { }
-    else
-      abort
-        ''
-          Please specify either <nixpkgs> (through -I or NIX_PATH=nixpkgs=...) or
-          add a package called "nixpkgs" to your sources.json.
-        '';
-
-  # The actual fetching function.
-  fetch = pkgs: name: spec:
-
-    if ! builtins.hasAttr "type" spec then
-      abort "ERROR: niv spec ${name} does not have a 'type' attribute"
-    else if spec.type == "file" then fetch_file pkgs name spec
-    else if spec.type == "tarball" then fetch_tarball pkgs name spec
-    else if spec.type == "git" then fetch_git name spec
-    else if spec.type == "local" then fetch_local spec
-    else if spec.type == "builtin-tarball" then fetch_builtin-tarball name
-    else if spec.type == "builtin-url" then fetch_builtin-url name
-    else
-      abort "ERROR: niv spec ${name} has unknown type ${builtins.toJSON spec.type}";
-
-  # If the environment variable NIV_OVERRIDE_${name} is set, then use
-  # the path directly as opposed to the fetched source.
-  replace = name: drv:
-    let
-      saneName = stringAsChars (c: if (builtins.match "[a-zA-Z0-9]" c) == null then "_" else c) name;
-      ersatz = builtins.getEnv "NIV_OVERRIDE_${saneName}";
-    in
-    if ersatz == "" then drv else
-      # this turns the string into an actual Nix path (for both absolute and
-      # relative paths)
-    if builtins.substring 0 1 ersatz == "/" then /. + ersatz else /. + builtins.getEnv "PWD" + "/${ersatz}";
-
-  # Ports of functions for older nix versions
-
-  # a Nix version of mapAttrs if the built-in doesn't exist
-  mapAttrs = builtins.mapAttrs or (
-    f: set: with builtins;
-    listToAttrs (map (attr: { name = attr; value = f attr set.${attr}; }) (attrNames set))
-  );
-
-  # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/lists.nix#L295
-  range = first: last: if first > last then [ ] else builtins.genList (n: first + n) (last - first + 1);
-
-  # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L257
-  stringToCharacters = s: map (p: builtins.substring p 1 s) (range 0 (builtins.stringLength s - 1));
-
-  # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L269
-  stringAsChars = f: s: concatStrings (map f (stringToCharacters s));
-  concatMapStrings = f: list: concatStrings (map f list);
-  concatStrings = builtins.concatStringsSep "";
-
-  # https://github.com/NixOS/nixpkgs/blob/8a9f58a375c401b96da862d969f66429def1d118/lib/attrsets.nix#L331
-  optionalAttrs = cond: as: if cond then as else { };
-
-  # fetchTarball version that is compatible between all the versions of Nix
-  builtins_fetchTarball = { url, name ? null, sha256 }@attrs:
-    let
-      inherit (builtins) lessThan nixVersion fetchTarball;
-    in
-    if lessThan nixVersion "1.12" then
-      fetchTarball ({ inherit url; } // (optionalAttrs (name != null) { inherit name; }))
-    else
-      fetchTarball attrs;
-
-  # fetchurl version that is compatible between all the versions of Nix
-  builtins_fetchurl = { url, name ? null, sha256 }@attrs:
-    let
-      inherit (builtins) lessThan nixVersion fetchurl;
-    in
-    if lessThan nixVersion "1.12" then
-      fetchurl ({ inherit url; } // (optionalAttrs (name != null) { inherit name; }))
-    else
-      fetchurl attrs;
-
-  # Create the final "sources" from the config
-  mkSources = config:
-    mapAttrs
-      (
-        name: spec:
-          if builtins.hasAttr "outPath" spec
-          then
-            abort
-              "The values in sources.json should not have an 'outPath' attribute"
-          else
-            spec // { outPath = replace name (fetch config.pkgs name spec); }
-      )
-      config.sources;
-
-  # The "config" used by the fetchers
-  mkConfig =
-    { sourcesFile ? if builtins.pathExists ./sources.json then ./sources.json else null
-    , sources ? if sourcesFile == null then { } else builtins.fromJSON (builtins.readFile sourcesFile)
-    , system ? builtins.currentSystem
-    , pkgs ? mkPkgs sources system
-    }: rec {
-      # The sources, i.e. the attribute set of spec name to spec
-      inherit sources;
-
-      # The "pkgs" (evaluated nixpkgs) to use for e.g. non-builtin fetchers
-      inherit pkgs;
-    };
-
-in
-mkSources (mkConfig { }) // { __functor = _: settings: mkSources (mkConfig settings); }