diff --git a/.circleci/config.yml b/.circleci/config.yml
index d477118ab06beb28edc6d95b03a6d2767342e67b..c7a34563355d1c80c3b31ee076ac1cc002ab3036 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -136,12 +136,11 @@ jobs:
 
   linux-tests: &LINUX_TESTS
     parameters:
-      tahoe-version:
-        # A Tahoe-LAFS version number string like "1.15.0" which corresponds
-        # to a version known by default.nix .  This is the version that will
-        # be declared as a dependency of the Nix package of ZKAPAuthorizer
-        # (and therefore used in the test run and pulled in should you install
-        # this package).
+      tahoe-lafs-source:
+        # The name of a niv source in nix/sources.json which corresponds to
+        # a Tahoe-LAFS version. This is the version that will be declared as a
+        # dependency of the Nix package of ZKAPAuthorizer (and therefore used
+        # in the test run and pulled in should you install this package).
         type: "string"
 
     docker:
@@ -188,11 +187,10 @@ jobs:
             #
             # Further, we want the "doc" output built as well because that's
             # where the coverage data ends up.
-            nix-build \
+            nix-build tests.nix \
               --argstr hypothesisProfile ci \
               --arg collectCoverage true \
-              --argstr tahoe-lafs << parameters.tahoe-version >> \
-              --attr doc
+              --argstr tahoe-lafs-source << parameters.tahoe-lafs-source >>
 
       - run:
           name: "Push to Cachix"
@@ -226,9 +224,8 @@ workflows:
     - "linux-tests":
         matrix:
           parameters:
-            tahoe-version:
-            - "1.14.0"
-            - "1.16.0rc1"
+            tahoe-lafs-source:
+            - "tahoe-lafs"
 
     - "macos-tests":
         matrix:
diff --git a/.circleci/report-coverage.sh b/.circleci/report-coverage.sh
index d7f8c24eac5379a67b9371edeeaac218728f4377..7eec56e8c76251b60460db3c985e3a73603dc05d 100755
--- a/.circleci/report-coverage.sh
+++ b/.circleci/report-coverage.sh
@@ -1,8 +1,8 @@
 #! /usr/bin/env nix-shell
 #! nix-shell -i bash -p "curl" -p "python.withPackages (ps: [ ps.coverage ])"
 set -x
-find ./result-doc/share/doc
-cp ./result-doc/share/doc/*/.coverage.* ./
+find ./result/share/doc
+cp ./result/share/doc/*/.coverage.* ./
 python -m coverage combine
 python -m coverage report
 python -m coverage xml
