diff --git a/CHANGELOG.md b/CHANGELOG.md
index 38aedcc..a2888d4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,12 @@
-## [*Unreleased*](https://github.com/freckle/graphula/compare/v2.0.2.1...main)
+## [_Unreleased_](https://github.com/freckle/graphula/compare/v2.1.0.0...main)
 
-None
+## [v2.1.0.0](https://github.com/freckle/graphula/compare/v2.0.2.2...v2.1.0.0)
+
+- Some unnecessary `SafeToInsert` have been removed from `node` and `nodeKeyed`.
+  - `node` only requires `SafeToInsert` when the `KeySource` is `SourceDefault`,
+    not when the `KeySource` is `KeyArbitrary`.
+  - `nodeKeyed` no longer ever requires `SafeToInsert`
+- `MonadGraphulaFrontend` has a new `insertKeyed` method.
 
 ## [v2.0.2.2](https://github.com/freckle/graphula/compare/v2.0.2.1...v2.0.2.2)
 
diff --git a/flake.lock b/flake.lock
new file mode 100644
index 0000000..32861e7
--- /dev/null
+++ b/flake.lock
@@ -0,0 +1,251 @@
+{
+  "nodes": {
+    "flake-utils": {
+      "inputs": {
+        "systems": "systems"
+      },
+      "locked": {
+        "lastModified": 1701680307,
+        "narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
+        "owner": "numtide",
+        "repo": "flake-utils",
+        "rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
+        "type": "github"
+      },
+      "original": {
+        "owner": "numtide",
+        "repo": "flake-utils",
+        "type": "github"
+      }
+    },
+    "flake-utils_2": {
+      "inputs": {
+        "systems": "systems_2"
+      },
+      "locked": {
+        "lastModified": 1694529238,
+        "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
+        "owner": "numtide",
+        "repo": "flake-utils",
+        "rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
+        "type": "github"
+      },
+      "original": {
+        "owner": "numtide",
+        "repo": "flake-utils",
+        "type": "github"
+      }
+    },
+    "freckle": {
+      "inputs": {
+        "flake-utils": "flake-utils_2",
+        "nixpkgs-23-05": "nixpkgs-23-05",
+        "nixpkgs-master-2023-05-06": "nixpkgs-master-2023-05-06",
+        "nixpkgs-master-2023-07-18": "nixpkgs-master-2023-07-18",
+        "nixpkgs-master-2023-09-15": "nixpkgs-master-2023-09-15",
+        "nixpkgs-stable": "nixpkgs-stable",
+        "nixpkgs-stable-2023-07-25": "nixpkgs-stable-2023-07-25",
+        "nixpkgs-unstable-2023-10-21": "nixpkgs-unstable-2023-10-21"
+      },
+      "locked": {
+        "dir": "main",
+        "lastModified": 1701736713,
+        "narHash": "sha256-LdXNxnzhvAXxX52d79DSTzbKUpnMB5dlZaxXa0KhYEM=",
+        "owner": "freckle",
+        "repo": "flakes",
+        "rev": "89b21c33e0705ecc2280625e25c7d94654fd43bb",
+        "type": "github"
+      },
+      "original": {
+        "dir": "main",
+        "owner": "freckle",
+        "repo": "flakes",
+        "type": "github"
+      }
+    },
+    "nixpkgs-23-05": {
+      "locked": {
+        "lastModified": 1701362232,
+        "narHash": "sha256-GVdzxL0lhEadqs3hfRLuj+L1OJFGiL/L7gCcelgBlsw=",
+        "owner": "nixos",
+        "repo": "nixpkgs",
+        "rev": "d2332963662edffacfddfad59ff4f709dde80ffe",
+        "type": "github"
+      },
+      "original": {
+        "owner": "nixos",
+        "ref": "nixos-23.05",
+        "repo": "nixpkgs",
+        "type": "github"
+      }
+    },
+    "nixpkgs-master-2023-05-06": {
+      "locked": {
+        "lastModified": 1683392273,
+        "narHash": "sha256-pZTuxvcuDeBG+vvE1zczNyEUzlPbzXVh8Ed45Fzo+tQ=",
+        "owner": "nixos",
+        "repo": "nixpkgs",
+        "rev": "16b3b0c53b1ee8936739f8c588544e7fcec3fc60",
+        "type": "github"
+      },
+      "original": {
+        "owner": "nixos",
+        "repo": "nixpkgs",
+        "rev": "16b3b0c53b1ee8936739f8c588544e7fcec3fc60",
+        "type": "github"
+      }
+    },
+    "nixpkgs-master-2023-07-18": {
+      "locked": {
+        "lastModified": 1689680872,
+        "narHash": "sha256-brNix2+ihJSzCiKwLafbyejrHJZUP0Fy6z5+xMOC27M=",
+        "owner": "nixos",
+        "repo": "nixpkgs",
+        "rev": "08700de174bc6235043cb4263b643b721d936bdb",
+        "type": "github"
+      },
+      "original": {
+        "owner": "nixos",
+        "repo": "nixpkgs",
+        "rev": "08700de174bc6235043cb4263b643b721d936bdb",
+        "type": "github"
+      }
+    },
+    "nixpkgs-master-2023-09-15": {
+      "locked": {
+        "lastModified": 1694760568,
+        "narHash": "sha256-3G07BiXrp2YQKxdcdms22MUx6spc6A++MSePtatCYuI=",
+        "owner": "nixos",
+        "repo": "nixpkgs",
+        "rev": "46688f8eb5cd6f1298d873d4d2b9cf245e09e88e",
+        "type": "github"
+      },
+      "original": {
+        "owner": "nixos",
+        "repo": "nixpkgs",
+        "rev": "46688f8eb5cd6f1298d873d4d2b9cf245e09e88e",
+        "type": "github"
+      }
+    },
+    "nixpkgs-stable": {
+      "locked": {
+        "lastModified": 1701263465,
+        "narHash": "sha256-lNXUIlkfyDyp9Ox21hr+wsEf/IBklLvb6bYcyeXbdRc=",
+        "owner": "nixos",
+        "repo": "nixpkgs",
+        "rev": "50aa30a13c4ab5e7ba282da460a3e3d44e9d0eb3",
+        "type": "github"
+      },
+      "original": {
+        "owner": "nixos",
+        "ref": "nixos-23.11",
+        "repo": "nixpkgs",
+        "type": "github"
+      }
+    },
+    "nixpkgs-stable-2023-07-25": {
+      "locked": {
+        "lastModified": 1690271650,
+        "narHash": "sha256-qwdsW8DBY1qH+9luliIH7VzgwvL+ZGI3LZWC0LTiDMI=",
+        "owner": "nixos",
+        "repo": "nixpkgs",
+        "rev": "6dc93f0daec55ee2f441da385aaf143863e3d671",
+        "type": "github"
+      },
+      "original": {
+        "owner": "nixos",
+        "repo": "nixpkgs",
+        "rev": "6dc93f0daec55ee2f441da385aaf143863e3d671",
+        "type": "github"
+      }
+    },
+    "nixpkgs-unstable-2023-10-21": {
+      "locked": {
+        "lastModified": 1697793076,
+        "narHash": "sha256-02e7sCuqLtkyRgrZmdOyvAcQTQdcXj+vpyp9bca6cY4=",
+        "owner": "nixos",
+        "repo": "nixpkgs",
+        "rev": "038b2922be3fc096e1d456f93f7d0f4090628729",
+        "type": "github"
+      },
+      "original": {
+        "owner": "nixos",
+        "repo": "nixpkgs",
+        "rev": "038b2922be3fc096e1d456f93f7d0f4090628729",
+        "type": "github"
+      }
+    },
+    "root": {
+      "inputs": {
+        "flake-utils": "flake-utils",
+        "freckle": "freckle",
+        "stable": "stable",
+        "unstable": "unstable"
+      }
+    },
+    "stable": {
+      "locked": {
+        "lastModified": 1701802827,
+        "narHash": "sha256-wTn0lpV75Uv6tU6haEypNsmnJJPb0hpaMIy/4uf5AiQ=",
+        "owner": "nixos",
+        "repo": "nixpkgs",
+        "rev": "a804fc878d7ba1558b960b4c64b0903da426ac41",
+        "type": "github"
+      },
+      "original": {
+        "owner": "nixos",
+        "ref": "nixos-23.11",
+        "repo": "nixpkgs",
+        "type": "github"
+      }
+    },
+    "systems": {
+      "locked": {
+        "lastModified": 1681028828,
+        "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+        "owner": "nix-systems",
+        "repo": "default",
+        "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+        "type": "github"
+      },
+      "original": {
+        "owner": "nix-systems",
+        "repo": "default",
+        "type": "github"
+      }
+    },
+    "systems_2": {
+      "locked": {
+        "lastModified": 1681028828,
+        "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+        "owner": "nix-systems",
+        "repo": "default",
+        "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+        "type": "github"
+      },
+      "original": {
+        "owner": "nix-systems",
+        "repo": "default",
+        "type": "github"
+      }
+    },
+    "unstable": {
+      "locked": {
+        "lastModified": 1701718080,
+        "narHash": "sha256-6ovz0pG76dE0P170pmmZex1wWcQoeiomUZGggfH9XPs=",
+        "owner": "nixos",
+        "repo": "nixpkgs",
+        "rev": "2c7f3c0fb7c08a0814627611d9d7d45ab6d75335",
+        "type": "github"
+      },
+      "original": {
+        "owner": "nixos",
+        "ref": "nixos-unstable",
+        "repo": "nixpkgs",
+        "type": "github"
+      }
+    }
+  },
+  "root": "root",
+  "version": 7
+}
diff --git a/flake.nix b/flake.nix
new file mode 100644
index 0000000..d264284
--- /dev/null
+++ b/flake.nix
@@ -0,0 +1,78 @@
+{
+  inputs = {
+    stable.url = "github:nixos/nixpkgs/nixos-23.11";
+    unstable.url = "github:nixos/nixpkgs/nixos-unstable";
+    freckle.url = "github:freckle/flakes?dir=main";
+    flake-utils.url = "github:numtide/flake-utils";
+  };
+  outputs = inputs:
+    inputs.flake-utils.lib.eachDefaultSystem (system:
+      let
+        nixpkgsArgs = { inherit system; config = { }; };
+
+        nixpkgs = {
+          stable = import inputs.stable nixpkgsArgs;
+          unstable = import inputs.unstable nixpkgsArgs;
+        };
+        freckle = inputs.freckle.packages.${system};
+        freckleLib = inputs.freckle.lib.${system};
+
+      in
+      rec {
+        packages = {
+          cabal = nixpkgs.stable.cabal-install;
+
+          fourmolu = freckle.fourmolu-0-13-x;
+
+          ghc = freckleLib.haskellBundle {
+            ghcVersion = "ghc-9-4-6";
+            packageSelection = p: [
+              p.annotated-exception
+              p.case-insensitive
+              p.http-client
+              p.http-client-tls
+              p.hspec
+              p.iproute
+              p.network
+              p.QuickCheck
+              p.unliftio
+              p.wai
+              p.warp
+              p.yesod-core
+            ];
+          };
+
+          haskell-language-server =
+            nixpkgs.stable.haskell-language-server.override
+              { supportedGhcVersions = [ "946" ]; };
+
+          stack = nixpkgs.stable.writeShellApplication {
+            name = "stack";
+            text = ''
+              ${nixpkgs.stable.stack}/bin/stack --system-ghc --no-nix "$@"
+            '';
+          };
+        };
+
+        devShells.default = nixpkgs.stable.mkShell {
+          buildInputs = with (nixpkgs.stable); [
+            pcre
+            pcre.dev
+            zlib
+            zlib.dev
+          ];
+
+          nativeBuildInputs = with (packages); [
+            cabal
+            fourmolu
+            ghc
+            haskell-language-server
+            stack
+          ];
+
+          shellHook = ''
+            export STACK_YAML=stack.yaml
+          '';
+        };
+      });
+}
diff --git a/graphula.cabal b/graphula.cabal
index 4886a65..a80cf85 100644
--- a/graphula.cabal
+++ b/graphula.cabal
@@ -1,13 +1,13 @@
 cabal-version: 1.12
 
