diff --git a/Cargo.lock b/Cargo.lock
index 954ba09fab9751f18ae9bdc0e715deffd3699587..a8e35d800aad1c690fb6dc71a3c0aab31c1cda6a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -79,7 +79,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "465a6172cf69b960917811022d8f29bc0b7fa1398bc4f78b3c466673db1213b6"
 dependencies = [
  "quote",
- "syn",
+ "syn 1.0.99",
 ]
 
 [[package]]
@@ -213,7 +213,7 @@ dependencies = [
  "actix-router",
  "proc-macro2",
  "quote",
- "syn",
+ "syn 1.0.99",
 ]
 
 [[package]]
@@ -285,6 +285,17 @@ dependencies = [
  "winapi",
 ]
 
+[[package]]
+name = "argon2"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95c2fcf79ad1932ac6269a738109997a83c227c09b75842ae564dc8ede6a861c"
+dependencies = [
+ "base64ct",
+ "blake2",
+ "password-hash",
+]
+
 [[package]]
 name = "ascii"
 version = "0.9.3"
@@ -299,7 +310,7 @@ checksum = "76464446b8bc32758d7e88ee1a804d9914cd9b1cb264c029899680b0be29826f"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 1.0.99",
 ]
 
 [[package]]
@@ -325,12 +336,27 @@ version = "0.13.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
 
+[[package]]
+name = "base64ct"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
+
 [[package]]
 name = "bitflags"
 version = "1.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
 
+[[package]]
+name = "blake2"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
+dependencies = [
+ "digest",
+]
+
 [[package]]
 name = "block-buffer"
 version = "0.10.2"
@@ -542,7 +568,7 @@ dependencies = [
  "proc-macro-error",
  "proc-macro2",
  "quote",
- "syn",
+ "syn 1.0.99",
 ]
 
 [[package]]
@@ -754,7 +780,7 @@ dependencies = [
  "proc-macro2",
  "quote",
  "strsim",
- "syn",
+ "syn 1.0.99",
 ]
 
 [[package]]
@@ -765,7 +791,7 @@ checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835"
 dependencies = [
  "darling_core",
  "quote",
- "syn",
+ "syn 1.0.99",
 ]
 
 [[package]]
@@ -778,7 +804,7 @@ dependencies = [
  "proc-macro2",
  "quote",
  "rustc_version",
- "syn",
+ "syn 1.0.99",
 ]
 
 [[package]]
@@ -789,6 +815,7 @@ checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506"
 dependencies = [
  "block-buffer",
  "crypto-common",
+ "subtle",
 ]
 
 [[package]]
@@ -952,7 +979,7 @@ checksum = "42cd15d1c7456c04dbdf7e88bcd69760d74f3a798d6444e16974b505b0e62f17"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 1.0.99",
 ]
 
 [[package]]
@@ -1215,6 +1242,12 @@ dependencies = [
  "unicode-normalization",
 ]
 
+[[package]]
+name = "if_chain"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed"
+
 [[package]]
 name = "indexmap"
 version = "1.9.1"
@@ -1297,10 +1330,13 @@ version = "0.10.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2eabca5e0b4d0e98e7f2243fb5b7520b6af2b65d8f87bcc86f2c75185a6ff243"
 dependencies = [
+ "async-trait",
  "base64",
  "email-encoding",
  "email_address",
  "fastrand",
+ "futures-io",
+ "futures-util",
  "httpdate",
  "idna",
  "mime",
@@ -1310,19 +1346,22 @@ dependencies = [
  "rustls",
  "rustls-pemfile",
  "socket2",
+ "tokio",
+ "tokio-rustls",
  "webpki-roots",
 ]
 
 [[package]]
 name = "libc"
-version = "0.2.132"
+version = "0.2.142"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5"
+checksum = "6a987beff54b60ffa6d51982e1aa1146bc42f19bd26be28b0586f252fccf5317"
 
 [[package]]
 name = "libpod"
 version = "0.4.4"
 dependencies = [
+ "argon2",
  "chacha20poly1305",
  "clap 4.0.26",
  "criterion",
@@ -1348,6 +1387,7 @@ dependencies = [
  "reqwest",
  "rusqlite",
  "scheduled-thread-pool",
+ "secrecy",
  "serde",
  "serde_json",
  "serde_path_to_error",
@@ -1358,6 +1398,7 @@ dependencies = [
  "tokio",
  "tracing",
  "tracing-subscriber",
+ "validator",
  "zeroize",
 ]
 
@@ -1475,7 +1516,7 @@ dependencies = [
  "libc",
  "log",
  "wasi",
- "windows-sys",
+ "windows-sys 0.36.1",
 ]
 
 [[package]]
@@ -1624,7 +1665,7 @@ checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 1.0.99",
 ]
 
 [[package]]
@@ -1682,7 +1723,18 @@ dependencies = [
  "libc",
  "redox_syscall",
  "smallvec",
- "windows-sys",
+ "windows-sys 0.36.1",
+]
+
+[[package]]
+name = "password-hash"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
+dependencies = [
+ "base64ct",
+ "rand_core",
+ "subtle",
 ]
 
 [[package]]
@@ -1714,7 +1766,7 @@ checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 1.0.99",
 ]
 
 [[package]]
@@ -1817,7 +1869,7 @@ dependencies = [
  "proc-macro-error-attr",
  "proc-macro2",
  "quote",
- "syn",
+ "syn 1.0.99",
  "version_check",
 ]
 
@@ -1834,18 +1886,18 @@ dependencies = [
 
 [[package]]
 name = "proc-macro2"
-version = "1.0.43"
+version = "1.0.56"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab"
+checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435"
 dependencies = [
  "unicode-ident",
 ]
 
 [[package]]
 name = "quote"
-version = "1.0.21"
+version = "1.0.26"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179"
+checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc"
 dependencies = [
  "proc-macro2",
 ]
@@ -1900,9 +1952,9 @@ dependencies = [
 
 [[package]]
 name = "rand_core"
-version = "0.6.3"
+version = "0.6.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
 dependencies = [
  "getrandom",
 ]
@@ -1981,7 +2033,7 @@ dependencies = [
  "quote",
  "refinery-core",
  "regex",
- "syn",
+ "syn 1.0.99",
 ]
 
 [[package]]
@@ -2144,7 +2196,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2"
 dependencies = [
  "lazy_static",
- "windows-sys",
+ "windows-sys 0.36.1",
 ]
 
 [[package]]
@@ -2172,6 +2224,16 @@ dependencies = [
  "untrusted",
 ]
 
+[[package]]
+name = "secrecy"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e"
+dependencies = [
+ "serde",
+ "zeroize",
+]
+
 [[package]]
 name = "security-framework"
 version = "2.7.0"
@@ -2228,7 +2290,7 @@ checksum = "94ed3a816fb1d101812f83e789f888322c34e291f894f19590dc310963e87a00"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 1.0.99",
 ]
 
 [[package]]
@@ -2282,7 +2344,7 @@ dependencies = [
  "darling",
  "proc-macro2",
  "quote",
- "syn",
+ "syn 1.0.99",
 ]
 
 [[package]]
@@ -2358,9 +2420,9 @@ checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1"
 
 [[package]]
 name = "socket2"
-version = "0.4.7"
+version = "0.4.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd"
+checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662"
 dependencies = [
  "libc",
  "winapi",
@@ -2395,6 +2457,17 @@ dependencies = [
  "unicode-ident",
 ]
 
+[[package]]
+name = "syn"
+version = "2.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
 [[package]]
 name = "tempfile"
 version = "3.3.0"
@@ -2436,7 +2509,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8901a55b0a7a06ebc4a674dcca925170da8e613fa3b163a1df804ed10afb154d"
 dependencies = [
  "quote",
- "syn",
+ "syn 1.0.99",
 ]
 
 [[package]]
@@ -2447,7 +2520,7 @@ checksum = "38f0c854faeb68a048f0f2dc410c5ddae3bf83854ef0e4977d58306a5edef50e"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 1.0.99",
 ]
 
 [[package]]
@@ -2476,7 +2549,7 @@ checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 1.0.99",
 ]
 
 [[package]]
@@ -2533,34 +2606,32 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
 
 [[package]]
 name = "tokio"
-version = "1.21.0"
+version = "1.28.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "89797afd69d206ccd11fb0ea560a44bbb87731d020670e79416d442919257d42"
+checksum = "c3c786bf8134e5a3a166db9b29ab8f48134739014a3eca7bc6bfa95d673b136f"
 dependencies = [
  "autocfg",
  "bytes",
  "libc",
- "memchr",
  "mio",
  "num_cpus",
- "once_cell",
  "parking_lot",
  "pin-project-lite",
  "signal-hook-registry",
  "socket2",
  "tokio-macros",
- "winapi",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
 name = "tokio-macros"
-version = "1.8.0"
+version = "2.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484"
+checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.15",
 ]
 
 [[package]]
@@ -2634,7 +2705,7 @@ checksum = "11c75893af559bc8e10716548bdef5cb2b983f8e637db9d0e15126b61b484ee2"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 1.0.99",
 ]
 
 [[package]]
@@ -2753,6 +2824,48 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "validator"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32ad5bf234c7d3ad1042e5252b7eddb2c4669ee23f32c7dd0e9b7705f07ef591"
+dependencies = [
+ "idna",
+ "lazy_static",
+ "regex",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "url",
+ "validator_derive",
+]
+
+[[package]]
+name = "validator_derive"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc44ca3088bb3ba384d9aecf40c6a23a676ce23e09bdaca2073d99c207f864af"
+dependencies = [
+ "if_chain",
+ "lazy_static",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "regex",
+ "syn 1.0.99",
+ "validator_types",
+]
+
+[[package]]
+name = "validator_types"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "111abfe30072511849c5910134e8baf8dc05de4c0e5903d681cbd5c9c4d611e3"
+dependencies = [
+ "proc-macro2",
+ "syn 1.0.99",
+]
+
 [[package]]
 name = "valuable"
 version = "0.1.0"
@@ -2825,7 +2938,7 @@ dependencies = [
  "once_cell",
  "proc-macro2",
  "quote",
- "syn",
+ "syn 1.0.99",
  "wasm-bindgen-shared",
 ]
 
@@ -2859,7 +2972,7 @@ checksum = "5be8e654bdd9b79216c2929ab90721aa82faf65c48cdf08bdc4e7f51357b80da"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 1.0.99",
  "wasm-bindgen-backend",
  "wasm-bindgen-shared",
 ]