diff --git a/CONTRIBUTING b/CONTRIBUTING
deleted file mode 100644
index 587c37bc5cf8bd3580a6c65dfa1d63220760d2f2..0000000000000000000000000000000000000000
--- a/CONTRIBUTING
+++ /dev/null
@@ -1,23 +0,0 @@
-Contributing to ZKAPAuthorizer
-==============================
-
-Contributions are accepted in many forms.
-
-Examples of contributions include:
-
-* Bug reports and patch reviews
-* Documentation improvements
-* Code patches
-
-File a ticket at:
-
-https://github.com/PrivateStorageio/ZKAPAuthorizer/issues/new
-
-ZKAPAuthorizer uses GitHub keep track of bugs, feature requests, and associated patches.
-
-Contributions are managed using GitHub's Pull Requests.
-For a PR to be accepted it needs to have:
-
-* an associated issue
-* all CI tests passing
-* patch coverage of 100% as reported by codecov.io
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
new file mode 100644
index 0000000000000000000000000000000000000000..467ebe7c1e6199366594768bcb4a246091157651
--- /dev/null
+++ b/CONTRIBUTING.rst
@@ -0,0 +1,75 @@
+Contributing to ZKAPAuthorizer
+==============================
+
+Contributions are accepted in many forms.
+
+Examples of contributions include:
+
+* Bug reports and patch reviews
+* Documentation improvements
+* Code patches
+
+File a ticket at:
+
+https://github.com/PrivateStorageio/ZKAPAuthorizer/issues/new
+
+ZKAPAuthorizer uses GitHub keep track of bugs, feature requests, and associated patches.
+
+Contributions are managed using GitHub's Pull Requests.
+For a PR to be accepted it needs to have:
+
+* an associated issue
+* all CI tests passing
+* patch coverage of 100% as reported by codecov.io
+
+Updating Dependencies
+---------------------
+
+We use `niv <https://github.com/nmattia/niv>`_ to manage several of our dependencies.
+
+Python Dependencies
+...................
+
+We use `mach-nix <https://github.com/DavHau/mach-nix/>`_ to build python packages.
+It uses a snapshot of pypi to expose python dependencies to nix,
+thus our python depedencies (on nix) are automatically pinned.
+To update the pypy snapshot (and thus our python dependencies), run
+
+.. code:: shell
+
+   nix-shell --run 'niv update pypi-deps-db'
+
+tahoe-lafs
+..........
+
+We depend on pinned commit of tahoe-lafs.
+To update to the latest commit, run
+
+.. code:: shell
+
+   nix-shell --run 'niv update tahoe-lafs --branch master'
+
+It is also possible to pass ``pull/<pr-number>/head`` to test against a specific PR.
+
+.. note::
+
+   Since tahoe-lafs doesn't have correct version information when installed from a github archive,
+   the packaging in ``default.nix`` includes a fake version number.
+   This will need to be update manually at least when the minor version of tahoe-lafs changes.
+
+If you want to test multiple versions, you can add an additional source, pointing at other version
+
+.. code:: shell
+
+   nix-shell --run 'niv add -n tahoe-lafs-next tahoe-lafs/tahoe-lafs --rev "<rev>"'
+   nix-build tests.nix --argstr tahoe-lafs-source tahoe-lafs-next
+
+``--argstr tahoe-lafs-source <...>`` can also be passed to ``nix-shell`` and ``nix-build default.nix``.
+
+nixpkgs
+.......
+
+We pin to a nixos channel release, which isn't directly supported by niv (`issue <https://github.com/nmattia/niv/issues/225>`_).
+Thus, the pin needs to be update manually.
+To do this, copy the ``url`` and ``sha256`` values from PrivateStorageio's `nixpkgs-2105.json <https://whetstone.privatestorage.io/privatestorage/PrivateStorageio/-/blob/develop/nixpkgs-2105.json>`_ into the ``release2015`` entry in ``nix/sources.json``.
+When this is deployed as part of Privatestorageio, we use the value pinned there, rather than the pin in this repository.
diff --git a/default.nix b/default.nix
index 6d24dab31df44598c1baff1feefa9e2e836cf724..7f9b277c7290c3a277c7f80ff194ddabbb32c00b 100644
--- a/default.nix
+++ b/default.nix
@@ -1,22 +1,96 @@
-{ pkgs ? import <nixpkgs> { }
-, hypothesisProfile ? null
-, collectCoverage ? false
-, testSuite ? null
-, trialArgs ? null
-, tahoe-lafs ? "1.14.0"
-}:
 let
-  tahoe-packages = {
-    "1.14.0"    = pkgs.python2Packages.tahoe-lafs-1_14;
-    "1.16.0rc1" = pkgs.python2Packages.callPackage ./nix/tahoe-lafs-1_16.nix { };
-  };
-  tahoe-lafs' = builtins.getAttr tahoe-lafs tahoe-packages;
-
-  pkgs' = pkgs.extend (import ./overlays.nix);
-  callPackage = pkgs'.python27Packages.callPackage;
+  sources = import nix/sources.nix;
 in