--- This file has been generated from package.yaml by hpack version 0.35.2.
+-- This file has been generated from package.yaml by hpack version 0.36.0.
 --
 -- see: https://github.com/sol/hpack
 --
--- hash: ca3c9631f0eb250d085bf4e69cf99d4cd5d82c6c7975a14ce18b57fc549575c5
+-- hash: 77f36204ab2afc392bf0f4eb7413aaf8ff36c25b86f28dde5747f3ed9121f0d4
 
 name:           graphula
-version:        2.0.2.2
+version:        2.1.0.0
 synopsis:       A simple interface for generating persistent data and linking its dependencies
 description:    Please see README.md
 category:       Network
diff --git a/package.yaml b/package.yaml
index 80f5dfc..1d49f8e 100644
--- a/package.yaml
+++ b/package.yaml
@@ -1,12 +1,11 @@
 name: graphula
-version: 2.0.2.2
+version: 2.1.0.0
 maintainer: Freckle Education
 category: Network
 github: freckle/graphula
 synopsis: >-
   A simple interface for generating persistent data and linking its dependencies
 description: Please see README.md
-
 extra-source-files:
   - README.md
   - CHANGELOG.md
diff --git a/src/Graphula.hs b/src/Graphula.hs
index 4ba4c6b..09532dc 100755
--- a/src/Graphula.hs
+++ b/src/Graphula.hs
@@ -150,9 +150,9 @@ import Database.Persist
   , delete
   , get
   , getEntity