@@ -2936,43 +3049,109 @@ version = "0.36.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2"
 dependencies = [
- "windows_aarch64_msvc",
- "windows_i686_gnu",
- "windows_i686_msvc",
- "windows_x86_64_gnu",
- "windows_x86_64_msvc",
+ "windows_aarch64_msvc 0.36.1",
+ "windows_i686_gnu 0.36.1",
+ "windows_i686_msvc 0.36.1",
+ "windows_x86_64_gnu 0.36.1",
+ "windows_x86_64_msvc 0.36.1",
 ]
 
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc 0.48.0",
+ "windows_i686_gnu 0.48.0",
+ "windows_i686_msvc 0.48.0",
+ "windows_x86_64_gnu 0.48.0",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc 0.48.0",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc"
+
 [[package]]
 name = "windows_aarch64_msvc"
 version = "0.36.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47"
 
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3"
+
 [[package]]
 name = "windows_i686_gnu"
 version = "0.36.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6"
 
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241"
+
 [[package]]
 name = "windows_i686_msvc"
 version = "0.36.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024"
 
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00"
+
 [[package]]
 name = "windows_x86_64_gnu"
 version = "0.36.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1"
 
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953"
+
 [[package]]
 name = "windows_x86_64_msvc"
 version = "0.36.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"
 
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
+
 [[package]]
 name = "winreg"
 version = "0.10.1"
diff --git a/Cargo.toml b/Cargo.toml
index 37c53cd5c9dea04b6f757a3211b4ef7932ee52c5..d09ecc1eeedd475b26cb008dc8ca9df5cbac213f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -17,7 +17,7 @@ sha2 = "0.10.2"
 serde = { version = "1.0.130" }
 serde_json = "1.0.72"
 serde_path_to_error = "0.1.5"
-tokio = { version = "1.14.0", features = ["full"] }
+tokio = { version = "1.28.0", features = ["full"] }
 tracing = "0.1.36"
 tracing-subscriber = { version = "0.3.15" }
 futures = "0.3.24"
diff --git a/libpod/Cargo.toml b/libpod/Cargo.toml
index ec76540c9607e7ef2da5d709cdea95d159d9c9cf..35318b08541a0d83e71532bd5dedb6f6e2c430b4 100644
--- a/libpod/Cargo.toml
+++ b/libpod/Cargo.toml
@@ -15,6 +15,8 @@ lettre = { version = "0.10.0-rc.3", default-features = false, features = [
   "builder",
   "rustls-tls",
   "smtp-transport",
+  "tokio1-rustls-tls",
+  "tokio1"
 ] }
 libc = "0.2.108"
 md5 = "0.7.0"
@@ -54,6 +56,9 @@ futures = { workspace = true }
 thiserror = "1.0.38"
 mime = "0.3.16"
 scheduled-thread-pool = "0.2.7"
+secrecy = { version = "0.8.0", features = ["serde"] }
+argon2 = "0.5.0"
+validator = { version = "0.16.0", features = ["derive"] }
 
 [dev-dependencies]
 criterion = {version = "0.3.5", features = ["async_tokio"]}
diff --git a/libpod/res/migrations/V15__item_UserAccount.sql b/libpod/res/migrations/V15__item_UserAccount.sql
new file mode 100644
index 0000000000000000000000000000000000000000..ee5f96f45b7ed32173469ab3eed3ac9fee7a8378
--- /dev/null
+++ b/libpod/res/migrations/V15__item_UserAccount.sql
@@ -0,0 +1,103 @@
+-- PodUserAccount {
+--    login_hash: String,
+--    password_hash: String,
+--    salt: String,
+--    state: String,
+--    code: String
+-- }
+
+
+-- login: String
+INSERT INTO items(id, type, dateCreated, dateModified, dateServerModified, deleted) VALUES(
+    "d894f98c-20cc-4de1-a7dc-b48590d2cadd",
+    "ItemPropertySchema", 0, 0, 0, 0
+);
+INSERT INTO strings(item, name, value) VALUES(
+    (SELECT rowid FROM items WHERE id = "d894f98c-20cc-4de1-a7dc-b48590d2cadd"),
+    "itemType", "PodUserAccount"
+);
+
+INSERT INTO strings(item, name, value) VALUES(
+    (SELECT rowid FROM items WHERE id = "d894f98c-20cc-4de1-a7dc-b48590d2cadd"),
+    "propertyName", "loginHash"
+);
+INSERT INTO strings(item, name, value) VALUES(
+    (SELECT rowid FROM items WHERE id = "d894f98c-20cc-4de1-a7dc-b48590d2cadd"),
+    "valueType", "Text"
+);
+
+-- passwordHash: String
+INSERT INTO items(id, type, dateCreated, dateModified, dateServerModified, deleted) VALUES(
+    "c9eff54e-8164-4b21-a00e-4c4e9a1fda09",
+    "ItemPropertySchema", 0, 0, 0, 0
+);
+INSERT INTO strings(item, name, value) VALUES(
+    (SELECT rowid FROM items WHERE id = "c9eff54e-8164-4b21-a00e-4c4e9a1fda09"),
+    "itemType", "PodUserAccount"
+);
+
+INSERT INTO strings(item, name, value) VALUES(
+    (SELECT rowid FROM items WHERE id = "c9eff54e-8164-4b21-a00e-4c4e9a1fda09"),
+    "propertyName", "passwordHash"
+);
+INSERT INTO strings(item, name, value) VALUES(
+    (SELECT rowid FROM items WHERE id = "c9eff54e-8164-4b21-a00e-4c4e9a1fda09"),
+    "valueType", "Text"
+);
+
+-- salt: String
+INSERT INTO items(id, type, dateCreated, dateModified, dateServerModified, deleted) VALUES(
+    "6defe71b-d43e-494c-abf4-423dc6c46acc",
+    "ItemPropertySchema", 0, 0, 0, 0
+);
+INSERT INTO strings(item, name, value) VALUES(
+    (SELECT rowid FROM items WHERE id = "6defe71b-d43e-494c-abf4-423dc6c46acc"),
+    "itemType", "PodUserAccount"
+);
+
+INSERT INTO strings(item, name, value) VALUES(
+    (SELECT rowid FROM items WHERE id = "6defe71b-d43e-494c-abf4-423dc6c46acc"),
+    "propertyName", "salt"
+);
+INSERT INTO strings(item, name, value) VALUES(
+    (SELECT rowid FROM items WHERE id = "6defe71b-d43e-494c-abf4-423dc6c46acc"),
+    "valueType", "Text"
+);
+
+-- state: String
+INSERT INTO items(id, type, dateCreated, dateModified, dateServerModified, deleted) VALUES(
+    "570dd24e-fe70-4566-8e45-e3de7a0e0d55",
+    "ItemPropertySchema", 0, 0, 0, 0
+);
+INSERT INTO strings(item, name, value) VALUES(
+    (SELECT rowid FROM items WHERE id = "570dd24e-fe70-4566-8e45-e3de7a0e0d55"),
+    "itemType", "PodUserAccount"
+);
+
+INSERT INTO strings(item, name, value) VALUES(
+    (SELECT rowid FROM items WHERE id = "570dd24e-fe70-4566-8e45-e3de7a0e0d55"),
+    "propertyName", "state"
+);
+INSERT INTO strings(item, name, value) VALUES(
+    (SELECT rowid FROM items WHERE id = "570dd24e-fe70-4566-8e45-e3de7a0e0d55"),
+    "valueType", "Text"
+);
+
+-- code: String
+INSERT INTO items(id, type, dateCreated, dateModified, dateServerModified, deleted) VALUES(
+    "be8d0096-b635-4de4-8b44-6f2a2acec0fa",
+    "ItemPropertySchema", 0, 0, 0, 0
+);
+INSERT INTO strings(item, name, value) VALUES(
+    (SELECT rowid FROM items WHERE id = "be8d0096-b635-4de4-8b44-6f2a2acec0fa"),
+    "itemType", "PodUserAccount"
+);
+
+INSERT INTO strings(item, name, value) VALUES(
+    (SELECT rowid FROM items WHERE id = "be8d0096-b635-4de4-8b44-6f2a2acec0fa"),
+    "propertyName", "code"
+);
+INSERT INTO strings(item, name, value) VALUES(
+    (SELECT rowid FROM items WHERE id = "be8d0096-b635-4de4-8b44-6f2a2acec0fa"),
+    "valueType", "Text"
+);
\ No newline at end of file
diff --git a/libpod/src/api_model.rs b/libpod/src/api_model.rs
index b4f602ef83f26ec2eba76463629425c8a4d7ecb7..1d5055143e9819426a6852a885ddb285f352f5c1 100644
--- a/libpod/src/api_model.rs
+++ b/libpod/src/api_model.rs
@@ -1,16 +1,19 @@
 use reqwest::Method;
+use secrecy::Secret;
 use serde::de::{self, Visitor};
-use serde::{Deserialize, Serialize};
+use serde::{Deserialize, Deserializer, Serialize};
 use serde_json::Value;
+use std::ops::Deref;
 use std::str::FromStr;
 use std::{collections::HashMap, fmt::Debug, fmt::Display};
 
 //
 // Wrapper structs:
 //
+
+// TODO: create a macro for implementing truncated logging
 #[derive(Clone, Serialize, Deserialize, Debug)]
 pub struct PodOwner(String);
-
 impl Display for PodOwner {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         let s = self.0.as_str();
@@ -22,7 +25,6 @@ impl Display for PodOwner {
     }
 }
 
-use std::ops::Deref;
 impl Deref for PodOwner {
     type Target = String;
 
@@ -45,6 +47,43 @@ impl FromStr for PodOwner {
     }
 }
 