-callPackage ./zkapauthorizer.nix {
-  challenge-bypass-ristretto = callPackage ./python-challenge-bypass-ristretto.nix { };
-  inherit hypothesisProfile collectCoverage testSuite trialArgs;
-  tahoe-lafs = tahoe-lafs';
-}
+{ pkgs ? import sources.release2015 {}
+, pypiData ? sources.pypi-deps-db
+, mach-nix ? import sources.mach-nix { inherit pkgs pypiData; }
+, tahoe-lafs-source ? "tahoe-lafs"
+, tahoe-lafs-repo ? sources.${tahoe-lafs-source}
+}:
+  let
+    lib = pkgs.lib;
+    python = "python27";
+    providers = {
+      _default = "sdist,nixpkgs,wheel";
+      # mach-nix doesn't provide a good way to depend on mach-nix packages,
+      # so we get it as a nixpkgs dependency from an overlay. See below for
+      # details.
+      tahoe-lafs = "nixpkgs";
+      # not packaged in nixpkgs at all, we can use the binary wheel from
+      # pypi though.
+      python-challenge-bypass-ristretto = "wheel";
+      # Pure python packages that don't build correctly from sdists
+      # - patches in nixpkgs that don't apply
+      boltons = "wheel";
+      chardet = "wheel";
+      urllib3 = "wheel";
+      # - incorrectly detected dependencies due to pbr
+      fixtures = "wheel";
+      testtools = "wheel";
+      traceback2 = "wheel";
+      # - Incorrectly merged extras - https://github.com/DavHau/mach-nix/pull/334
+      tqdm = "wheel";
+    };
+  in
+    rec {
+      tahoe-lafs = mach-nix.buildPythonPackage rec {
+        inherit python providers;
+        name = "tahoe-lafs";
+        version = "1.16.post999";
+        # See https://github.com/DavHau/mach-nix/issues/190
+        requirementsExtra = ''
+          pyrsistent < 0.17
+          foolscap == 0.13.1
+          configparser
+          eliot
+        '';
+        postPatch = ''
+          cat > src/allmydata/_version.py <<EOF
+          # This _version.py is generated by nix.
+
+          verstr = "${version}+git-${tahoe-lafs-repo.rev}"
+          __version__ = verstr
+          EOF
+        '';
+        src = tahoe-lafs-repo;
+      };
+      zkapauthorizer = mach-nix.buildPythonApplication rec {
+        inherit python providers;
+        src = lib.cleanSource ./.;
+        # mach-nix does not provide a way to specify dependencies on other
+        # mach-nix packages, that incorporates the requirements and overlays
+        # of that package.
+        # See https://github.com/DavHau/mach-nix/issues/123
+        # In particular, we explicitly include the requirements of tahoe-lafs
+        # here, and include it in a python package overlay.
+        requirementsExtra = tahoe-lafs.requirements;
+        overridesPre = [
+          (
+            self: super: {
+              inherit tahoe-lafs;
+            }
+          )
+        ];
+        # Record some settings here, so downstream nix files can consume them.
+        meta.mach-nix = { inherit python providers; };
+      };
+
+      privatestorage = let
+        python-env = mach-nix.mkPython {
+          inherit python providers;
+          packagesExtra = [ zkapauthorizer tahoe-lafs ];
+        };
+      in
+        # Since we use this derivation in `environment.systemPackages`,
+        # we create a derivation that has just the executables we use,
+        # to avoid polluting the system PATH with all the executables
+        # from our dependencies.
+        pkgs.runCommandNoCC "privatestorage" {}
+          ''
+            mkdir -p $out/bin
+            ln -s ${python-env}/bin/tahoe $out/bin
+            # Include some tools that are useful for debugging.
+            ln -s ${python-env}/bin/flogtool $out/bin
+            ln -s ${python-env}/bin/eliot-prettyprint $out/bin
+          '';
+    }
diff --git a/docs/source/CONTRIBUTING.rst b/docs/source/CONTRIBUTING.rst
index fe245d9d5560297d0d8fb5c6d53e0fa674c822c8..ac7b6bcf35e98139c791cb5c4b94f08849972d65 100644
--- a/docs/source/CONTRIBUTING.rst
+++ b/docs/source/CONTRIBUTING.rst
@@ -1 +1 @@
-.. include:: ../../CONTRIBUTING
+.. include:: ../../CONTRIBUTING.rst
diff --git a/nix/repo-1_16_0_rc1.nix b/nix/repo-1_16_0_rc1.nix
deleted file mode 100644
index 4dce852de07a9ae2116b6ef3a813e44aca8d5d15..0000000000000000000000000000000000000000
--- a/nix/repo-1_16_0_rc1.nix
+++ /dev/null
@@ -1,7 +0,0 @@
-{ fetchFromGitHub }:
-fetchFromGitHub {
-  owner = "fenn-cs";
-  repo = "tahoe-lafs";
-  rev = "f6a96ae3976ee21ad0376f7b6a22fc3d12110dce";
-  sha256 = "sha256:127z83c388mvxkz1qdjqdnlj5xgshyn5w5v40vda6mpyy7k9bpb4";
-}
diff --git a/nix/setup.cfg.patch b/nix/setup.cfg.patch
deleted file mode 100644
index 3a2762bdb9f3e03979393e1c79f09b3ed4b67970..0000000000000000000000000000000000000000
--- a/nix/setup.cfg.patch
+++ /dev/null
@@ -1,13 +0,0 @@
-diff --git a/setup.cfg b/setup.cfg
-index dfc49607..822ea8dd 100644
---- a/setup.cfg
-+++ b/setup.cfg
-@@ -40,5 +40,5 @@ install_requires =
-     # incompatible with Tahoe-LAFS'.  So duplicate them here (the ones that
-     # have been observed to cause problems).
-     Twisted[tls,conch]>=18.4.0
--    tahoe-lafs >=1.14, <1.17, !=1.15.*
-+    tahoe-lafs
-     treq
- 
- [versioneer]
diff --git a/nix/sources.json b/nix/sources.json
new file mode 100644
index 0000000000000000000000000000000000000000..d5482b693a41b0e2eb8b5b29dd6ae8a779481d2d
--- /dev/null
+++ b/nix/sources.json
@@ -0,0 +1,56 @@
+{
+    "mach-nix": {
+        "branch": "merged",
+        "description": "Create highly reproducible python environments",
+        "homepage": "",
+        "owner": "PrivateStorageio",
+        "repo": "mach-nix",
+        "rev": "0872dd81afe9c4a6552604f7d21fe7f2baddf454",
+        "sha256": "0hsbm6rmjjjzxdciirmcxyvrrlz19cbhprd2hfksrv6nnl4c3mc3",
+        "type": "tarball",
+        "url": "https://github.com/PrivateStorageio/mach-nix/archive/0872dd81afe9c4a6552604f7d21fe7f2baddf454.tar.gz",
+        "url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
+    },
+    "niv": {
+        "branch": "master",
+        "description": "Easy dependency management for Nix projects",
+        "homepage": "https://github.com/nmattia/niv",
+        "owner": "nmattia",
+        "repo": "niv",
+        "rev": "e0ca65c81a2d7a4d82a189f1e23a48d59ad42070",
+        "sha256": "1pq9nh1d8nn3xvbdny8fafzw87mj7gsmp6pxkdl65w2g18rmcmzx",
+        "type": "tarball",
+        "url": "https://github.com/nmattia/niv/archive/e0ca65c81a2d7a4d82a189f1e23a48d59ad42070.tar.gz",
+        "url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
+    },
+    "pypi-deps-db": {
+        "branch": "master",
+        "description": "Probably the most complete python dependency database",
+        "homepage": "",
+        "owner": "DavHau",
+        "repo": "pypi-deps-db",
+        "rev": "96d01556b4597c022647acbf8c3b58d2a99bc963",
+        "sha256": "0s6ll2hi40gj6mp2zdg7w3dq17g381gnfkm390mqgp574lmbq6yw",
+        "type": "tarball",
+        "url": "https://github.com/DavHau/pypi-deps-db/archive/96d01556b4597c022647acbf8c3b58d2a99bc963.tar.gz",
+        "url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
+    },
+    "release2015": {
+        "sha256": "112drvixj81vscj8cncmks311rk2ik5gydpd03d3r0yc939zjskg",
+        "type": "tarball",
+        "url": "https://releases.nixos.org/nixos/21.05/nixos-21.05.3740.ce7a1190a0f/nixexprs.tar.xz",
+        "url_template": "https://releases.nixos.org/nixos/21.05/nixos-21.05.3740.ce7a1190a0f/nixexprs.tar.xz"
+    },
+    "tahoe-lafs": {
+        "branch": "tahoe-lafs-1.16.0",
+        "description": "The Tahoe-LAFS decentralized secure filesystem.",
+        "homepage": "https://tahoe-lafs.org/",
+        "owner": "tahoe-lafs",
+        "repo": "tahoe-lafs",
+        "rev": "4bfb9d21700b8084d5fb2c697ceeb7088dd97c37",
+        "sha256": "1hcp9gq5hcw43xmg7n24xx580jrg0fd382pklv79r5lr4cicyx7g",
+        "type": "tarball",
+        "url": "https://github.com/tahoe-lafs/tahoe-lafs/archive/4bfb9d21700b8084d5fb2c697ceeb7088dd97c37.tar.gz",
+        "url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
+    }
+}
diff --git a/nix/sources.nix b/nix/sources.nix
new file mode 100644
index 0000000000000000000000000000000000000000..1938409dddb0b57d9f298046cf51875060283df2
--- /dev/null
+++ b/nix/sources.nix
@@ -0,0 +1,174 @@
+# 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 =
+        if spec ? ref then spec.ref else
+          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`!";
+    in
+      builtins.fetchGit { url = spec.repo; inherit (spec) rev; inherit ref; };
+
+  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 isNull (builtins.match "[a-zA-Z0-9]" c) 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 (!isNull name) { 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 (!isNull name) { 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 isNull sourcesFile 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); }
diff --git a/nix/tahoe-lafs-1_16.nix b/nix/tahoe-lafs-1_16.nix
deleted file mode 100644
index 0aed9bd9b1b83c3ff40bf33c5597c10cfd46c28e..0000000000000000000000000000000000000000
--- a/nix/tahoe-lafs-1_16.nix
+++ /dev/null
@@ -1,6 +0,0 @@
-{ callPackage }:
-let
-  repo = callPackage ./repo-1_16_0_rc1.nix { };
-  tahoe-lafs = callPackage "${repo}/nix" { };
-in
-  tahoe-lafs
diff --git a/overlays.nix b/overlays.nix
deleted file mode 100644
index a56387cc6297699930dbde34ae7ed3327218bb50..0000000000000000000000000000000000000000
--- a/overlays.nix
+++ /dev/null
@@ -1,19 +0,0 @@
-self: super: {
-  ristretto = super.callPackage ./ristretto.nix { };
-
-  python27 = super.python27.override {
-    packageOverrides = python-self: python-super: {
-      # The newest typing is incompatible with the packaged version of
-      # Hypothesis.  Upgrading Hypothesis is like pulling on a loose thread in
-      # a sweater.  I pulled it as far as pytest where I found there was no
-      # upgrade route because pytest has dropped Python 2 support.
-      # Fortunately, downgrading typing ends up being fairly straightforward.
-      #
-      # For now.  This is, no doubt, a sign of things to come for the Python 2
-      # ecosystem - the early stages of a slow, painful death by the thousand
-      # cuts of incompatibilities between libraries with no maintained Python
-      # 2 support.
-      typing = python-self.callPackage ./typing.nix { };
-    };
-  };
-}
diff --git a/python-challenge-bypass-ristretto-repo.nix b/python-challenge-bypass-ristretto-repo.nix
deleted file mode 100644
index c7e246f6c2bfb2b8b2fb73bc02ef38b818ac4ddf..0000000000000000000000000000000000000000
--- a/python-challenge-bypass-ristretto-repo.nix
+++ /dev/null
@@ -1,9 +0,0 @@
-let
-  pkgs = import <nixpkgs> {};
-in
-  pkgs.fetchFromGitHub {
-    owner = "LeastAuthority";
-    repo = "python-challenge-bypass-ristretto";
-    rev = "f1a7cfab1a7f1bf8b3345c228c2183064889ad83";
-    sha256 = "12myak2jwaisljs7bmx1vydgd0fnxvkaisk4zsf0kshwxrlnyh3x";
-  }
\ No newline at end of file
diff --git a/python-challenge-bypass-ristretto.nix b/python-challenge-bypass-ristretto.nix
deleted file mode 100644
index 0824d347c371b03705e3021e074508ebbd5a5a0f..0000000000000000000000000000000000000000
--- a/python-challenge-bypass-ristretto.nix
+++ /dev/null
@@ -1,10 +0,0 @@
-{ callPackage }:
-let
-  src = import ./python-challenge-bypass-ristretto-repo.nix;
-  python-challenge-bypass-ristretto = callPackage "${src}" { };
-in
-  python-challenge-bypass-ristretto.overrideAttrs (old: {
-    patches = [
-      ./remove-setuptools-scm.patch
-    ];
-  })
diff --git a/requirements/test.in b/requirements/test.in
new file mode 100644
index 0000000000000000000000000000000000000000..93cf1deaefec1eb0aaa179f111d4533fa3c8b8b7
--- /dev/null
+++ b/requirements/test.in
@@ -0,0 +1,5 @@
+coverage
+fixtures
+testtools
+hypothesis
+pyflakes
diff --git a/ristretto.nix b/ristretto.nix
deleted file mode 100644
index a2bd2c0cd0158455127bd3fd3eef17aae274eefd..0000000000000000000000000000000000000000
--- a/ristretto.nix
+++ /dev/null
@@ -1,5 +0,0 @@
-{ fetchFromGitHub, callPackage }:
-let
-  src = import ./python-challenge-bypass-ristretto-repo.nix { inherit fetchFromGitHub; };
-in
-  callPackage "${src}/challenge-bypass-ristretto.nix" { }
diff --git a/setup.cfg b/setup.cfg
index 6b99b460b88f7e7f96e14f84f65be7da50a5bee9..0c4f044719917527c1935362b1b2b41492e8b4c3 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,6 +1,8 @@
 # Generally describe the project
 [metadata]
-name = Zero-Knowledge Access Pass Authorizer
+# See https://packaging.python.org/guides/distributing-packages-using-setuptools/#name
+# for requiremnts of a valid project name.
+name = zero-knowledge-access-pass-authorizer
 version = attr: _zkapauthorizer.__version__
 description = A `Tahoe-LAFS`_ storage-system plugin which authorizes storage operations based on privacy-respecting tokens.
 long_description = file: README.rst, CHANGELOG.rst, LICENSE-2.0.txt
diff --git a/shell.nix b/shell.nix
index 82afac579e08b5fe57cd8fb8e787fbdd19e78605..f34fcf949ca84d7b092cf06761aae4f03db8df51 100644
--- a/shell.nix
+++ b/shell.nix
@@ -1,14 +1,17 @@
-{ pkgs ? import ./nixpkgs.nix { } }:
 let
-  zkapauthorizer = pkgs.callPackage ./default.nix { };
+  sources = import nix/sources.nix;
 in
-  (pkgs.python27.buildEnv.override {
-    extraLibs = with pkgs.python27Packages; [
-      fixtures
-      testtools
-      hypothesis
-      pyhamcrest
-      zkapauthorizer
-    ];
-    ignoreCollisions = true;
-  }).env
+{ pkgs ? import sources.release2015 {}
+, tahoe-lafs-source ? "tahoe-lafs"
+}:
+  let
+    tests = pkgs.callPackage ./tests.nix {
+      inherit tahoe-lafs-source;
+    };
+  in
+    pkgs.mkShell {
+      packages = [
+        tests.python
+        pkgs.niv
+      ];
+    }
diff --git a/tests.nix b/tests.nix
new file mode 100644
index 0000000000000000000000000000000000000000..961d29d3399ff0bec7f9e0af63fae3cef12a86c8
--- /dev/null
+++ b/tests.nix
@@ -0,0 +1,58 @@
+let
+  sources = import nix/sources.nix;
+in
+{ pkgs ? import sources.release2015 {}
+, pypiData ? sources.pypi-deps-db
+, mach-nix ? import sources.mach-nix { inherit pkgs pypiData; }
+, tahoe-lafs-source ? "tahoe-lafs"
+, tahoe-lafs-repo ? sources.${tahoe-lafs-source}
+, privatestorage ? import ./. {
+    inherit pkgs pypiData mach-nix;
+    inherit tahoe-lafs-repo;
+  }
+, hypothesisProfile ? null
+, collectCoverage ? false
+, testSuite ? null
+, trialArgs ? null
+,
+}:
+  let
+    inherit (pkgs) lib;
+    inherit (privatestorage) zkapauthorizer;
+    hypothesisProfile' = if hypothesisProfile == null then "default" else hypothesisProfile;
+    defaultTrialArgs = [ "--rterrors" ] ++ (lib.optional (! collectCoverage) "--jobs=$NIX_BUILD_CORES");
+    trialArgs' = if trialArgs == null then defaultTrialArgs else trialArgs;
+    extraTrialArgs = builtins.concatStringsSep " " trialArgs';
+    testSuite' = if testSuite == null then "_zkapauthorizer" else testSuite;
+
+    python = mach-nix.mkPython {
+      inherit (zkapauthorizer.meta.mach-nix) python providers;
+      requirements =
+        builtins.readFile ./requirements/test.in;
+      packagesExtra = [ zkapauthorizer ];
+      _.hypothesis.postUnpack = "";
+    };
+  in
+    pkgs.runCommand "zkapauthorizer-tests" {
+      passthru = {
+        inherit python;
+      };
+    } ''
+      mkdir -p $out
+
+      pushd ${zkapauthorizer.src}
+      ${python}/bin/pyflakes
+      popd
+
+      ZKAPAUTHORIZER_HYPOTHESIS_PROFILE=${hypothesisProfile'} ${python}/bin/python -m ${if collectCoverage
+        then "coverage run --debug=config --rcfile=${zkapauthorizer.src}/.coveragerc --module"
+        else ""
+      } twisted.trial ${extraTrialArgs} ${testSuite'}
+
+      ${lib.optionalString collectCoverage
+        ''
+          mkdir -p "$out/coverage"
+          cp -v .coverage.* "$out/coverage"
+        ''
+      }
+    ''
diff --git a/typing.nix b/typing.nix
deleted file mode 100644
index 84c08746f9fdc6bc19bd121e37f168febefb2025..0000000000000000000000000000000000000000
--- a/typing.nix
+++ /dev/null
@@ -1,30 +0,0 @@
-{ lib, buildPythonPackage, fetchPypi, pythonOlder, isPy3k, isPyPy, python }:
-
-let
-  testDir = if isPy3k then "src" else "python2";
-
-in buildPythonPackage rec {
-  pname = "typing";
-  version = "3.6.6";
-
-  src = fetchPypi {
-    inherit pname version;
-    sha256 = "sha256:0ba9acs4awx15bf9v3nrs781msbd2nx826906nj6fqks2bvca9s0";
-  };
-
-  # Error for Python3.6: ImportError: cannot import name 'ann_module'
-  # See https://github.com/python/typing/pull/280
-  # Also, don't bother on PyPy: AssertionError: TypeError not raised
-  doCheck = pythonOlder "3.6" && !isPyPy;
-
-  checkPhase = ''
-    cd ${testDir}
-    ${python.interpreter} -m unittest discover
-  '';
-
-  meta = with lib; {
-    description = "Backport of typing module to Python versions older than 3.5";
-    homepage = https://docs.python.org/3/library/typing.html;
-    license = licenses.psfl;
-  };
-}
diff --git a/zkapauthorizer.nix b/zkapauthorizer.nix
deleted file mode 100644
index 484c364166b2179a50e47e97acc468600949755e..0000000000000000000000000000000000000000
--- a/zkapauthorizer.nix
+++ /dev/null
@@ -1,73 +0,0 @@
-{ lib
-, buildPythonPackage, sphinx, git
-, attrs, zope_interface, aniso8601, twisted, tahoe-lafs, challenge-bypass-ristretto, treq
-, fixtures, testtools, hypothesis, pyflakes, coverage
-, hypothesisProfile ? null
-, collectCoverage ? false
-, testSuite ? null
-, trialArgs ? null
-}:
-let
-  hypothesisProfile' = if hypothesisProfile == null then "default" else hypothesisProfile;
-  testSuite' = if testSuite == null then "_zkapauthorizer" else testSuite;
-  defaultTrialArgs = [ "--rterrors" ] ++ ( lib.optional ( ! collectCoverage ) "--jobs=$NIX_BUILD_CORES" );
-  trialArgs' = if trialArgs == null then defaultTrialArgs else trialArgs;
-  extraTrialArgs = builtins.concatStringsSep " " trialArgs';
-in
-buildPythonPackage rec {
-  version = "0.0";
-  pname = "zero-knowledge-access-pass-authorizer";
-  name = "${pname}-${version}";
-  src = ./.;
-
-  outputs = [ "out" ] ++ (if collectCoverage then [ "doc" ] else [ ]);
-
-  depsBuildBuild = [
-    git
-    sphinx
-  ];
-
-  patches = [
-    # Remove the Tahoe-LAFS version pin in distutils config.  We have our own
-    # pinning and also our Tahoe-LAFS package has a bogus version number. :/
-    ./nix/setup.cfg.patch
-  ];
-
-  propagatedBuildInputs = [
-    aniso8601
-    tahoe-lafs
-    challenge-bypass-ristretto
-
-    # Inherit some things from tahoe-lafs to avoid conflicting versions
-    #
-    # attrs
-    # zope_interface
-    # twisted
-    # eliot
-    # treq
-  ];
-
-  checkInputs = [
-    coverage
-    fixtures
-    testtools
-    hypothesis
-  ];
-
-  checkPhase = ''
-    runHook preCheck
-    "${pyflakes}/bin/pyflakes" src/_zkapauthorizer
-    ZKAPAUTHORIZER_HYPOTHESIS_PROFILE=${hypothesisProfile'} python -m ${if collectCoverage
-      then "coverage run --debug=config --module"
-      else ""
-    } twisted.trial ${extraTrialArgs} ${testSuite'}
-    runHook postCheck
-  '';
-
-  postCheck = if collectCoverage
-    then ''
-    mkdir -p "$doc/share/doc/${name}"
-    cp -v .coverage.* "$doc/share/doc/${name}"
-    ''
-    else "";
-}