-  , insertKey
   , insertUnique
   )
+import qualified Database.Persist as Persist
 import Database.Persist.Sql (SqlBackend)
 import Graphula.Class
 import Graphula.Dependencies
@@ -223,9 +223,19 @@ instance (MonadIO m, MonadIO n) => MonadGraphulaFrontend (GraphulaT n m) where
         whenNothing existingKey $ do
           existingUnique <- checkUnique n
           whenNothing existingUnique $ do
-            insertKey key n
+            Persist.insertKey key n
             getEntity key
 
+  insertKeyed key n = do
+    RunDB runDB <- asks dbRunner
+    lift . runDB $ do
+      existingKey <- get key
+      whenNothing existingKey $ do
+        existingUnique <- checkUnique n
+        whenNothing existingUnique $ do
+          Persist.insertKey key n
+          getEntity key
+
   remove key = do
     RunDB runDB <- asks dbRunner
     lift . runDB $ delete key
diff --git a/src/Graphula/Class.hs b/src/Graphula/Class.hs
index 199de82..491020d 100644
--- a/src/Graphula/Class.hs
+++ b/src/Graphula/Class.hs
@@ -67,6 +67,15 @@ class MonadGraphulaFrontend m where
     -> a
     -> m (Maybe (Entity a))
 
+  insertKeyed
+    :: ( PersistEntityBackend a ~ SqlBackend
+       , PersistEntity a
+       , Monad m
+       )
+    => Key a
+    -> a
+    -> m (Maybe (Entity a))
+
   remove
     :: (PersistEntityBackend a ~ SqlBackend, PersistEntity a, Monad m)
     => Key a