+#[derive(Clone, Serialize, Deserialize, Debug)]
+pub struct PodUserLogin(String);
+
+/// In order to get full value use deref and reborrow: (&*login)
+impl Display for PodUserLogin {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        let s = self.0.as_str();
+        if s.len() < 7 {
+            write!(f, "{s}")
+        } else {
+            write!(f, "{}..{}", &s[..5], &s[s.len() - 5..])
+        }
+    }
+}
+
+impl Deref for PodUserLogin {
+    type Target = String;
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+impl From<String> for PodUserLogin {
+    fn from(s: String) -> Self {
+        Self(s)
+    }
+}
+
+impl FromStr for PodUserLogin {
+    type Err = core::convert::Infallible;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        Ok(PodUserLogin(s.into()))
+    }
+}
+
 #[derive(Serialize, Deserialize, Debug)]
 #[serde(rename_all = "camelCase")]
 pub struct PluginAuthData {
@@ -365,11 +404,44 @@ pub struct PluginStatusRes {
 
 #[derive(Serialize, Deserialize, Debug)]
 #[serde(rename_all = "camelCase")]
-pub struct CreateUserReq {
+pub struct PodCredentials {
     pub owner_key: PodOwner,
     pub database_key: String,
 }
 
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+pub struct UserAccountCredentials {
+    #[serde(deserialize_with = "deserialize_login")]
+    pub login: PodUserLogin,
+    pub password: Secret<String>,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+pub struct RegisterVerifyAccountReq {
+    #[serde(deserialize_with = "deserialize_login")]
+    pub login: PodUserLogin,
+    pub code: Secret<String>,
+}
+
+fn deserialize_login<'de, D>(deserializer: D) -> std::result::Result<PodUserLogin, D::Error>
+where
+    D: Deserializer<'de>,
+{
+    let mail: &str = Deserialize::deserialize(deserializer)?;
+    // Even though local part of the mail IS case sensitive, we convert to
+    // lowercase. Major mail service providers correctly assume it
+    // should be insensitive.
+    let login = PodUserLogin(mail.to_lowercase());
+
+    if !validator::validate_email(&login.0) {
+        return Err(serde::de::Error::custom("Invalid email format"));
+    }
+
+    Ok(login)
+}
+
 #[derive(Deserialize, Debug)]
 #[serde(rename_all = "camelCase")]
 pub struct PluginGetApiReq {
diff --git a/libpod/src/command_line_interface.rs b/libpod/src/command_line_interface.rs
index da71460931901d0345ebe0b431c75953f524b625..1ce42695069130585c5f89cd5058917371008a22 100644
--- a/libpod/src/command_line_interface.rs
+++ b/libpod/src/command_line_interface.rs
@@ -2,6 +2,11 @@ use clap::Parser;
 use lazy_static::lazy_static;
 use std::net::IpAddr;
 
+pub const DEFAULT_POD_DB_KEY: &str =
+    "54fec98071c745d0b812f97cb315f665e497301b39af499686ad8f0464663952";
+pub const DEFAULT_POD_OWNER_KEY: &str =
+    "ad2e49e832214740af7ff3fa67d7b45bc0d8179346d443c9894b3ebe45978f54";
+
 #[derive(Parser, Debug, Clone)]
 #[command(
     name = "Pod, the open-source backend for Memri project.",
@@ -168,12 +173,22 @@ pub struct CliOptions {
     pub email_smtp_password: Option<String>,
 
     /// DB credentials used by the POD to store non volatile information
-    #[arg(long, env = "POD_OWNER_KEY_FOR_POD", requires = "db_key_for_pod")]
-    pub owner_key_for_pod: Option<String>,
+    #[arg(
+        long,
+        env = "POD_OWNER_KEY_FOR_POD",
+        requires = "db_key_for_pod",
+        default_value = DEFAULT_POD_DB_KEY
+    )]
+    pub owner_key_for_pod: String,
 
     /// DB credentials used by the POD to store non volatile information
-    #[arg(long, env = "POD_DB_KEY_FOR_POD", requires = "owner_key_for_pod")]
-    pub db_key_for_pod: Option<String>,
+    #[arg(
+        long,
+        env = "POD_DB_KEY_FOR_POD",
+        requires = "owner_key_for_pod",
+        default_value = DEFAULT_POD_OWNER_KEY
+    )]
+    pub db_key_for_pod: String,
 
     #[arg(long, env = "POD_SHARED_PLUGINS", action(clap::ArgAction::Append))]
     pub shared_plugins: Vec<String>,
@@ -240,8 +255,8 @@ pub mod tests {
             email_smtp_port: 465,
             email_smtp_user: None,
             email_smtp_password: None,
-            owner_key_for_pod: None,
-            db_key_for_pod: None,
+            owner_key_for_pod: Default::default(),
+            db_key_for_pod: Default::default(),
             shared_plugins: Vec::new(),
             opened_connections: None,
         }
diff --git a/libpod/src/constants.rs b/libpod/src/constants.rs
index f565f21a5d668c75a3e6699111edf750d5c3e6f8..64739fd3e6a102ff16102c1c3a8a8f2ea9c8e25d 100644
--- a/libpod/src/constants.rs
+++ b/libpod/src/constants.rs
@@ -7,8 +7,6 @@ pub const FILES_DIR: &str = "./data/files";
 /// (in future, the files should also be s3-uploaded).
 pub const FILES_FINAL_SUBDIR: &str = "final";
 
-pub const PLUGIN_EMAIL_SUBJECT_PREFIX: &str = "Memri plugin message: ";
-pub const PLUGIN_EMAIL_FOOTER: &str =
-    "This is an automated message from a Memri plugin, do not reply.
+pub const PLUGIN_EMAIL_FOOTER: &str = "This is an automated message from Memri, do not reply.
 
 ";
diff --git a/libpod/src/database_pool.rs b/libpod/src/database_pool.rs
index a98945642f296dc4ccc7ccbc8e4924707d7993e3..e79059e598af7e09ac5466aca271a25cb7a99048 100644
--- a/libpod/src/database_pool.rs
+++ b/libpod/src/database_pool.rs
@@ -1,7 +1,7 @@
 use crate::{
     any_error,
     async_db_connection::AsyncConnection,
-    bad_request, command_line_interface, constants, database_migrate_refinery,
+    command_line_interface, constants, database_migrate_refinery,
     error::{ErrorContext, Result},
     internal_error,
     plugin_auth_crypto::{DatabaseKey, SHA256Output},
@@ -52,7 +52,8 @@ pub async fn initialize_db(
 ) -> Result<()> {
     let mut db = init_db.write().await;
     if owner_database_path(owner).exists() {
-        return Err(bad_request! { "Account for {owner} already exist" });
+        // Database is already set up
+        return Ok(());
     } else if db.get(owner).is_some() {
         return Err(
             internal_error! { "DB file does not exist but connection pool does for {owner}" },
diff --git a/libpod/src/db_model.rs b/libpod/src/db_model.rs
index fa7324cfeb77871b550f40bb3d10d626259144c4..baa3ccbe609e2ee4a0c2f3eb0a16f2ccd81bb0e8 100644
--- a/libpod/src/db_model.rs
+++ b/libpod/src/db_model.rs
@@ -102,3 +102,28 @@ pub struct Oauth2Flow {
     #[serde(flatten)]
     pub base: ItemBase,
 }
+
+#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
+#[serde(rename_all = "camelCase")]
+pub enum RegisterState {
+    VerifyEmailSent,
+    RegistrationComplete,
+    EnforcePasswordReset,
+}
+
+pub const POD_ACCOUNT: &str = "PodUserAccount";
+pub const LOGIN_HASH: &str = "loginHash";
+pub const PASSWORD_HASH: &str = "passwordHash";
+pub const SALT: &str = "salt";
+pub const STATE: &str = "state";
+pub const CODE: &str = "code";
+#[derive(Serialize, Deserialize, Debug, Clone)]
+#[serde(rename_all = "camelCase")]
+pub struct PodUserAccount {
+    pub login_hash: String,
+    /// In PHC format
+    pub password_hash: String,
+    pub salt: String,
+    pub state: RegisterState,
+    pub code: String,
+}
diff --git a/libpod/src/email.rs b/libpod/src/email.rs
index 2886e4a14158d3056d7d364152770ac544664a0a..ef58684d43f8f76ba4f1d9d1be5dd7aad4b3b3c5 100644
--- a/libpod/src/email.rs
+++ b/libpod/src/email.rs
@@ -1,14 +1,15 @@
 use crate::{
-    api_model::SendEmail,
-    command_line_interface::CliOptions,
-    constants::{PLUGIN_EMAIL_FOOTER, PLUGIN_EMAIL_SUBJECT_PREFIX},
+    api_model::SendEmail, command_line_interface::CliOptions, constants::PLUGIN_EMAIL_FOOTER,
     error::Result,
 };
-use lettre::{transport::smtp::authentication::Credentials, Message, SmtpTransport, Transport};
-use tracing::{info, trace};
+use lettre::{
+    transport::smtp::authentication::Credentials, AsyncSmtpTransport, AsyncTransport, Message,
+    Tokio1Executor,
+};
+use tracing::{debug, info};
 
-pub fn send_email(email: SendEmail, cli: &CliOptions) -> Result<()> {
-    trace!("Starting to send email: {:?}", email);
+pub async fn send_email(email: SendEmail, cli: &CliOptions) -> Result<()> {
+    debug!("Starting to send email: {:?}", email);
     match (
         &cli.email_smtp_relay,
         &cli.email_smtp_user,
@@ -16,17 +17,18 @@ pub fn send_email(email: SendEmail, cli: &CliOptions) -> Result<()> {
     ) {
         (Some(relay), Some(user), Some(password)) => {
             let email = Message::builder()
-                .from(format!("Memri plugin <{user}>").parse()?)
+                .from(format!("Memri <{user}>").parse()?)
                 .to(email.to.parse()?)
-                .subject(format!("{}{}", PLUGIN_EMAIL_SUBJECT_PREFIX, email.subject))
-                .body(format!("{}{}", PLUGIN_EMAIL_FOOTER, email.body))?;
+                .subject(email.subject.to_string())
+                .body(format!("{}\n{}", email.body, PLUGIN_EMAIL_FOOTER))?;
             let credentials: Credentials = Credentials::new(user.to_string(), password.to_string());
-            let server = SmtpTransport::relay(relay)?
+            let server = AsyncSmtpTransport::<Tokio1Executor>::relay(relay)?
                 .port(cli.email_smtp_port)
                 .credentials(credentials)
                 .timeout(Some(core::time::Duration::from_millis(5000)))
                 .build();
-            server.send(&email)?;
+            server.send(email).await?;
+            debug!("Mail sent");
             Ok(())
         }
         _ => {
diff --git a/libpod/src/lib.rs b/libpod/src/lib.rs
index 13eda9e1e3db504865e944f86f0ae2b78fed45a8..0f8707567233baccd33c81c94e3919a243d53495 100644
--- a/libpod/src/lib.rs
+++ b/libpod/src/lib.rs
@@ -19,6 +19,7 @@ pub mod plugin_auth_crypto;
 pub mod plugin_run;
 mod plugin_trigger;
 pub mod schema;
-pub mod shared_plugins;
+pub mod shared_state;
 pub mod test_helpers;
 pub mod triggers;
+pub mod user_account;
diff --git a/libpod/src/plugin_run.rs b/libpod/src/plugin_run.rs
index 984593520220a49c1240330cbde5b7d376b31b07..7c999ac3464c5225156dc6f417d3edbf8574917a 100644
--- a/libpod/src/plugin_run.rs
+++ b/libpod/src/plugin_run.rs
@@ -512,12 +512,7 @@ fn kubernetes_set_resource_limits(cli_options: &CliOptions, plugin: &PluginRunIt
 
 /// Returns true if pod_owner account is used to store shared data
 fn is_pod_share_account(pod_owner: &str, cli: &CliOptions) -> bool {
-    pod_owner.to_ascii_lowercase()
-        == cli
-            .owner_key_for_pod
-            .as_ref()
-            .unwrap_or(&"".to_string())
-            .to_ascii_lowercase()
+    pod_owner.to_ascii_lowercase() == cli.owner_key_for_pod.to_ascii_lowercase()
 }
 
 pub fn get_logs(tx: &AsyncTx, plugin_run_id: &str, cli_options: &CliOptions) -> Result<Value> {
diff --git a/libpod/src/shared_plugins.rs b/libpod/src/shared_state.rs
similarity index 64%
rename from libpod/src/shared_plugins.rs
rename to libpod/src/shared_state.rs
index c70dba5da02f7847337e85e921d908ef7262c531..df50c8383f47296ccde5ed3bfa92f59ba373324e 100644
--- a/libpod/src/shared_plugins.rs
+++ b/libpod/src/shared_state.rs
@@ -2,47 +2,62 @@ use crate::{
     api_model::{AuthKey, ClientAuth, CreateItem, Search},
     async_db_connection::{AsyncConnection, AsyncTx},
     command_line_interface::PARSED,
-    database_api::{self, dangerous_permament_remove_item},
+    database_api,
     database_pool::{get_db_connection, initialize_db, InitDb},
     db_model::ItemBase,
-    error::Result,
-    internal_api::{self, search},
+    error::{ErrorContext, Result},
+    internal_api,
     plugin_auth_crypto::auth_to_database_key,
 };
 use std::ops::Deref;
 use tracing::{debug, info, warn};
 
+pub async fn initialize(init_db: &InitDb) -> Result<()> {
+    initialize_database(init_db)
+        .await
+        .context_str("While initializing shared database connection")?;
+    initialize_plugins(init_db)
+        .await
+        .context_str("While initializing shared plugins")
+}
+
+pub async fn initialize_database(init_db: &InitDb) -> Result<()> {
+    let (owner_key, database_key) = (
+        &PARSED.owner_key_for_pod,
+        auth_to_database_key(AuthKey::ClientAuth(ClientAuth {
+            database_key: PARSED.db_key_for_pod.clone(),
+        }))?,
+    );
+
+    initialize_db(owner_key, init_db, &database_key).await
+}
+
+pub async fn db_connection(init_db: &InitDb) -> Result<AsyncConnection> {
+    let (owner, database_key) = (
+        &PARSED.owner_key_for_pod,
+        auth_to_database_key(AuthKey::ClientAuth(ClientAuth {
+            database_key: PARSED.db_key_for_pod.clone(),
+        }))?,
+    );
+
+    get_db_connection(owner, init_db, &database_key).await
+}
+
 // TODO: there is no proper plugin state management
 // if pod restarts - plugins are unable to reach it, plugin auth is broken
 // plugins listens for that, and shuts down
 // garbage in the db stays
 // if plugin dies, pod does nothing with it
 
-pub async fn db_connection(init_db: &InitDb) -> Result<Option<AsyncConnection>> {
-    if let (Some(owner), Some(database_key)) = (
-        PARSED.owner_key_for_pod.clone(),
-        PARSED.db_key_for_pod.clone(),
-    ) {
-        let database_key = auth_to_database_key(AuthKey::ClientAuth(ClientAuth { database_key }))?;
-        let conn = get_db_connection(&owner, init_db, &database_key).await?;
-
-        Ok(Some(conn))
-    } else {
-        Ok(None)
-    }
-}
-
-pub async fn initialize(init_db: &InitDb) -> Result<()> {
-    let (Some(owner_key), Some(database_key)) = (PARSED.owner_key_for_pod.clone(), PARSED.db_key_for_pod.clone()) else {
-        info!("No POD credentials provided, skipping startup of shared plugins");
-        return Ok(());
-    };
-
-    let database_key = auth_to_database_key(AuthKey::ClientAuth(ClientAuth { database_key }))?;
-
-    let _ = initialize_db(&owner_key, init_db, &database_key).await;
+pub async fn initialize_plugins(init_db: &InitDb) -> Result<()> {
+    let (owner_key, database_key) = (
+        &PARSED.owner_key_for_pod,
+        auth_to_database_key(AuthKey::ClientAuth(ClientAuth {
+            database_key: PARSED.db_key_for_pod.clone(),
+        }))?,
+    );
 
-    let mut conn = get_db_connection(&owner_key, init_db, &database_key).await?;
+    let mut conn = db_connection(init_db).await?;
 
     // Remove PluginRun from the DB before starting new plugins
     // Currently plugins have listeners that will detect lack of POD connection
@@ -59,7 +74,7 @@ pub async fn initialize(init_db: &InitDb) -> Result<()> {
         match internal_api::create_item(
             payload,
             &mut conn,
-            &owner_key,
+            owner_key,
             &database_key,
             PARSED.deref(),
         )
@@ -85,7 +100,7 @@ async fn clean_db_from_plugins(conn: &mut AsyncConnection) -> Result<()> {
             ..Default::default()
         };
 
-        let plugins = search(&tx, &schema, query)?
+        let plugins = internal_api::search(&tx, &schema, query)?
             .into_iter()
             .map(|element| {
                 serde_json::from_value::<ItemBase>(element).unwrap_or_else(|err| {
@@ -99,7 +114,7 @@ async fn clean_db_from_plugins(conn: &mut AsyncConnection) -> Result<()> {
         for plugin in plugins {
             debug!("Removing {}", plugin.id);
 
-            if let Err(e) = dangerous_permament_remove_item(&tx, plugin.rowid) {
+            if let Err(e) = database_api::dangerous_permament_remove_item(&tx, plugin.rowid) {
                 warn!(
                     "Failed to remove PluginRun with id {}, reason {e:?}",
                     plugin.id
diff --git a/libpod/src/test_helpers.rs b/libpod/src/test_helpers.rs
index 03f17e26cf0f9883500ec33fdeae16d72fc0beda..ec83e605ee29a1a3877d5603c34203f3555bdf78 100644
--- a/libpod/src/test_helpers.rs
+++ b/libpod/src/test_helpers.rs
@@ -45,8 +45,8 @@ pub fn default_cli() -> CliOptions {
         email_smtp_port: 465,
         email_smtp_user: None,
         email_smtp_password: None,
-        owner_key_for_pod: None,
-        db_key_for_pod: None,
+        owner_key_for_pod: Default::default(),
+        db_key_for_pod: Default::default(),
         shared_plugins: Vec::new(),
         opened_connections: None,
     }
diff --git a/libpod/src/user_account.rs b/libpod/src/user_account.rs
new file mode 100644
index 0000000000000000000000000000000000000000..847cbe3984f8e2639dc1d364bb28ffae6006d1e1
--- /dev/null
+++ b/libpod/src/user_account.rs
@@ -0,0 +1,292 @@
+use std::collections::HashMap;
+
+use argon2::password_hash::{Salt, SaltString};
+use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
+use rand::Rng;
+use secrecy::{ExposeSecret, Secret};
+use serde_json::json;
+use sha2::{Digest, Sha256};
+use tracing::{debug, info, instrument};
+
+use crate::api_model::{
+    AuthKey, ClientAuth, CreateItem, PodCredentials, PodOwner, RegisterVerifyAccountReq, Search,
+    SendEmail, UserAccountCredentials,
+};
+use crate::async_db_connection::{AsyncConnection, AsyncTx};
+use crate::command_line_interface::CliOptions;
+use crate::database_pool::{initialize_db, InitDb};
+use crate::db_model::{
+    ItemWithBase, PodUserAccount, RegisterState, CODE, LOGIN_HASH, PASSWORD_HASH, POD_ACCOUNT,
+    SALT, STATE,
+};
+use crate::email::send_email;
+use crate::error::{ErrorContext, Result};
+use crate::plugin_auth_crypto::auth_to_database_key;
+use crate::{
+    bad_request, database_api, database_utils, internal_api, internal_error, shared_state,
+};
+
+#[instrument(fields(login=%body.login), skip_all)]
+pub async fn register(
+    cli: &CliOptions,
+    init_db: &InitDb,
+    body: &UserAccountCredentials,
+) -> Result<RegisterState> {
+    let mut conn = shared_state::db_connection(init_db).await?;
+    if let Some(acc) = get_account_from_db(&mut conn, &body.login).await? {
+        Err(bad_request!(
+            "Account already exists, status {:?}",
+            acc.item.state
+        ))
+    } else {
+        let mut rnd = rand::thread_rng();
+
+        let code: Vec<i32> = (0..4).map(|_| rnd.gen_range(0..10)).collect();
+
+        let email = SendEmail {
+            to: (*body.login).to_string(),
+            subject: "Create new account".to_string(),
+            body: format!(
+                "Hey, use this token to continue with registration {} {} {} {}",
+                code[0], code[1], code[2], code[3]
+            ),
+        };
+
+        send_email(email, cli).await?;
+
+        let password_hash = hash_password(body.password.clone()).await?;
+        let login_hash = hex::encode(Sha256::new_with_prefix(body.login.as_bytes()).finalize());
+
+        let code_str = format!("{}{}{}{}", code[0], code[1], code[2], code[3]);
+
+        let salt_for_db_keys = SaltString::generate(&mut rand::thread_rng());
+        let salt_string = salt_for_db_keys.to_string();
+        create_account_in_db(
+            &mut conn,
+            password_hash,
+            &salt_string,
+            &login_hash,
+            &code_str,
+            cli,
+        )
+        .await
+    }
+}
+
+#[instrument(fields(login=%body.login), skip_all)]
+pub async fn verify(init_db: &InitDb, body: &RegisterVerifyAccountReq) -> Result<()> {
+    let mut conn = shared_state::db_connection(init_db).await?;
+    if let Some(mut acc) = get_account_from_db(&mut conn, &body.login).await? {
+        // Account already exists
+        if acc.item.state == RegisterState::VerifyEmailSent {
+            debug!("Verifying the account");
+            if &acc.item.code == body.code.expose_secret() {
+                debug!("Registration completed");
+                acc.item.state = RegisterState::RegistrationComplete;
+                update_account_in_db(&mut conn, &acc).await
+            } else {
+                Err(bad_request!("Invalid token provided"))
+            }
+        } else {
+            Err(bad_request!(
+                "Cannot verify account, invalid state {:?}",
+                acc.item.state
+            ))
+        }
+    } else {
+        Err(bad_request!("Account does not exist"))
+    }
+}
+
+/// Get POD keys from user credentials
+#[instrument(fields(login=%body.login), skip_all)]
+pub async fn get_pod_keys(
+    init_db: &InitDb,
+    body: &UserAccountCredentials,
+) -> Result<PodCredentials> {
+    debug!("Deriving keys");
+
+    let mut conn = shared_state::db_connection(init_db).await?;
+    let Some(acc) = get_account_from_db(&mut conn, &body.login).await? else {
+        return Err(bad_request!("Account does not exist"));
+    };
+
+    if acc.item.state != RegisterState::RegistrationComplete {
+        return Err(bad_request!("Account is not verified"));
+    }
+
+    validate_password(body.password.clone(), acc.item.password_hash.clone()).await?;
+
+    let salt = SaltString::from_b64(&acc.item.salt).expect("Invalid salt format");
+    let owner_key = derive_pod_key(Secret::new(body.login.to_string()), salt.clone()).await?;
+    let database_key = derive_pod_key(body.password.clone(), salt).await?;
+
+    Ok(PodCredentials {
+        owner_key: PodOwner::from(owner_key.expose_secret().clone()),
+        database_key: database_key.expose_secret().clone(),
+    })
+}
+#[instrument(fields(owner=%body.owner_key), skip_all)]
+pub async fn open_pod(init_db: &InitDb, body: PodCredentials) -> Result<PodOwner> {
+    info!("Opening POD DB");
+    database_utils::check_owner(&body.owner_key)?;
+
+    let database_key = auth_to_database_key(AuthKey::ClientAuth(ClientAuth {
+        database_key: body.database_key,
+    }))?;
+
+    initialize_db(&body.owner_key, init_db, &database_key).await?;
+
+    debug!("POD DB ready");
+
+    Ok(body.owner_key)
+}
+
+async fn validate_password(password: Secret<String>, expected_hash: String) -> Result<()> {
+    tokio::task::spawn_blocking(move || {
+        Argon2::default()
+            .verify_password(
+                password.expose_secret().as_bytes(),
+                &PasswordHash::new(&expected_hash).expect("Invalid PHC string format"),
+            )
+            .map_err(|e| bad_request!("Invalid password {e}"))?;
+        Ok(())
+    })
+    .await?
+}
+
+/// Generate POD key from the input and provided salt. Returns hexstring.
+async fn derive_pod_key(input: Secret<String>, salt: SaltString) -> Result<Secret<String>> {
+    tokio::task::spawn_blocking(move || {
+        let mut raw_key = [0u8; 32];
+        let mut raw_salt = [0u8; Salt::RECOMMENDED_LENGTH];
+        salt.decode_b64(&mut raw_salt)
+            .map_err(|e| internal_error!("While decoding salt {e}"))?;
+
+        // https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html
+        Argon2::default()
+            .hash_password_into(input.expose_secret().as_bytes(), &raw_salt, &mut raw_key)
+            .map_err(|e| internal_error!("While deriving the pod key {}", e))?;
+
+        Ok(Secret::new(hex::encode(raw_key)))
+    })
+    .await?
+}
+
+/// Generate argon2 hash with unique salt. Returns PHC string.
+async fn hash_password(password: Secret<String>) -> Result<String> {
+    tokio::task::spawn_blocking(move || {
+        let salt = SaltString::generate(&mut rand::thread_rng());
+        // https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html
+        let phc_hash = Argon2::default()
+            .hash_password(password.expose_secret().as_bytes(), salt.as_salt())
+            .map_err(|e| internal_error!("While hashing the password {}", e))?
+            .to_string();
+
+        Ok(phc_hash)
+    })
+    .await?
+}
+
+async fn create_account_in_db(
+    conn: &mut AsyncConnection,
+    password_hash: String,
+    salt: &str,
+    login_hash: &str,
+    code: &str,
+    cli: &CliOptions,
+) -> Result<RegisterState> {
+    conn.in_write_transaction(|tx: AsyncTx| async move {
+        let mut schema = database_api::get_schema(&tx)?;
+        let state = RegisterState::VerifyEmailSent;
+        let item = CreateItem {
+            _type: POD_ACCOUNT.to_string(),
+            fields: json!({
+                LOGIN_HASH: login_hash,
+                PASSWORD_HASH: password_hash,
+                SALT: salt,
+                CODE: code,
+                STATE: state,
+            })
+            .as_object()
+            .expect("Invalid json object for PodAccount")
+            .clone()
+            .into_iter()
+            .collect(),
+            ..Default::default()
+        };
+
+        let id = internal_api::create_item_tx(&tx, &mut schema, item, "", cli).await?;
+        debug!("PodAccount created {id}");
+        Ok(state)
+    })
+    .await
+}
+
+async fn get_account_from_db(
+    conn: &mut AsyncConnection,
+    login: &str,
+) -> Result<Option<ItemWithBase<PodUserAccount>>> {
+    let login_hash = hex::encode(Sha256::new_with_prefix(login.as_bytes()).finalize());
+
+    let mut res = conn
+        .in_read_transaction(|tx: AsyncTx| async move {
+            let schema = database_api::get_schema(&tx)?;
+
+            let query = Search {
+                _type: Some(POD_ACCOUNT.to_string()),
+                deleted: Some(false),
+                limit: u64::MAX,
+                other_properties: HashMap::from([(
+                    LOGIN_HASH.to_string(),
+                    serde_json::to_value(login_hash).expect("Cannot serialize email_hash to Value"),
+                )]),
+                ..Default::default()
+            };
+
+            internal_api::search(&tx, &schema, query)
+        })
+        .await?;
+
+    debug_assert!(
+        res.len() <= 1,
+        "There is more than one PodAccount entry in the database for {:?}",
+        login
+    );
+
+    if let Some(value) = res.pop() {
+        let pod_account: ItemWithBase<PodUserAccount> = serde_json::from_value(value)
+            .context(|| format!("Cannot deserialize PodAccount for {:?}", login))?;
+
+        debug!("Found PodAccount item {:#?}", pod_account);
+        Ok(Some(pod_account))
+    } else {
+        Ok(None)
+    }
+}
+
+async fn update_account_in_db(
+    conn: &mut AsyncConnection,
+    acc: &ItemWithBase<PodUserAccount>,
+) -> Result<()> {
+    conn.in_write_transaction(|tx: AsyncTx| async move {
+        let schema = database_api::get_schema(&tx)?;
+
+        internal_api::update_item_tx(
+            &tx,
+            &schema,
+            &acc.base.id,
+            serde_json::to_value(acc.item.clone())
+                .expect("Unable to convert PodAccount to Value")
+                .as_object()
+                .expect("Invalid json object for PodAccount")
+                .clone()
+                .into_iter()
+                .collect(),
+        )?;
+
+        debug!("PodAccount {} updated", acc.item.login_hash);
+        Ok(())
+    })
+    .await
+}
diff --git a/pod/Cargo.toml b/pod/Cargo.toml
index 57785d35eef823c8e1f15eea879fd43491f0e21c..7881d1e1cb76d335f6e092bce495c85f01030f97 100644
--- a/pod/Cargo.toml
+++ b/pod/Cargo.toml
@@ -76,8 +76,8 @@ required-features = ["include_slow_tests"]
 
 
 [[test]]
-name = "test_create_account"
-path = "tests/test_create_account.rs"
+name = "test_pod_keys"
+path = "tests/test_pod_keys.rs"
 required-features = ["include_slow_tests"]
 
 
diff --git a/pod/src/actix_api.rs b/pod/src/actix_api.rs
index 4b8898170c9476851de6aca2653534bffb0ce80f..b6d66183575bdc5aeeed8ebff588ed30717fdad2 100644
--- a/pod/src/actix_api.rs
+++ b/pod/src/actix_api.rs
@@ -1,9 +1,10 @@
 use crate::actix_endpoints::{
-    bulk, create_account, create_edge, create_item, delete_edge_by_source_target, delete_item,
-    delete_user, get_edges, get_file, get_item, get_logs, graphql, not_found, oauth1_access_token,
-    oauth1_request_token, oauth2_access_token, oauth2_auth_url, oauth2_authorize, plugin_api,
-    plugin_api_call, plugin_attach, plugins_status, search, trace_filter, trigger_status,
-    update_item, upload_file, upload_file_b, version,
+    account, account_derive_pod_keys, account_register, account_verify, bulk, create_edge,
+    create_item, delete_edge_by_source_target, delete_item, delete_pod, get_edges, get_file,
+    get_item, get_logs, graphql, not_found, oauth1_access_token, oauth1_request_token,
+    oauth2_access_token, oauth2_auth_url, oauth2_authorize, open_pod, plugin_api, plugin_api_call,
+    plugin_attach, plugins_status, search, send_email, trace_filter, trigger_status, update_item,
+    upload_file, upload_file_b, version,
 };
 use actix_cors::Cors;
 use actix_web::{
@@ -15,7 +16,7 @@ use libpod::{
     command_line_interface::CliOptions,
     database_pool::ConnectionPool,
     error::{ErrorContext, Result},
-    internal_error, shared_plugins,
+    internal_error, shared_state,
 };
 use rustls::{Certificate, PrivateKey, ServerConfig};
 use rustls_pemfile::{certs, pkcs8_private_keys};
@@ -43,7 +44,7 @@ pub async fn run_server<S: 'static>(
         HashMap::<String, ConnectionPool>::new(),
     ));
 
-    let shared_plugin_db = init_db.clone();
+    let shared_state = init_db.clone();
 
     let cli_options = web::Data::new(cli_options);
     let trace_handle = web::Data::new(trace_handle);
@@ -65,6 +66,7 @@ pub async fn run_server<S: 'static>(
             .route("/trace_filter", web::post().to(trace_filter::<S>))
             .service(
                 web::scope("/v4")
+                    .service(send_email)
                     .service(create_item)
                     .service(get_item)
                     .service(oauth1_request_token)
@@ -75,8 +77,9 @@ pub async fn run_server<S: 'static>(
                     .service(update_item)
                     .service(bulk)
                     .service(delete_item)
-                    .service(delete_user)
-                    .service(create_account)
+                    .service(delete_pod)
+                    .service(open_pod)
+                    .service(account)
                     .service(create_edge)
                     .service(delete_edge_by_source_target)
                     .service(get_edges)
@@ -109,7 +112,10 @@ pub async fn run_server<S: 'static>(
                             .service(plugin_attach),
                     )
                     .service(get_logs)
-                    .service(trigger_status),
+                    .service(trigger_status)
+                    .service(account_register)
+                    .service(account_verify)
+                    .service(account_derive_pod_keys),
             )
     });
 
@@ -165,10 +171,10 @@ pub async fn run_server<S: 'static>(
         Ok(())
     });
 
-    // Note initializing shared plugins must happen after webserver is up and ready,
+    // Note initializing shared state and plugins in particular must happen after webserver is up and ready,
     // otherwise starting plugin might not be able to reach the pod - RC for the webserver
-    if let Err(e) = shared_plugins::initialize(shared_plugin_db.deref()).await {
-        warn!("Failed to initialize shared plugins: {e}");
+    if let Err(e) = shared_state::initialize(shared_state.deref()).await {
+        warn!("Failed to initialize shared state: {e}");
     }
 
     server_handle.await?
diff --git a/pod/src/actix_endpoints.rs b/pod/src/actix_endpoints.rs
index f0790d7ce502a8ef2f9b481e794b87ee06501703..cbd82a2e0113277dc784627fb907641520af3036 100644
--- a/pod/src/actix_endpoints.rs
+++ b/pod/src/actix_endpoints.rs
@@ -169,26 +169,81 @@ pub async fn delete_item(
 }
 
 #[instrument(fields(uid=trace_uid(), %owner), skip_all)]
-#[post("{owner}/delete_user")]
-pub async fn delete_user(
+#[post("{owner}/delete_pod")]
+pub async fn delete_pod(
     owner: web::Path<PodOwner>,
     init_db: web::Data<InitDb>,
     body: web::Bytes,
 ) -> actix_web::Result<impl Responder> {
     let body = extract_json(&body)?;
-    let result = pod_handlers::delete_user(owner.to_owned(), &init_db.to_owned(), body).await;
+    let result = pod_handlers::delete_pod(owner.to_owned(), &init_db.to_owned(), body).await;
     let result = result.map(|()| web::Json(serde_json::json!({})));
     respond_with_result(result)
 }
 
+// TODO: deprecated
 #[instrument(fields(uid=trace_uid()), skip_all)]
 #[post("/account")]
-pub async fn create_account(
+pub async fn account(
+    init_db: web::Data<InitDb>,
+    body: web::Bytes,
+) -> actix_web::Result<impl Responder> {
+    do_open_pod(init_db, body).await
+}
+
+#[post("/account/pod/open")]
+pub async fn open_pod(
+    init_db: web::Data<InitDb>,
+    body: web::Bytes,
+) -> actix_web::Result<impl Responder> {
+    do_open_pod(init_db, body).await
+}
+
+// TODO: current implementation allows everyone to reach this endpoint and
+// do as many pods as you like. Changing this will break the pymemri and plugins tests.
+async fn do_open_pod(
+    init_db: web::Data<InitDb>,
+    body: web::Bytes,
+) -> actix_web::Result<impl Responder> {
+    let body = extract_json(&body)?;
+    let result = pod_handlers::open_pod(&init_db.into_inner(), body).await;
+    let result = result.map(|result| web::Json(serde_json::json!(result)));
+    respond_with_result(result)
+}
+
+#[instrument(fields(uid=trace_uid()), skip_all)]
+#[post("/account/register")]
+pub async fn account_register(
+    cli: web::Data<CliOptions>,
+    init_db: web::Data<InitDb>,
+    body: web::Bytes,
+) -> actix_web::Result<impl Responder> {
+    let body = extract_json(&body)?;
+    let result = pod_handlers::account_register(&cli, &init_db.into_inner(), body).await;
+    let result = result.map(|result| web::Json(serde_json::json!(result)));
+    respond_with_result(result)
+}
+
+#[instrument(fields(uid=trace_uid()), skip_all)]
+#[post("/account/verify")]
+pub async fn account_verify(
+    init_db: web::Data<InitDb>,
+    body: web::Bytes,
+) -> actix_web::Result<impl Responder> {
+    let body = extract_json(&body)?;
+    let result = pod_handlers::account_verify(&init_db.into_inner(), body).await;
+    let result = result.map(|result| web::Json(serde_json::json!(result)));
+    respond_with_result(result)
+}
+
+#[instrument(fields(uid=trace_uid()), skip_all)]
+#[post("/account/derive_pod_keys")]
+pub async fn account_derive_pod_keys(
     init_db: web::Data<InitDb>,
     body: web::Bytes,
 ) -> actix_web::Result<impl Responder> {
     let body = extract_json(&body)?;
-    let result = pod_handlers::create_account(&init_db.into_inner(), body).await;
+    let result = pod_handlers::account_derive_pod_keys(&init_db.into_inner(), body).await;
     let result = result.map(|result| web::Json(serde_json::json!(result)));
     respond_with_result(result)
 }
diff --git a/pod/src/pod_handlers.rs b/pod/src/pod_handlers.rs
index ce46aa1fa7e0396ccc5795dfdff5e8c722e01624..50d0f65c370954b17563313239ec9e4221092790 100644
--- a/pod/src/pod_handlers.rs
+++ b/pod/src/pod_handlers.rs
@@ -5,25 +5,25 @@ use http::StatusCode;
 use libpod::{
     any_error,
     api_model::{
-        AuthKey, Bulk, BulkResponse, ClientAuth, CreateEdge, CreateItem, CreateUserReq,
-        DeleteEdgeBySourceTarget, GetEdges, GetFile, Oauth2AccessTokenRequest,
-        Oauth2AccessTokenResponse, Oauth2AuthUrlRequest, Oauth2AuthUrlResponse,
-        Oauth2AuthorizeTokenRequest, OauthAccessTokenPayload, OauthRequestTokenPayload,
-        PayloadWrapper, PluginAttachReq, PluginCallApiReq, PluginCallApiRes, PluginGetApiReq,
-        PluginGetApiRes, PluginStatusReq, PluginStatusRes, PodOwner, Search, SendEmail,
-        TraceRequest, UpdateItem,
+        AuthKey, Bulk, BulkResponse, CreateEdge, CreateItem, DeleteEdgeBySourceTarget, GetEdges,
+        GetFile, Oauth2AccessTokenRequest, Oauth2AccessTokenResponse, Oauth2AuthUrlRequest,
+        Oauth2AuthUrlResponse, Oauth2AuthorizeTokenRequest, OauthAccessTokenPayload,
+        OauthRequestTokenPayload, PayloadWrapper, PluginAttachReq, PluginCallApiReq,
+        PluginCallApiRes, PluginGetApiReq, PluginGetApiRes, PluginStatusReq, PluginStatusRes,
+        PodCredentials, PodOwner, RegisterVerifyAccountReq, Search, SendEmail, TraceRequest,
+        UpdateItem, UserAccountCredentials,
     },
     async_db_connection::AsyncTx,
     bad_request,
     command_line_interface::CliOptions,
     database_api::{self},
-    database_pool::{initialize_db, InitDb},
+    database_pool::InitDb,
     database_utils, email,
     error::Result,
     error::{ErrorContext, ErrorType},
     file_api, internal_api, internal_error, oauth1_api, oauth2_api,
     plugin_auth_crypto::{auth_to_database_key, DatabaseKey},
-    plugin_run, shared_plugins, triggers,
+    plugin_run, shared_state, triggers, user_account,
 };
 use serde_json::Value;
 use sha2::{Digest, Sha256};
@@ -31,7 +31,7 @@ use std::{
     collections::HashMap,
     sync::{atomic::AtomicU16, Arc},
 };
-use tokio::task;
+
 use tracing::{debug, info, warn};
 use tracing_subscriber::{reload::Handle, EnvFilter};
 
@@ -209,7 +209,7 @@ pub async fn delete_item(
     .await
 }
 #[inline(always)]
-pub async fn delete_user(
+pub async fn delete_pod(
     owner: PodOwner,
     init_db: &InitDb,
     body: PayloadWrapper<String>,
@@ -220,21 +220,42 @@ pub async fn delete_user(
     database_utils::check_owner_and_delete_db(&owner, init_db, &database_key).await?;
     Result::Ok(())
 }
+
 #[inline(always)]
-pub async fn create_account(init_db: &InitDb, body: CreateUserReq) -> Result<PodOwner> {
-    info!("Creating account for {}", body.owner_key);
-    database_utils::check_owner(&body.owner_key)?;
+pub async fn open_pod(init_db: &InitDb, body: PodCredentials) -> Result<PodOwner> {
+    user_account::open_pod(init_db, body).await
+}
 
-    let database_key = auth_to_database_key(AuthKey::ClientAuth(ClientAuth {
-        database_key: body.database_key,
-    }))?;
+#[inline(always)]
+pub async fn account_register(
+    cli: &CliOptions,
+    init_db: &InitDb,
+    body: UserAccountCredentials,
+) -> Result<()> {
+    info!("Register account for {:?}", body.login);
 
-    initialize_db(&body.owner_key, init_db, &database_key).await?;
+    user_account::register(cli, init_db, &body).await?;
 
     debug!("Account created");
 
-    Ok(body.owner_key)
+    Ok(())
+}
+
+#[inline(always)]
+pub async fn account_verify(init_db: &InitDb, body: RegisterVerifyAccountReq) -> Result<()> {
+    info!("Verify account for {:?}", body.login);
+
+    user_account::verify(init_db, &body).await
+}
+
+#[inline(always)]
+pub async fn account_derive_pod_keys(
+    init_db: &InitDb,
+    body: UserAccountCredentials,
+) -> Result<PodCredentials> {
+    user_account::get_pod_keys(init_db, &body).await
 }
+
 #[inline(always)]
 pub async fn create_edge(
     owner: PodOwner,
@@ -410,12 +431,14 @@ pub async fn send_email(
 ) -> Result<()> {
     let auth = body.auth;
     let payload = body.payload;
+
+    // Get conn to validate creds
     let database_key = auth_to_database_key(auth)?;
     let _conn =
         database_utils::check_owner_and_initialize_db(&owner, init_db, &database_key).await?;
 
     let cli = cli.clone();
-    task::spawn_blocking(move || email::send_email(payload, &cli)).await?
+    email::send_email(payload, &cli).await
 }
 
 #[inline(always)]
@@ -459,11 +482,9 @@ pub async fn plugins_status(
 
     let plugins = plugin_run::get_plugins_status(&mut conn, cli, &body.payload).await?;
 
-    let shared_plugins = if let Some(mut conn) = shared_plugins::db_connection(init_db).await? {
-        plugin_run::get_plugins_status(&mut conn, cli, &body.payload).await?
-    } else {
-        HashMap::new()
-    };
+    let mut conn = shared_state::db_connection(init_db).await?;
+
+    let shared_plugins = plugin_run::get_plugins_status(&mut conn, cli, &body.payload).await?;
 
     Ok(PluginStatusRes {
         plugins,
@@ -492,9 +513,8 @@ pub async fn plugin_api(
 
     if plugin_not_found {
         // Plugin not found, maybe it's shared plugin?
-        if let Some(mut conn) = shared_plugins::db_connection(init_db).await? {
-            api = plugin_run::get_plugin_api(&mut conn, cli, &body.payload).await;
-        }
+        let mut conn = shared_state::db_connection(init_db).await?;
+        api = plugin_run::get_plugin_api(&mut conn, cli, &body.payload).await;
     }
 
     api
@@ -521,9 +541,8 @@ pub async fn plugin_api_call(
 
     if plugin_not_found {
         // Plugin not found, maybe it's shared plugin?
-        if let Some(mut conn) = shared_plugins::db_connection(init_db).await? {
-            api_response = plugin_run::call_plugin_api(&mut conn, cli, &body.payload).await
-        }
+        let mut conn = shared_state::db_connection(init_db).await?;
+        api_response = plugin_run::call_plugin_api(&mut conn, cli, &body.payload).await;
     }
 
     api_response
diff --git a/pod/tests/common/pod_client.rs b/pod/tests/common/pod_client.rs
index f1b3b67cecc635b21eb3dc5faad71008a8932bef..cb2b92f2982bd853645c7c55eda74571f0f1aaf9 100644
--- a/pod/tests/common/pod_client.rs
+++ b/pod/tests/common/pod_client.rs
@@ -31,12 +31,10 @@ impl PodClient {
     }
 
     /// Convenient function to do POST request with given payload to the POD
-    pub async fn post_to<T>(&self, payload: T, endpoint: &str) -> Response
+    pub async fn post_to_with_owner<T>(&self, payload: T, endpoint: &str) -> Response
     where
         T: Serialize + Debug,
     {
-        debug!("POST: {endpoint}, body {payload:#?}");
-
         let body = PayloadWrapper {
             auth: AuthKey::ClientAuth(libpod::api_model::ClientAuth {
                 database_key: self.database_key.clone(),
@@ -44,12 +42,19 @@ impl PodClient {
             payload,
         };
 
+        self.post_to(body, &format!("{}/{endpoint}", self.owner_key))
+            .await
+    }
+
+    pub async fn post_to<T>(&self, payload: T, endpoint: &str) -> Response
+    where
+        T: Serialize + Debug,
+    {
+        debug!("POST: {endpoint}, body {payload:#?}");
+
         self.client
-            .post(&format!(
-                "{}/v4/{}/{endpoint}",
-                self.pod_url, self.owner_key
-            ))
-            .json(&body)
+            .post(&format!("{}/v4/{endpoint}", self.pod_url))
+            .json(&payload)
             .send()
             .await
             .unwrap()
diff --git a/pod/tests/common/pod_request.rs b/pod/tests/common/pod_request.rs
index c3c82f177f341b0daa2493dfdd90ff2f8959dedc..189a4bc6cc5fac2ea9fbe18ee5499256266bae7a 100644
--- a/pod/tests/common/pod_request.rs
+++ b/pod/tests/common/pod_request.rs
@@ -34,7 +34,7 @@ pub async fn register_account(client: &PodClient) {
 pub async fn migrate_plugin_definition(client: &PodClient) {
     let plugin_def = serde_json::from_str::<Bulk>(include_str!("plugin_migration.json")).unwrap();
 
-    let resp = client.post_to(plugin_def, "bulk").await;
+    let resp = client.post_to_with_owner(plugin_def, "bulk").await;
     assert_eq!(resp.status(), reqwest::StatusCode::OK);
 }
 
@@ -62,7 +62,7 @@ pub async fn start_plugin(ctx: &mut TestData) -> (String, String) {
           }
     );
 
-    let resp = ctx.pod_client.post_to(payload, "bulk").await;
+    let resp = ctx.pod_client.post_to_with_owner(payload, "bulk").await;
     assert_eq!(resp.status(), reqwest::StatusCode::OK);
 
     // Starting container takes time...
@@ -134,7 +134,9 @@ pub async fn start_plugin(ctx: &mut TestData) -> (String, String) {
 pub async fn delete_plugin(pod_client: &PodClient) {
     let plugin_id = "4fa2094c-51d6-4874-a135-9017fcdedf16";
 
-    let resp = pod_client.post_to(plugin_id, "delete_item").await;
+    let resp = pod_client
+        .post_to_with_owner(plugin_id, "delete_item")
+        .await;
     assert_eq!(resp.status(), reqwest::StatusCode::OK);
 
     let plugin_item = get_db_item(plugin_id, pod_client).await;
@@ -178,7 +180,7 @@ pub async fn register_trigger(client: &PodClient) -> String {
       ]
     });
 
-    let resp = client.post_to(payload, "bulk").await;
+    let resp = client.post_to_with_owner(payload, "bulk").await;
     assert_eq!(resp.status(), reqwest::StatusCode::OK);
 
     trigger_id
@@ -192,7 +194,7 @@ pub async fn send_fake_msg(client: &PodClient) {
         "dateSent": 1655170839993u64
     });
 
-    let resp = client.post_to(payload, "create_item").await;
+    let resp = client.post_to_with_owner(payload, "create_item").await;
     assert_eq!(resp.status(), reqwest::StatusCode::OK);
 }
 
@@ -202,7 +204,7 @@ pub async fn get_db_item(item_id: &str, client: &PodClient) -> Value {
         limit: u64::MAX,
         ..Default::default()
     };
-    let resp = client.post_to(payload, "search").await;
+    let resp = client.post_to_with_owner(payload, "search").await;
 
     resp.json::<Vec<Value>>()
         .await
diff --git a/pod/tests/common/test_data.rs b/pod/tests/common/test_data.rs
index 4c2dd692886a754d772b493471f25aaa180f7fce..9ca330eca1d61f5735024ea474ac42d66d54b714 100644
--- a/pod/tests/common/test_data.rs
+++ b/pod/tests/common/test_data.rs
@@ -66,7 +66,7 @@ fn get_pod_path_executable() -> PathBuf {
         build_mode = "release";
     }
 
-    // From root path go to partent workspace root, where target dir is present
+    // From root path go to parent workspace root, where target dir is present
     root_path()
         .parent()
         .unwrap()
diff --git a/pod/tests/test_create_item.rs b/pod/tests/test_create_item.rs
index 52584febecafa6ee2e683a01fe6d70d0d600b742..ec113c836c0ba2c35902785b88eabeb0672f6fa5 100644
--- a/pod/tests/test_create_item.rs
+++ b/pod/tests/test_create_item.rs
@@ -18,7 +18,10 @@ async fn test_cannot_create_item_outside_schema(ctx: &mut TestData) {
         "dateSent": 1655170839993u64
     });
 
-    let resp = ctx.pod_client.post_to(payload, "create_item").await;
+    let resp = ctx
+        .pod_client
+        .post_to_with_owner(payload, "create_item")
+        .await;
     assert_eq!(resp.status(), reqwest::StatusCode::BAD_REQUEST);
     assert_eq!(
         resp.json::<String>().await.unwrap(),
@@ -39,7 +42,10 @@ async fn test_cannot_create_item_with_props_outside_schema(ctx: &mut TestData) {
 
     });
 
-    let resp = ctx.pod_client.post_to(payload, "create_item").await;
+    let resp = ctx
+        .pod_client
+        .post_to_with_owner(payload, "create_item")
+        .await;
     assert_eq!(resp.status(), reqwest::StatusCode::BAD_REQUEST);
     assert!(resp
         .json::<String>()
@@ -64,7 +70,10 @@ async fn test_cannot_create_item_with_props_defined_for_other_object(ctx: &mut T
 
     });
 
-    let resp = ctx.pod_client.post_to(payload, "create_item").await;
+    let resp = ctx
+        .pod_client
+        .post_to_with_owner(payload, "create_item")
+        .await;
     assert_eq!(resp.status(), reqwest::StatusCode::BAD_REQUEST);
     assert!(resp.json::<String>().await.unwrap().contains(
         "Failure: Error 400 Bad Request, Property triggerOn not defined in Schema for PluginRun"
diff --git a/pod/tests/test_plugin.rs b/pod/tests/test_plugin.rs
index 2c15a1190d2c15264e17b83c8298848970f1c4a1..6ffe47aed0346927b8052726f74456777449d60e 100644
--- a/pod/tests/test_plugin.rs
+++ b/pod/tests/test_plugin.rs
@@ -57,7 +57,7 @@ async fn test_start_plugin_by_bulk(ctx: &mut TestData) {
         ]
     });
 
-    let resp = ctx.pod_client.post_to(payload, "bulk").await;
+    let resp = ctx.pod_client.post_to_with_owner(payload, "bulk").await;
 
     assert_eq!(resp.status(), reqwest::StatusCode::BAD_REQUEST);
 
diff --git a/pod/tests/test_plugin_api.rs b/pod/tests/test_plugin_api.rs
index 1d6d0734ff5fb92a70b9ad76daedca21d7bd7ef4..ba395d34ce2b49454f9fec6ff1fe95c4354f5ce2 100644
--- a/pod/tests/test_plugin_api.rs
+++ b/pod/tests/test_plugin_api.rs
@@ -23,7 +23,10 @@ async fn test_plugin_api(ctx: &mut TestData) {
 async fn test_plugin_get_api(id: String, alias: String, ctx: &TestData) {
     let payload = json!({ "id": id });
 
-    let response = ctx.pod_client.post_to(payload, "plugin/api").await;
+    let response = ctx
+        .pod_client
+        .post_to_with_owner(payload, "plugin/api")
+        .await;
 
     assert!(response.status().is_success());
 
@@ -57,7 +60,10 @@ async fn test_plugin_get_api(id: String, alias: String, ctx: &TestData) {
     // Same, but using alias
     let payload = json!({ "id": alias });
 
-    let response = ctx.pod_client.post_to(payload, "plugin/api").await;
+    let response = ctx
+        .pod_client
+        .post_to_with_owner(payload, "plugin/api")
+        .await;
 
     assert!(response.status().is_success());
 
@@ -94,7 +100,10 @@ async fn test_plugin_get_api(id: String, alias: String, ctx: &TestData) {
         }
     );
 
-    let response = ctx.pod_client.post_to(payload, "plugin/api").await;
+    let response = ctx
+        .pod_client
+        .post_to_with_owner(payload, "plugin/api")
+        .await;
 
     assert!(!response.status().is_success());
 
@@ -132,7 +141,10 @@ async fn test_plugin_call_api(id: String, alias: String, ctx: &TestData) {
       }
     });
 
-    let response = ctx.pod_client.post_to(payload, "plugin/api/call").await;
+    let response = ctx
+        .pod_client
+        .post_to_with_owner(payload, "plugin/api/call")
+        .await;
 
     assert!(response.status().is_success());
 
@@ -181,7 +193,10 @@ async fn test_plugin_call_api(id: String, alias: String, ctx: &TestData) {
       }
     });
 
-    let response = ctx.pod_client.post_to(payload, "plugin/api/call").await;
+    let response = ctx
+        .pod_client
+        .post_to_with_owner(payload, "plugin/api/call")
+        .await;
 
     assert!(response.status().is_success());
 
@@ -230,7 +245,10 @@ async fn test_plugin_call_api(id: String, alias: String, ctx: &TestData) {
       }
     });
 
-    let response = ctx.pod_client.post_to(payload, "plugin/api/call").await;
+    let response = ctx
+        .pod_client
+        .post_to_with_owner(payload, "plugin/api/call")
+        .await;
 
     assert!(response.status().is_success());
 
@@ -255,7 +273,10 @@ async fn test_plugin_call_api(id: String, alias: String, ctx: &TestData) {
         }
     );
 
-    let response = ctx.pod_client.post_to(payload, "plugin/api/call").await;
+    let response = ctx
+        .pod_client
+        .post_to_with_owner(payload, "plugin/api/call")
+        .await;
 
     assert!(!response.status().is_success());
 
diff --git a/pod/tests/test_plugin_state.rs b/pod/tests/test_plugin_state.rs
index 13f887fe9c9f7194e057272e523903c2d3c91252..c63ff84ae5c83620e640d03ec6b6087afddb7996 100644
--- a/pod/tests/test_plugin_state.rs
+++ b/pod/tests/test_plugin_state.rs
@@ -20,7 +20,10 @@ async fn test_plugin_state(ctx: &mut TestData) {
         }
     );
 
-    let response = ctx.pod_client.post_to(payload, "plugin/status").await;
+    let response = ctx
+        .pod_client
+        .post_to_with_owner(payload, "plugin/status")
+        .await;
 
     assert!(response.status().is_success());
     assert_eq!(
@@ -42,7 +45,10 @@ async fn test_plugin_state(ctx: &mut TestData) {
         }
     );
 
-    let response = ctx.pod_client.post_to(payload, "plugin/status").await;
+    let response = ctx
+        .pod_client
+        .post_to_with_owner(payload, "plugin/status")
+        .await;
 
     assert!(response.status().is_success());
 
@@ -66,7 +72,10 @@ async fn test_plugin_state(ctx: &mut TestData) {
         }
     );
 
-    let response = ctx.pod_client.post_to(payload, "plugin/status").await;
+    let response = ctx
+        .pod_client
+        .post_to_with_owner(payload, "plugin/status")
+        .await;
 
     assert!(response.status().is_success());
 
@@ -87,7 +96,10 @@ async fn test_plugin_state(ctx: &mut TestData) {
         }
     );
 
-    let response = ctx.pod_client.post_to(payload, "plugin/status").await;
+    let response = ctx
+        .pod_client
+        .post_to_with_owner(payload, "plugin/status")
+        .await;
 
     assert!(response.status().is_success());
 
@@ -108,7 +120,10 @@ async fn test_plugin_state(ctx: &mut TestData) {
         }
     );
 
-    let response = ctx.pod_client.post_to(payload, "plugin/status").await;
+    let response = ctx
+        .pod_client
+        .post_to_with_owner(payload, "plugin/status")
+        .await;
 
     assert!(response.status().is_success());
 
@@ -124,7 +139,10 @@ async fn test_plugin_state(ctx: &mut TestData) {
         }
     );
 
-    let response = ctx.pod_client.post_to(payload, "plugin/status").await;
+    let response = ctx
+        .pod_client
+        .post_to_with_owner(payload, "plugin/status")
+        .await;
 
     assert!(response.status().is_success());
 
diff --git a/pod/tests/test_create_account.rs b/pod/tests/test_pod_keys.rs
similarity index 64%
rename from pod/tests/test_create_account.rs
rename to pod/tests/test_pod_keys.rs
index 85fa1cfa3e89129b5e6e9db1906b4faeb9187f07..41e8ca9cdd4366531e9dc4cf2ecaba5cff103ee7 100644
--- a/pod/tests/test_create_account.rs
+++ b/pod/tests/test_pod_keys.rs
@@ -16,7 +16,7 @@ async fn use_pod(client: &PodClient) -> Response {
         "valueType": "Text"
     });
 
-    client.post_to(payload, "create_item").await
+    client.post_to_with_owner(payload, "create_item").await
 }
 
 // The truth table of possible states.
@@ -29,14 +29,14 @@ async fn use_pod(client: &PodClient) -> Response {
 // 2 |  0       |         0                |       1                                 | db, and pool created
 // 3 |  0       |         1                |       1                                 | unreachable -> return internal server error
 // 4 |  0       |         1                |                               1         | unreachable -> will return rusqlite error upon trying to access the db
-// 5 |  1       |         0                |       1                                 | error -> user already exists
+// 5 |  1       |         0                |       1                                 | ignore -> user already exists
 // 6 |  1       |         0                |                               1         | create connection pool, validate credentials, proceed
-// 7 |  1       |         1                |       1                                 | error -> user already exists
+// 7 |  1       |         1                |       1                                 | ignore -> user already exists
 // 8 |  1       |         1                |                               1         | check credentials, proceed
 
 #[test_context(TestData)]
 #[tokio::test]
-async fn test_account_use_not_registered(ctx: &mut TestData) {
+async fn test_db_not_registered(ctx: &mut TestData) {
     let valid_credentials = PodClient::new(&ctx.database_key, &ctx.owner_key, &ctx.pod_url);
 
     // State #1, trying to use pod, while not yet registered
@@ -46,49 +46,25 @@ async fn test_account_use_not_registered(ctx: &mut TestData) {
 
 #[test_context(TestData)]
 #[tokio::test]
-async fn test_account_create(ctx: &mut TestData) {
-    let valid_credentials = PodClient::new(&ctx.database_key, &ctx.owner_key, &ctx.pod_url);
-
+async fn test_create_pod_db(ctx: &mut TestData) {
     // State #2, register from the list - the happy path
     let register_request = json!( {
-        "ownerKey":  valid_credentials.owner_key,
-        "databaseKey": valid_credentials.database_key
+        "ownerKey":  ctx.owner_key,
+        "databaseKey": ctx.database_key
 
     });
 
-    let res = ctx
-        .pod_client
-        .client
-        .post(format!("{}/v4/account", valid_credentials.pod_url))
-        .json(&register_request)
-        .send()
-        .await
-        .unwrap();
+    let res = ctx.pod_client.post_to(&register_request, "account").await;
 
     assert_eq!(res.status(), StatusCode::OK);
 
-    // Cannot register twice, state #7
-    let res = ctx
-        .pod_client
-        .client
-        .post(format!("{}/v4/account", valid_credentials.pod_url))
-        .json(&register_request)
-        .send()
-        .await
-        .unwrap();
+    // Cannot create pod twice, state #7
+    let res = ctx.pod_client.post_to(&register_request, "account").await;
 
-    assert_eq!(res.status(), StatusCode::BAD_REQUEST);
-
-    assert_eq!(
-        res.json::<String>().await.unwrap(),
-        format!(
-            "Failure: Error 400 Bad Request, Account for {} already exist",
-            valid_credentials.owner_key
-        )
-    );
+    assert_eq!(res.status(), StatusCode::OK);
 
     // Can use the POD, state #8
-    let res = use_pod(&valid_credentials).await;
+    let res = use_pod(&ctx.pod_client).await;
     assert_eq!(res.status(), StatusCode::OK);
 
     // Cannot use the POD with invalid password
@@ -104,14 +80,7 @@ async fn test_account_create(ctx: &mut TestData) {
     // db disappeared, user tries incorrectly to register again
     ctx.remove_db().await;
 
-    let res = ctx
-        .pod_client
-        .client
-        .post(format!("{}/v4/account", valid_credentials.pod_url))
-        .json(&register_request)
-        .send()
-        .await
-        .unwrap();
+    let res = ctx.pod_client.post_to(&register_request, "account").await;
 
     assert_eq!(res.status(), StatusCode::INTERNAL_SERVER_ERROR);
 
@@ -119,47 +88,38 @@ async fn test_account_create(ctx: &mut TestData) {
         res.json::<String>().await.unwrap(),
         format!(
             "Failure: Error 500 Internal Server Error, DB file does not exist but connection pool does for {}",
-            valid_credentials.owner_key
+            ctx.owner_key
         )
     );
 
     // State #4: db disappeared, user tries to use the pod
-    let res = use_pod(&valid_credentials).await;
+    let res = use_pod(&ctx.pod_client).await;
     assert_eq!(res.status(), StatusCode::INTERNAL_SERVER_ERROR);
 
     assert_eq!(
         res.json::<String>().await.unwrap(),
         format!(
             "Failure: Error 500 Internal Server Error, No DB, but connection pool exist for {}",
-            valid_credentials.owner_key
+            ctx.owner_key
         )
     );
 }
 
 #[test_context(TestData)]
 #[tokio::test]
-async fn test_account_works_after_pod_restart(ctx: &mut TestData) {
-    let valid_credentials = PodClient::new(&ctx.database_key, &ctx.owner_key, &ctx.pod_url);
-
+async fn test_pod_keys_works_after_restart(ctx: &mut TestData) {
     // # 2 register from the list - the happy path
     let register_request = json!( {
-        "ownerKey":  valid_credentials.owner_key,
-        "databaseKey": valid_credentials.database_key
+        "ownerKey":  ctx.owner_key,
+        "databaseKey": ctx.database_key
 
     });
 
-    let res = ctx
-        .pod_client
-        .client
-        .post(format!("{}/v4/account", valid_credentials.pod_url))
-        .json(&register_request)
-        .send()
-        .await
-        .unwrap();
+    let res = ctx.pod_client.post_to(&register_request, "account").await;
 
     assert_eq!(res.status(), StatusCode::OK);
 
-    let res = use_pod(&valid_credentials).await;
+    let res = use_pod(&ctx.pod_client).await;
     assert_eq!(res.status(), StatusCode::OK);
 
     // Restart the POD
@@ -177,28 +137,21 @@ async fn test_account_works_after_pod_restart(ctx: &mut TestData) {
     ctx.pod_logs = pod_logs;
 
     // Can use the POD, state #6
-    let res = use_pod(&valid_credentials).await;
+    let res = use_pod(&ctx.pod_client).await;
     assert_eq!(res.status(), StatusCode::OK);
 
-    // Cannot register again, state #5
+    // Cannot create again, state #5
     let res = ctx
         .pod_client
         .client
-        .post(format!("{}/v4/account", valid_credentials.pod_url))
+        .post(format!("{}/v4/account", ctx.pod_url))
         .json(&register_request)
         .send()
         .await
         .unwrap();
 
-    assert_eq!(res.status(), StatusCode::BAD_REQUEST);
-
-    assert_eq!(
-        res.json::<String>().await.unwrap(),
-        format!(
-            "Failure: Error 400 Bad Request, Account for {} already exist",
-            valid_credentials.owner_key
-        )
-    );
+    // Ignore
+    assert_eq!(res.status(), StatusCode::OK);
 }
 
 #[test_context(TestData)]
diff --git a/pod/tests/test_shared_plugin.rs b/pod/tests/test_shared_plugin.rs
index 6cd9bcca8703accea8ec409b25b32d6e9c74292c..0c750a4a28d67660f1214d106889cb167fa484c1 100644
--- a/pod/tests/test_shared_plugin.rs
+++ b/pod/tests/test_shared_plugin.rs
@@ -52,7 +52,11 @@ async fn test_shared_plugin(ctx: &mut TestDataSharedPlugin) {
         }
     );
 
-    let resp = ctx.base.pod_client.post_to(payload, "bulk").await;
+    let resp = ctx
+        .base
+        .pod_client
+        .post_to_with_owner(payload, "bulk")
+        .await;
 
     assert_eq!(resp.status(), reqwest::StatusCode::OK);
 
@@ -64,7 +68,11 @@ async fn test_shared_plugin(ctx: &mut TestDataSharedPlugin) {
                     "plugins" : []
                 }
             );
-            let response = ctx.base.pod_client.post_to(payload, "plugin/status").await;
+            let response = ctx
+                .base
+                .pod_client
+                .post_to_with_owner(payload, "plugin/status")
+                .await;
             assert!(response.status().is_success());
 
             let response_plugins = response.json::<PluginStatusRes>().await.unwrap();
@@ -95,7 +103,11 @@ async fn test_shared_plugin(ctx: &mut TestDataSharedPlugin) {
     // It is possible to get API of shared plugin
     let payload = json!({ "id": SHARED_PLUGIN1 });
 
-    let response = ctx.base.pod_client.post_to(payload, "plugin/api").await;
+    let response = ctx
+        .base
+        .pod_client
+        .post_to_with_owner(payload, "plugin/api")
+        .await;
 
     assert!(response.status().is_success());
 
@@ -127,7 +139,7 @@ async fn test_shared_plugin(ctx: &mut TestDataSharedPlugin) {
     let response = ctx
         .base
         .pod_client
-        .post_to(payload, "plugin/api/call")
+        .post_to_with_owner(payload, "plugin/api/call")
         .await;
 
     assert!(response.status().is_success());