diff --git a/src/Graphula/Dependencies.hs b/src/Graphula/Dependencies.hs
index c62a457..d7e3193 100644
--- a/src/Graphula/Dependencies.hs
+++ b/src/Graphula/Dependencies.hs
@@ -24,18 +24,29 @@ module Graphula.Dependencies
 
     -- * Non-serial keys
   , KeySourceType (..)
+  , KeySourceTypeM
+  , KeyForInsert
+  , KeyRequirementForInsert
+  , InsertWithPossiblyRequiredKey (..)
+  , Required (..)
+  , Optional (..)
   , GenerateKey
   , generateKey
   ) where
 
 import Prelude
 
-import Data.Kind (Constraint)
+import Data.Kind (Constraint, Type)
 import Data.Proxy (Proxy (..))
-import Database.Persist (Key)
+import Database.Persist (Entity (..), Key, PersistEntity, PersistEntityBackend)
+import Database.Persist.Sql (SqlBackend)
 import GHC.Generics (Generic)
 import GHC.TypeLits (ErrorMessage (..), TypeError)
 import Generics.Eot (Eot, HasEot, fromEot, toEot)
+import Graphula.Class (GraphulaSafeToInsert, MonadGraphulaFrontend)
+import qualified Graphula.Class as MonadGraphulaFrontend
+  ( MonadGraphulaFrontend (..)
+  )
 import Graphula.Dependencies.Generic
 import Graphula.NoConstraint
 import Test.QuickCheck.Arbitrary (Arbitrary (..))
@@ -129,28 +140,85 @@ data KeySourceType
     -- See 'nodeKeyed'.
     SourceExternal
 
+newtype Required a = Required a
+
+newtype Optional a = Optional (Maybe a)
+
+-- | When a user of Graphula inserts, this wraps the key they provide.
+--   For 'SourceExternal' a key is required; for others it's optional.
+type family KeySourceTypeM (t :: KeySourceType) :: Type -> Type where
+  KeySourceTypeM 'SourceExternal = Required
+  KeySourceTypeM _ = Optional
+
+type KeyRequirementForInsert record = KeySourceTypeM (KeySource record)
+
+-- | When Graphula inserts into Persistent, this wraps the key is provides.
+--   For 'SourceDefault', a key is optional; for others it has always been
+--   generated.
+type family KeySourceTypeInternalM (t :: KeySourceType) :: Type -> Type where
+  KeySourceTypeInternalM 'SourceDefault = Optional
+  KeySourceTypeInternalM _ = Required
+
+type KeyRequirementForInsertInternal record =
+  KeySourceTypeInternalM (KeySource record)
+
+-- | When Graphula inserts into Persistent, this is the record's key.
+type KeyForInsert record = KeyRequirementForInsertInternal record (Key record)
+
+class InsertWithPossiblyRequiredKey (requirement :: Type -> Type) where
+  type InsertConstraint requirement :: Type -> Constraint
+  insertWithPossiblyRequiredKey
+    :: ( PersistEntityBackend record ~ SqlBackend
+       , PersistEntity record
+       , Monad m
+       , MonadGraphulaFrontend m
+       , InsertConstraint requirement record
+       )
+    => requirement (Key record)
+    -> record
+    -> m (Maybe (Entity record))
+  justKey :: key -> requirement key
+
+instance InsertWithPossiblyRequiredKey Optional where
+  type InsertConstraint Optional = GraphulaSafeToInsert
+  insertWithPossiblyRequiredKey (Optional key) = MonadGraphulaFrontend.insert key
+  justKey = Optional . Just
+
+instance InsertWithPossiblyRequiredKey Required where
+  type InsertConstraint Required = NoConstraint
+  insertWithPossiblyRequiredKey (Required key) = MonadGraphulaFrontend.insertKeyed key
+  justKey = Required
+
 -- | Abstract constraint that some @a@ can generate a key
 --
 -- This is part of ensuring better error messages.
 class
-  (GenerateKeyInternal (KeySource a) a, KeyConstraint (KeySource a) a) =>
+  ( GenerateKeyInternal (KeySource a) a
+  , KeyConstraint (KeySource a) a
+  , InsertWithPossiblyRequiredKey (KeySourceTypeInternalM (KeySource a))
+  , InsertConstraint (KeySourceTypeInternalM (KeySource a)) a
+  ) =>
   GenerateKey a
 
 instance
-  (GenerateKeyInternal (KeySource a) a, KeyConstraint (KeySource a) a)
+  ( GenerateKeyInternal (KeySource a) a
+  , KeyConstraint (KeySource a) a
+  , InsertWithPossiblyRequiredKey (KeySourceTypeInternalM (KeySource a))
+  , InsertConstraint (KeySourceTypeInternalM (KeySource a)) a
+  )
   => GenerateKey a
 
 class GenerateKeyInternal (s :: KeySourceType) a where
   type KeyConstraint s a :: Constraint
-  generateKey :: KeyConstraint s a => Gen (Maybe (Key a))
+  generateKey :: KeyConstraint s a => Gen (KeySourceTypeInternalM s (Key a))
 
 instance GenerateKeyInternal 'SourceDefault a where
-  type KeyConstraint 'SourceDefault a = NoConstraint a
-  generateKey = pure Nothing
+  type KeyConstraint 'SourceDefault a = GraphulaSafeToInsert a
+  generateKey = pure (Optional Nothing)
 
 instance GenerateKeyInternal 'SourceArbitrary a where
   type KeyConstraint 'SourceArbitrary a = Arbitrary (Key a)
-  generateKey = Just <$> arbitrary
+  generateKey = Required <$> arbitrary
 
 -- Rendered:
 --
diff --git a/src/Graphula/Idempotent.hs b/src/Graphula/Idempotent.hs
index 3243db3..d7a7fa6 100644
--- a/src/Graphula/Idempotent.hs
+++ b/src/Graphula/Idempotent.hs
@@ -61,6 +61,12 @@ instance
     for_ (entityKey <$> mEnt) $
       \key -> liftIO $ modifyIORef' finalizersRef (remove key >>)
     pure mEnt
+  insertKeyed key n = do
+    finalizersRef <- ask
+    mEnt <- lift $ insertKeyed key n
+    for_ (entityKey <$> mEnt) $
+      \key' -> liftIO $ modifyIORef' finalizersRef (remove key' >>)
+    pure mEnt
   remove = lift . remove
 
 runGraphulaIdempotentT :: MonadUnliftIO m => GraphulaIdempotentT m a -> m a
diff --git a/src/Graphula/Logged.hs b/src/Graphula/Logged.hs
index b56ac95..e47a9a3 100644
--- a/src/Graphula/Logged.hs
+++ b/src/Graphula/Logged.hs
@@ -73,6 +73,7 @@ instance (MonadGraphulaBackend m, MonadIO m) => MonadGraphulaBackend (GraphulaLo
 
 instance (Monad m, MonadGraphulaFrontend m) => MonadGraphulaFrontend (GraphulaLoggedT m) where
   insert mKey = lift . insert mKey
+  insertKeyed key = lift . insertKeyed key
   remove = lift . remove
 
 -- | Run the graph while logging to a temporary file
diff --git a/src/Graphula/Node.hs b/src/Graphula/Node.hs
index 9b8cf24..863f4ff 100644
--- a/src/Graphula/Node.hs
+++ b/src/Graphula/Node.hs
@@ -47,6 +47,7 @@ import Graphula.Arbitrary
 import Graphula.Class
 import Graphula.Dependencies
 import Test.QuickCheck (Arbitrary (..))
+import UnliftIO (MonadIO)
 import UnliftIO.Exception (Exception, throwIO)
 
 -- | Options for generating an individual node
@@ -97,7 +98,7 @@ edit f = mempty {nodeOptionsEdit = Kendo $ Just . f}
 ensure :: (a -> Bool) -> NodeOptions a
 ensure f = mempty {nodeOptionsEdit = Kendo $ \a -> a <$ guard (f a)}
 
--- | Generate a node with a default (Database-provided) key
+-- | Generate a node with a default (Arbitrary or database-provided) key
 --
 -- > a <- node @A () mempty
 node
@@ -110,35 +111,55 @@ node
      , PersistEntityBackend a ~ SqlBackend
      , PersistEntity a
      , Typeable a
-     , GraphulaSafeToInsert a
      )
   => Dependencies a
   -> NodeOptions a
   -> m (Entity a)
-node = nodeImpl $ generate $ generateKey @(KeySource a) @a
+node dependencies NodeOptions {..} =
+  let genKey = generate $ generateKey @(KeySource a) @a
+  in  attempt 100 10 $ do
+        initial <- generate arbitrary
+        for (appKendo nodeOptionsEdit initial) $ \edited -> do
+          -- N.B. dependencies setting always overrules edits
+          let hydrated = edited `dependsOn` dependencies
+          logNode hydrated
+          mKey <- genKey
+          pure (mKey, hydrated)
 
--- | Generate a node with an explictly-given key
---
--- > let someKey = UUID.fromString "..."
--- > a <- nodeKeyed @A someKey () mempty
-nodeKeyed
+attempt
   :: forall a m
    . ( MonadGraphula m
-     , Logging m a
-     , Arbitrary a
-     , HasDependencies a
      , PersistEntityBackend a ~ SqlBackend
      , PersistEntity a
+     , GenerateKey a
      , Typeable a
-     , GraphulaSafeToInsert a
      )
-  => Key a
-  -> Dependencies a
-  -> NodeOptions a
+  => Int
+  -> Int
+  -> m (Maybe (KeyForInsert a, a))
   -> m (Entity a)
-nodeKeyed key = nodeImpl $ pure $ Just key
+attempt maxEdits maxInserts source = loop 0 0
+ where
+  loop :: Int -> Int -> m (Entity a)
+  loop numEdits numInserts
+    | numEdits >= maxEdits = die GenerationFailureMaxAttemptsToConstrain
+    | numInserts >= maxInserts = die GenerationFailureMaxAttemptsToInsert
+    | otherwise =
+        source >>= \case
+          Nothing -> loop (succ numEdits) numInserts
+          --               ^ failed to edit, only increments this
+          Just (mKey, value) ->
+            insertWithPossiblyRequiredKey mKey value >>= \case
+              Nothing -> loop (succ numEdits) (succ numInserts)
+              --               ^ failed to insert, but also increments this. Are we
+              --                 sure that's what we want?
+              Just a -> pure a
 
-nodeImpl
+-- | Generate a node with an explictly-given key
+--
+-- > let someKey = UUID.fromString "..."
+-- > a <- nodeKeyed @A someKey () mempty
+nodeKeyed
   :: forall a m
    . ( MonadGraphula m
      , Logging m a
@@ -147,43 +168,33 @@ nodeImpl
      , PersistEntityBackend a ~ SqlBackend
      , PersistEntity a
      , Typeable a
-     , GraphulaSafeToInsert a
      )
-  => m (Maybe (Key a))
+  => Key a
   -> Dependencies a
   -> NodeOptions a
   -> m (Entity a)
-nodeImpl genKey dependencies NodeOptions {..} = attempt 100 10 $ do
-  initial <- generate arbitrary
-  for (appKendo nodeOptionsEdit initial) $ \edited -> do
-    -- N.B. dependencies setting always overrules edits
-    let hydrated = edited `dependsOn` dependencies
-    logNode hydrated
-    mKey <- genKey
-    pure (mKey, hydrated)
-
-data GenerationFailure
-  = -- | Could not satisfy constraints defined using 'ensure'
-    GenerationFailureMaxAttemptsToConstrain TypeRep
-  | -- | Could not satisfy database constraints on 'insert'
-    GenerationFailureMaxAttemptsToInsert TypeRep
-  deriving stock (Show, Eq)
-
-instance Exception GenerationFailure
-
-attempt
+nodeKeyed key dependencies NodeOptions {..} =
+  attempt' 100 10 key $ do
+    initial <- generate arbitrary
+    for (appKendo nodeOptionsEdit initial) $ \edited -> do
+      -- N.B. dependencies setting always overrules edits
+      let hydrated = edited `dependsOn` dependencies
+      logNode hydrated
+      pure hydrated
+
+attempt'
   :: forall a m
    . ( MonadGraphula m
      , PersistEntityBackend a ~ SqlBackend
      , PersistEntity a
      , Typeable a
-     , GraphulaSafeToInsert a
      )
   => Int
   -> Int
-  -> m (Maybe (Maybe (Key a), a))
+  -> Key a
+  -> m (Maybe a)
   -> m (Entity a)
-attempt maxEdits maxInserts source = loop 0 0
+attempt' maxEdits maxInserts key source = loop 0 0
  where
   loop :: Int -> Int -> m (Entity a)
   loop numEdits numInserts
@@ -193,12 +204,25 @@ attempt maxEdits maxInserts source = loop 0 0
         source >>= \case
           Nothing -> loop (succ numEdits) numInserts
           --               ^ failed to edit, only increments this
-          Just (mKey, value) ->
-            insert mKey value >>= \case
+          Just value ->
+            insertKeyed key value >>= \case
               Nothing -> loop (succ numEdits) (succ numInserts)
               --               ^ failed to insert, but also increments this. Are we
               --                 sure that's what we want?
               Just a -> pure a
 
-  die :: (TypeRep -> GenerationFailure) -> m (Entity a)
-  die e = throwIO $ e $ typeRep (Proxy :: Proxy a)
+die
+  :: forall a m
+   . (MonadIO m, Typeable a)
+  => (TypeRep -> GenerationFailure)
+  -> m (Entity a)
+die e = throwIO $ e $ typeRep $ Proxy @a
+
+data GenerationFailure
+  = -- | Could not satisfy constraints defined using 'ensure'
+    GenerationFailureMaxAttemptsToConstrain TypeRep
+  | -- | Could not satisfy database constraints on 'insert'
+    GenerationFailureMaxAttemptsToInsert TypeRep
+  deriving stock (Show, Eq)
+
+instance Exception GenerationFailure
diff --git a/stack.yaml b/stack.yaml
index 5f2793c..15fad6d 100644
--- a/stack.yaml
+++ b/stack.yaml
@@ -1,4 +1,4 @@
-resolver: lts-21.4
+resolver: lts-21.11
 
 ghc-options:
   "$locals": -fwrite-ide-info
diff --git a/stack.yaml.lock b/stack.yaml.lock
index 24d61c7..30cbd43 100644
--- a/stack.yaml.lock
+++ b/stack.yaml.lock
@@ -6,7 +6,7 @@
 packages: []
 snapshots:
 - completed:
-    sha256: caa77fdbc5b9f698262b21ee78030133272ec53116ad6ddbefdc4c321f668e0c
-    size: 640014
-    url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/21/4.yaml
-  original: lts-21.4
+    sha256: 64d66303f927e87ffe6b8ccf736229bf608731e80d7afdf62bdd63c59f857740
+    size: 640037
+    url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/21/11.yaml
+  original: lts-21.11