diff --git a/.gitignore b/.gitignore
index c5078494..3acd8afb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
 target
 .idea
+files
diff --git a/.idea/stacker.iml b/.idea/stacker.iml
index 227e58a7..a97e9259 100644
--- a/.idea/stacker.iml
+++ b/.idea/stacker.iml
@@ -4,6 +4,8 @@
     <content url="file://$MODULE_DIR$">
       <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
       <sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" />
+      <excludeFolder url="file://$MODULE_DIR$/.idea/dataSources" />
+      <excludeFolder url="file://$MODULE_DIR$/files" />
       <excludeFolder url="file://$MODULE_DIR$/target" />
     </content>
     <orderEntry type="inheritedJdk" />
diff --git a/Cargo.lock b/Cargo.lock
index a4b8f33d..2ac84498 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -279,6 +279,54 @@ version = "0.2.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5"
 
+[[package]]
+name = "amq-protocol"
+version = "7.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d40d8b2465c7959dd40cee32ba6ac334b5de57e9fca0cc756759894a4152a5d"
+dependencies = [
+ "amq-protocol-tcp",
+ "amq-protocol-types",
+ "amq-protocol-uri",
+ "cookie-factory",
+ "nom",
+ "serde",
+]
+
+[[package]]
+name = "amq-protocol-tcp"
+version = "7.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9cb2100adae7da61953a2c3a01935d86caae13329fadce3333f524d6d6ce12e2"
+dependencies = [
+ "amq-protocol-uri",
+ "tcp-stream",
+ "tracing",
+]
+
+[[package]]
+name = "amq-protocol-types"
+version = "7.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "156ff13c8a3ced600b4e54ed826a2ae6242b6069d00dd98466827cef07d3daff"
+dependencies = [
+ "cookie-factory",
+ "nom",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "amq-protocol-uri"
+version = "7.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "751bbd7d440576066233e740576f1b31fdc6ab86cfabfbd48c548de77eca73e4"
+dependencies = [
+ "amq-protocol-types",
+ "percent-encoding",
+ "url",
+]
+
 [[package]]
 name = "android-tzdata"
 version = "0.1.1"
@@ -294,6 +342,104 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "async-channel"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35"
+dependencies = [
+ "concurrent-queue",
+ "event-listener",
+ "futures-core",
+]
+
+[[package]]
+name = "async-executor"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b0c4a4f319e45986f347ee47fef8bf5e81c9abc3f6f58dc2391439f30df65f0"
+dependencies = [
+ "async-lock",
+ "async-task",
+ "concurrent-queue",
+ "fastrand 2.0.1",
+ "futures-lite",
+ "slab",
+]
+
+[[package]]
+name = "async-global-executor"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1b6f5d7df27bd294849f8eec66ecfc63d11814df7a4f5d74168a2394467b776"
+dependencies = [
+ "async-channel",
+ "async-executor",
+ "async-io",
+ "async-lock",
+ "blocking",
+ "futures-lite",
+ "once_cell",
+]
+
+[[package]]
+name = "async-global-executor-trait"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33dd14c5a15affd2abcff50d84efd4009ada28a860f01c14f9d654f3e81b3f75"
+dependencies = [
+ "async-global-executor",
+ "async-trait",
+ "executor-trait",
+]
+
+[[package]]
+name = "async-io"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af"
+dependencies = [
+ "async-lock",
+ "autocfg",
+ "cfg-if",
+ "concurrent-queue",
+ "futures-lite",
+ "log",
+ "parking",
+ "polling",
+ "rustix 0.37.27",
+ "slab",
+ "socket2 0.4.9",
+ "waker-fn",
+]
+
+[[package]]
+name = "async-lock"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b"
+dependencies = [
+ "event-listener",
+]
+
+[[package]]
+name = "async-reactor-trait"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a6012d170ad00de56c9ee354aef2e358359deb1ec504254e0e5a3774771de0e"
+dependencies = [
+ "async-io",
+ "async-trait",
+ "futures-core",
+ "reactor-trait",
+]
+
+[[package]]
+name = "async-task"
+version = "4.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4eb2cdb97421e01129ccb49169d8279ed21e829929144f4a22a6e54ac549ca1"
+
 [[package]]
 name = "async-trait"
 version = "0.1.74"
@@ -314,6 +460,12 @@ dependencies = [
  "num-traits",
 ]
 
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
 [[package]]
 name = "autocfg"
 version = "1.1.0"
@@ -368,6 +520,31 @@ dependencies = [
  "generic-array",
 ]
 
+[[package]]
+name = "block-padding"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "blocking"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c36a4d0d48574b3dd360b4b7d95cc651d2b6557b6402848a27d4b228a473e2a"
+dependencies = [
+ "async-channel",
+ "async-lock",
+ "async-task",
+ "fastrand 2.0.1",
+ "futures-io",
+ "futures-lite",
+ "piper",
+ "tracing",
+]
+
 [[package]]
 name = "brotli"
 version = "3.4.0"
@@ -416,6 +593,15 @@ dependencies = [
  "bytes",
 ]
 
+[[package]]
+name = "cbc"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6"
+dependencies = [
+ "cipher",
+]
+
 [[package]]
 name = "cc"
 version = "1.0.83"
@@ -448,6 +634,25 @@ dependencies = [
  "windows-targets",
 ]
 
+[[package]]
+name = "cipher"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
+dependencies = [
+ "crypto-common",
+ "inout",
+]
+
+[[package]]
+name = "concurrent-queue"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f057a694a54f12365049b0958a1685bb52d567f5593b355fbf685838e873d400"
+dependencies = [
+ "crossbeam-utils",
+]
+
 [[package]]
 name = "config"
 version = "0.13.3"
@@ -484,6 +689,12 @@ dependencies = [
  "version_check",
 ]
 
+[[package]]
+name = "cookie-factory"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "396de984970346b0d9e93d1415082923c679e5ae5c3ee3dcbd104f5610af126b"
+
 [[package]]
 name = "core-foundation"
 version = "0.9.3"
@@ -562,6 +773,41 @@ dependencies = [
  "typenum",
 ]
 
+[[package]]
+name = "darling"
+version = "0.14.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850"
+dependencies = [
+ "darling_core",
+ "darling_macro",
+]
+
+[[package]]
+name = "darling_core"
+version = "0.14.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0"
+dependencies = [
+ "fnv",
+ "ident_case",
+ "proc-macro2",
+ "quote",
+ "strsim",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "darling_macro"
+version = "0.14.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e"
+dependencies = [
+ "darling_core",
+ "quote",
+ "syn 1.0.109",
+]
+
 [[package]]
 name = "deranged"
 version = "0.3.9"
@@ -571,6 +817,37 @@ dependencies = [
  "powerfmt",
 ]
 
+[[package]]
+name = "derive_builder"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d67778784b508018359cbc8696edb3db78160bab2c2a28ba7f56ef6932997f8"
+dependencies = [
+ "derive_builder_macro",
+]
+
+[[package]]
+name = "derive_builder_core"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c11bdc11a0c47bc7d37d582b5285da6849c96681023680b906673c5707af7b0f"
+dependencies = [
+ "darling",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "derive_builder_macro"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebcda35c7a396850a55ffeac740804b40ffec779b98fffbb1738f4033f0ee79e"
+dependencies = [
+ "derive_builder_core",
+ "syn 1.0.109",
+]
+
 [[package]]
 name = "derive_more"
 version = "0.99.17"
@@ -584,6 +861,15 @@ dependencies = [
  "syn 1.0.109",
 ]
 
+[[package]]
+name = "des"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ffdd80ce8ce993de27e9f063a444a4d53ce8e8db4c1f00cc03af5ad5a9867a1e"
+dependencies = [
+ "cipher",
+]
+
 [[package]]
 name = "digest"
 version = "0.10.7"
@@ -621,6 +907,12 @@ version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257"
 
+[[package]]
+name = "doc-comment"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
+
 [[package]]
 name = "dotenvy"
 version = "0.15.7"
@@ -645,6 +937,12 @@ dependencies = [
  "cfg-if",
 ]
 
+[[package]]
+name = "equivalent"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
+
 [[package]]
 name = "errno"
 version = "0.3.5"
@@ -661,6 +959,24 @@ version = "2.5.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
 
+[[package]]
+name = "executor-trait"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a1052dd43212a7777ec6a69b117da52f5e52f07aec47d00c1a2b33b85d06b08"
+dependencies = [
+ "async-trait",
+]
+
+[[package]]
+name = "fastrand"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be"
+dependencies = [
+ "instant",
+]
+
 [[package]]
 name = "fastrand"
 version = "2.0.1"
@@ -683,6 +999,18 @@ dependencies = [
  "miniz_oxide",
 ]
 
+[[package]]
+name = "flume"
+version = "0.10.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+ "pin-project",
+ "spin 0.9.8",
+]
+
 [[package]]
 name = "fnv"
 version = "1.0.7"
@@ -772,6 +1100,21 @@ version = "0.3.29"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa"
 
+[[package]]
+name = "futures-lite"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce"
+dependencies = [
+ "fastrand 1.9.0",
+ "futures-core",
+ "futures-io",
+ "memchr",
+ "parking",
+ "pin-project-lite",
+ "waker-fn",
+]
+
 [[package]]
 name = "futures-macro"
 version = "0.3.29"
@@ -850,6 +1193,12 @@ version = "0.28.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0"
 
+[[package]]
+name = "glob"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
+
 [[package]]
 name = "h2"
 version = "0.3.21"
@@ -862,7 +1211,7 @@ dependencies = [
  "futures-sink",
  "futures-util",
  "http",
- "indexmap",
+ "indexmap 1.9.3",
  "slab",
  "tokio",
  "tokio-util",
@@ -1030,6 +1379,12 @@ dependencies = [
  "cc",
 ]
 
+[[package]]
+name = "ident_case"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
+
 [[package]]
 name = "idna"
 version = "0.4.0"
@@ -1051,6 +1406,27 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "indexmap"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.14.1",
+ "serde",
+]
+
+[[package]]
+name = "inout"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5"
+dependencies = [
+ "block-padding",
+ "generic-array",
+]
+
 [[package]]
 name = "instant"
 version = "0.1.12"
@@ -1060,6 +1436,17 @@ dependencies = [
  "cfg-if",
 ]
 
+[[package]]
+name = "io-lifetimes"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "windows-sys",
+]
+
 [[package]]
 name = "ipnet"
 version = "2.8.0"
@@ -1125,6 +1512,29 @@ version = "0.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388"
 
+[[package]]
+name = "lapin"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f3067a1fcfbc3fc46455809c023e69b8f6602463201010f4ae5a3b572adb9dc"
+dependencies = [
+ "amq-protocol",
+ "async-global-executor-trait",
+ "async-reactor-trait",
+ "async-trait",
+ "executor-trait",
+ "flume",
+ "futures-core",
+ "futures-io",
+ "parking_lot 0.12.1",
+ "pinky-swear",
+ "reactor-trait",
+ "serde",
+ "serde_json",
+ "tracing",
+ "waker-fn",
+]
+
 [[package]]
 name = "lazy_static"
 version = "1.4.0"
@@ -1143,6 +1553,12 @@ version = "0.5.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
 
+[[package]]
+name = "linux-raw-sys"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519"
+
 [[package]]
 name = "linux-raw-sys"
 version = "0.4.10"
@@ -1372,6 +1788,29 @@ version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
 
+[[package]]
+name = "p12"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4873306de53fe82e7e484df31e1e947d61514b6ea2ed6cd7b45d63006fd9224"
+dependencies = [
+ "cbc",
+ "cipher",
+ "des",
+ "getrandom",
+ "hmac",
+ "lazy_static",
+ "rc2",
+ "sha1",
+ "yasna",
+]
+
+[[package]]
+name = "parking"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae"
+
 [[package]]
 name = "parking_lot"
 version = "0.11.2"
@@ -1515,12 +1954,51 @@ version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
 
+[[package]]
+name = "pinky-swear"
+version = "6.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d894b67aa7a4bf295db5e85349078c604edaa6fa5c8721e8eca3c7729a27f2ac"
+dependencies = [
+ "doc-comment",
+ "flume",
+ "parking_lot 0.12.1",
+ "tracing",
+]
+
+[[package]]
+name = "piper"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4"
+dependencies = [
+ "atomic-waker",
+ "fastrand 2.0.1",
+ "futures-io",
+]
+
 [[package]]
 name = "pkg-config"
 version = "0.3.27"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964"
 
+[[package]]
+name = "polling"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce"
+dependencies = [
+ "autocfg",
+ "bitflags 1.3.2",
+ "cfg-if",
+ "concurrent-queue",
+ "libc",
+ "log",
+ "pin-project-lite",
+ "windows-sys",
+]
+
 [[package]]
 name = "powerfmt"
 version = "0.2.0"
@@ -1605,6 +2083,26 @@ dependencies = [
  "getrandom",
 ]
 
+[[package]]
+name = "rc2"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62c64daa8e9438b84aaae55010a93f396f8e60e3911590fcba770d04643fc1dd"
+dependencies = [
+ "cipher",
+]
+
+[[package]]
+name = "reactor-trait"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "438a4293e4d097556730f4711998189416232f009c137389e0f961d2bc0ddc58"
+dependencies = [
+ "async-trait",
+ "futures-core",
+ "futures-io",
+]
+
 [[package]]
 name = "redox_syscall"
 version = "0.2.16"
@@ -1790,6 +2288,20 @@ dependencies = [
  "semver",
 ]
 
+[[package]]
+name = "rustix"
+version = "0.37.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2"
+dependencies = [
+ "bitflags 1.3.2",
+ "errno",
+ "io-lifetimes",
+ "libc",
+ "linux-raw-sys 0.3.8",
+ "windows-sys",
+]
+
 [[package]]
 name = "rustix"
 version = "0.38.19"
@@ -1799,7 +2311,7 @@ dependencies = [
  "bitflags 2.4.1",
  "errno",
  "libc",
- "linux-raw-sys",
+ "linux-raw-sys 0.4.10",
  "windows-sys",
 ]
 
@@ -1815,6 +2327,42 @@ dependencies = [
  "webpki",
 ]
 
+[[package]]
+name = "rustls"
+version = "0.21.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "446e14c5cda4f3f30fe71863c34ec70f5ac79d6087097ad0bb433e1be5edf04c"
+dependencies = [
+ "log",
+ "ring 0.17.4",
+ "rustls-webpki",
+ "sct",
+]
+
+[[package]]
+name = "rustls-connector"
+version = "0.18.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "060bcc1795b840d0e56d78f3293be5f652aa1611d249b0e63ffe19f4a8c9ae23"
+dependencies = [
+ "log",
+ "rustls 0.21.8",
+ "rustls-native-certs",
+ "rustls-webpki",
+]
+
+[[package]]
+name = "rustls-native-certs"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00"
+dependencies = [
+ "openssl-probe",
+ "rustls-pemfile",
+ "schannel",
+ "security-framework",
+]
+
 [[package]]
 name = "rustls-pemfile"
 version = "1.0.3"
@@ -1824,6 +2372,16 @@ dependencies = [
  "base64 0.21.4",
 ]
 
+[[package]]
+name = "rustls-webpki"
+version = "0.101.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765"
+dependencies = [
+ "ring 0.17.4",
+ "untrusted 0.9.0",
+]
+
 [[package]]
 name = "ryu"
 version = "1.0.15"
@@ -1933,7 +2491,7 @@ version = "0.16.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0adc7a19d45e581abc6d169c865a0b14b84bb43a9e966d1cca4d733e70f7f35a"
 dependencies = [
- "indexmap",
+ "indexmap 1.9.3",
  "itertools 0.10.5",
  "num-traits",
  "once_cell",
@@ -1971,6 +2529,19 @@ dependencies = [
  "regex",
 ]
 
+[[package]]
+name = "serde_yaml"
+version = "0.9.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a49e178e4452f45cb61d0cd8cebc1b0fafd3e41929e996cef79aa3aca91f574"
+dependencies = [
+ "indexmap 2.1.0",
+ "itoa",
+ "ryu",
+ "serde",
+ "unsafe-libyaml",
+]
+
 [[package]]
 name = "sha1"
 version = "0.10.6"
@@ -2057,6 +2628,9 @@ name = "spin"
 version = "0.9.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
+dependencies = [
+ "lock_api",
+]
 
 [[package]]
 name = "sqlformat"
@@ -2106,7 +2680,7 @@ dependencies = [
  "hex",
  "hkdf",
  "hmac",
- "indexmap",
+ "indexmap 1.9.3",
  "itoa",
  "libc",
  "log",
@@ -2116,7 +2690,7 @@ dependencies = [
  "paste",
  "percent-encoding",
  "rand",
- "rustls",
+ "rustls 0.20.9",
  "rustls-pemfile",
  "serde",
  "serde_json",
@@ -2177,9 +2751,14 @@ dependencies = [
  "actix-web-httpauth",
  "chrono",
  "config",
+ "derive_builder",
  "futures",
+ "futures-lite",
  "futures-util",
+ "glob",
  "hmac",
+ "indexmap 2.1.0",
+ "lapin",
  "rand",
  "regex",
  "reqwest",
@@ -2187,6 +2766,7 @@ dependencies = [
  "serde_derive",
  "serde_json",
  "serde_valid",
+ "serde_yaml",
  "sha2",
  "sqlx",
  "thiserror",
@@ -2266,6 +2846,18 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "tcp-stream"
+version = "0.26.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4da30af7998f51ee1aa48ab24276fe303a697b004e31ff542b192c088d5630a5"
+dependencies = [
+ "cfg-if",
+ "p12",
+ "rustls-connector",
+ "rustls-pemfile",
+]
+
 [[package]]
 name = "tempfile"
 version = "3.8.0"
@@ -2273,9 +2865,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef"
 dependencies = [
  "cfg-if",
- "fastrand",
+ "fastrand 2.0.1",
  "redox_syscall 0.3.5",
- "rustix",
+ "rustix 0.38.19",
  "windows-sys",
 ]
 
@@ -2410,7 +3002,7 @@ version = "0.23.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59"
 dependencies = [
- "rustls",
+ "rustls 0.20.9",
  "tokio",
  "webpki",
 ]
@@ -2598,6 +3190,12 @@ version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
 
+[[package]]
+name = "unsafe-libyaml"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f28467d3e1d3c6586d8f25fa243f544f5800fec42d97032474e17222c2b75cfa"
+
 [[package]]
 name = "untrusted"
 version = "0.7.1"
@@ -2649,6 +3247,12 @@ version = "0.9.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
 
+[[package]]
+name = "waker-fn"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3c4517f54858c779bbcbf228f4fca63d121bf85fbecb2dc578cdf4a39395690"
+
 [[package]]
 name = "want"
 version = "0.3.1"
@@ -2891,6 +3495,12 @@ dependencies = [
  "linked-hash-map",
 ]
 
+[[package]]
+name = "yasna"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd"
+
 [[package]]
 name = "zstd"
 version = "0.12.4"
diff --git a/Cargo.toml b/Cargo.toml
index 91530318..03017486 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -40,6 +40,13 @@ actix-http = "3.4.0"
 hmac = "0.12.1"
 sha2 = "0.10.8"
 
+# dctypes
+derive_builder = "0.12.0"
+indexmap = { version = "2.0.0", features = ["serde"], optional = true }
+serde_yaml = "0.9"
+lapin = { version = "2.3.1", features = ["serde_json"] }
+futures-lite = "1.13.0"
+
 [dependencies.sqlx]
 version = "0.6.3"
 features = [
@@ -51,3 +58,10 @@ features = [
     "json",
     "offline"
 ]
+
+[features]
+default = ["indexmap"]
+indexmap = ["dep:indexmap"]
+
+[dev-dependencies]
+glob = "0.3"
diff --git a/configuration.yaml b/configuration.yaml
index 5099d3d3..030dd49d 100644
--- a/configuration.yaml
+++ b/configuration.yaml
@@ -9,3 +9,9 @@ database:
   username: postgres
   password: postgres
   database_name: stacker
+
+amqp:
+  host: 127.0.0.1
+  port: 5672
+  username: guest
+  password: guest
diff --git a/docker-compose.yml b/docker-compose.yml
index 2d3b934d..26311c4c 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -13,7 +13,7 @@ networks:
 services:
 
   stacker:
-    image: trydirect/stacker:0.0.3
+    image: trydirect/stacker:0.0.4
     build: .
     container_name: stacker
     restart: always
diff --git a/docker/dev/docker-compose.yml b/docker/dev/docker-compose.yml
index 1ba68f2c..bdfdd892 100644
--- a/docker/dev/docker-compose.yml
+++ b/docker/dev/docker-compose.yml
@@ -7,7 +7,7 @@ volumes:
 services:
 
   stacker:
-    image: trydirect/stacker:0.0.3
+    image: trydirect/stacker:0.0.4
     build: .
     container_name: stacker
     restart: always
diff --git a/migrations/20230905145525_creating_stack_tables.up.sql b/migrations/20230905145525_creating_stack_tables.up.sql
index e908e970..b20b2cdb 100644
--- a/migrations/20230905145525_creating_stack_tables.up.sql
+++ b/migrations/20230905145525_creating_stack_tables.up.sql
@@ -1,12 +1,13 @@
--- Add up migration script here
--- Add migration script here
 CREATE TABLE user_stack (
-    id serial,
+    id serial4 NOT NULL,
     stack_id uuid NOT NULL,
     user_id VARCHAR(50) NOT NULL,
     name TEXT NOT NULL UNIQUE,
     body JSON NOT NULL,
     created_at timestamptz NOT NULL,
-    updated_at timestamptz NOT NULL
-)
+    updated_at timestamptz NOT NULL,
+    CONSTRAINT user_stack_pkey PRIMARY KEY (id)
+);
 
+CREATE INDEX idx_stack_id ON user_stack(stack_id);
+CREATE INDEX idx_stack_user_id ON user_stack(user_id);
\ No newline at end of file
diff --git a/src/configuration.rs b/src/configuration.rs
index a3beeaf4..90d22c92 100644
--- a/src/configuration.rs
+++ b/src/configuration.rs
@@ -7,6 +7,7 @@ pub struct Settings {
     pub app_host: String,
     pub auth_url: String,
     pub max_clients_number: i64,
+    pub amqp: AmqpSettings
 }
 
 #[derive(Debug, serde::Deserialize)]
@@ -18,6 +19,13 @@ pub struct DatabaseSettings {
     pub database_name: String,
 }
 
+#[derive(Debug, serde::Deserialize)]
+pub struct AmqpSettings {
+    pub username: String,
+    pub password: String,
+    pub host: String,
+    pub port: u16,
+}
 impl DatabaseSettings {
     // Connection string: postgresql://<username>:<password>@<host>:<port>/<database_name>
     pub fn connection_string(&self) -> String {
@@ -35,6 +43,15 @@ impl DatabaseSettings {
     }
 }
 
+impl AmqpSettings {
+    pub fn connection_string(&self) -> String {
+        format!(
+            "amqp://{}:{}@{}:{}/%2f",
+            self.username, self.password, self.host, self.port,
+        )
+    }
+}
+
 pub fn get_configuration() -> Result<Settings, config::ConfigError> {
     // Initialize our configuration reader
     let mut settings = config::Config::default();
diff --git a/src/forms/stack.rs b/src/forms/stack.rs
index 93a1be85..78a0847b 100644
--- a/src/forms/stack.rs
+++ b/src/forms/stack.rs
@@ -1,10 +1,99 @@
 use serde::{Deserialize, Serialize};
 use serde_json::Value;
 use serde_valid::Validate;
+use std::fmt;
+
+
+#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)]
+pub struct Role {
+    pub role: Option<Vec<String>>,
+}
+
+#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)]
+pub struct Requirements {
+    #[validate(minimum=0.1)]
+    pub cpu: Option<f64>,
+    #[validate(min_length=1)]
+    #[validate(max_length=10)]
+    #[validate(pattern = r"^\d+G$")]
+    #[serde(rename = "disk_size")]
+    pub disk_size: Option<String>,
+    #[serde(rename = "ram_size")]
+    #[validate(min_length=1)]
+    #[validate(max_length=10)]
+    #[validate(pattern = r"^\d+G$")]
+    pub ram_size: Option<String>,
+}
+
+#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)]
+pub struct Ports {
+    #[serde(rename(deserialize = "sharedPorts"))]
+    #[serde(rename(serialize = "shared_ports"))]
+    #[serde(alias = "shared_ports")]
+    pub shared_ports: Option<Vec<String>>,
+    pub ports: Option<Vec<String>>,
+}
+
+#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)]
+pub struct DockerImage {
+    pub dockerhub_user: Option<String>,
+    pub dockerhub_name: Option<String>,
+    pub dockerhub_image: Option<String>,
+}
+
+impl fmt::Display for DockerImage
+{
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        let tag = "latest";
+
+        let dim = self.dockerhub_image.clone()
+            .unwrap_or("".to_string());
+        write!(f, "{}/{}:{}", self.dockerhub_user.clone()
+            .unwrap_or("trydirect".to_string()).clone(),
+                self.dockerhub_name.clone().unwrap_or(dim), tag
+        )
+    }
+}
+
+impl AsRef<DockerImage> for App {
+    fn as_ref(&self) -> &DockerImage {
+        &self.docker_image
+    }
+}
+
 
 #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)]
-#[serde(rename_all = "camelCase")]
 pub struct StackForm {
+    #[serde(rename= "commonDomain")]
+    pub common_domain: Option<String>,
+    pub domain_list: Option<DomainList>,
+    pub stack_code: Option<String>,
+    pub region: String,
+    pub zone: Option<String>,
+    pub server: String,
+    pub os: String,
+    pub ssl: String,
+    pub vars: Option<Vec<Var>>,
+    pub integrated_features: Option<Vec<Value>>,
+    pub extended_features: Option<Vec<Value>>,
+    pub subscriptions: Option<Vec<String>>,
+    pub form_app: Option<Vec<String>>,
+    pub disk_type: Option<String>,
+    pub save_token: bool,
+    pub cloud_token: String,
+    pub provider: String,
+    pub selected_plan: String,
+    pub custom: Custom,
+}
+
+
+#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)]
+#[serde(rename_all = "snake_case")]
+pub struct StackPayload {
+    pub(crate) id: Option<i32>,
+    pub(crate) user_token: Option<String>,
+    pub(crate) user_email: Option<String>,
+    #[serde(rename= "commonDomain")]
     pub common_domain: String,
     pub domain_list: Option<DomainList>,
     pub region: String,
@@ -18,12 +107,13 @@ pub struct StackForm {
     #[serde(rename = "extended_features")]
     pub extended_features: Option<Vec<Value>>,
     pub subscriptions: Option<Vec<String>>,
+    pub form_app: Option<Vec<String>>,
+    pub disk_type: Option<String>,
     #[serde(rename = "save_token")]
     pub save_token: bool,
     #[serde(rename = "cloud_token")]
     pub cloud_token: String,
     pub provider: String,
-    #[serde(rename = "stack_code")]
     pub stack_code: String,
     #[serde(rename = "selected_plan")]
     pub selected_plan: String,
@@ -52,13 +142,11 @@ pub struct Custom {
     pub feature: Option<Vec<Feature>>,
     pub service: Option<Vec<Service>>,
     #[serde(rename = "servers_count")]
-    pub servers_count: i64,
-    #[serde(rename = "custom_stack_name")]
-    pub custom_stack_name: String,
+    pub servers_count: u32,
     #[serde(rename = "custom_stack_code")]
     pub custom_stack_code: String,
-    #[serde(rename = "custom_stack_git_url")]
-    pub custom_stack_git_url: Option<String>,
+    #[serde(rename = "project_git_url")]
+    pub project_git_url: Option<String>,
     #[serde(rename = "custom_stack_category")]
     pub custom_stack_category: Option<Vec<String>>,
     #[serde(rename = "custom_stack_short_description")]
@@ -73,44 +161,8 @@ pub struct Custom {
     pub project_description: Option<String>,
 }
 
-
-#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)]
-#[serde(rename_all = "camelCase")]
-pub struct Web {
-    pub name: String,
-    pub code: String,
-    pub domain: Option<String>,
-    pub shared_ports: Option<Vec<String>>,
-    pub versions: Option<Vec<Version>>,
-    pub custom: bool,
-    #[serde(rename = "type")]
-    pub type_field: String,
-    pub main: bool,
-    #[serde(rename = "_id")]
-    pub id: String,
-    #[serde(rename = "dockerhub_user")]
-    pub dockerhub_user: String,
-    #[serde(rename = "dockerhub_name")]
-    pub dockerhub_name: String,
-    pub url_app: Option<String>,
-    pub url_git: Option<String>,
-    #[validate(min_length=1)]
-    #[validate(max_length=10)]
-    #[validate(pattern = r"^\d+G$")]
-    #[serde(rename = "disk_size")]
-    pub disk_size: String,
-    #[serde(rename = "ram_size")]
-    #[validate(min_length=1)]
-    #[validate(max_length=10)]
-    #[validate(pattern = r"^\d+G$")]
-    pub ram_size: String,
-    #[validate(minimum=0.1)]
-    pub cpu: f64,
-}
-
 #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)]
-#[serde(rename_all = "camelCase")]
-pub struct Feature {
+pub struct App {
     #[serde(rename = "_etag")]
     pub etag: Option<String>,
     #[serde(rename = "_id")]
@@ -121,57 +173,60 @@ pub struct Feature {
     pub updated: Option<String>,
     pub name: String,
     pub code: String,
-    pub role: Vec<String>,
     #[serde(rename = "type")]
     pub type_field: String,
+    #[serde(flatten)]
+    pub role: Role,
     pub default: Option<bool>,
-    pub popularity: Option<u32>,
-    pub descr: Option<String>,
+    #[serde(flatten)]
     pub ports: Option<Ports>,
+    pub versions: Option<Vec<Version>>,
+    #[serde(flatten)]
+    pub docker_image: DockerImage,
+    #[serde(flatten)]
+    pub requirements: Requirements,
+    pub popularity: Option<u32>,
     pub commercial: Option<bool>,
     pub subscription: Option<Value>,
     pub autodeploy: Option<bool>,
     pub suggested: Option<bool>,
     pub dependency: Option<Value>,
-    #[serde(rename = "avoid_render")]
     pub avoid_render: Option<bool>,
     pub price: Option<Price>,
     pub icon: Option<Icon>,
-    #[serde(rename = "category_id")]
+    pub domain: Option<String>,
+    pub main: bool,
     pub category_id: Option<u32>,
-    #[serde(rename = "parent_app_id")]
     pub parent_app_id: Option<u32>,
-    #[serde(rename = "full_description")]
+    pub descr: Option<String>,
     pub full_description: Option<String>,
     pub description: Option<String>,
-    #[serde(rename = "plan_type")]
     pub plan_type: Option<String>,
-    #[serde(rename = "ansible_var")]
     pub ansible_var: Option<String>,
-    #[serde(rename = "repo_dir")]
     pub repo_dir: Option<String>,
-    #[validate(min_length=1)]
-    pub cpu: String,
-    #[validate(min_length=1)]
-    #[serde(rename = "ram_size")]
-    pub ram_size: String,
-    #[validate(min_length=1)]
-    #[serde(rename = "disk_size")]
-    pub disk_size: String,
-    #[serde(rename = "dockerhub_image")]
-    pub dockerhub_image: Option<String>,
-    pub versions: Option<Vec<Version>>,
-    pub domain: Option<String>,
-    pub shared_ports: Option<Vec<String>>,
-    pub main: bool,
+    pub url_app: Option<String>,
+    pub url_git: Option<String>,
 }
 
-#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct Ports {
-    pub public: Vec<String>,
+#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)]
+pub struct Web {
+    #[serde(flatten)]
+    pub app: App,
+    pub custom: Option<bool>,
 }
 
+#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)]
+pub struct Feature {
+    #[serde(flatten)]
+    pub app: App,
+}
+
+#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)]
+#[serde(rename_all = "camelCase")]
+pub struct Service {
+    #[serde(flatten)]
+    pub(crate) app: App,
+}
 
 #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
 #[serde(rename_all = "camelCase")]
@@ -199,13 +254,13 @@ pub struct Version {
     #[serde(rename = "_etag")]
     pub etag: Option<String>,
     #[serde(rename = "_id")]
-    pub id: i64,
+    pub id: u32,
     #[serde(rename = "_created")]
     pub created: Option<String>,
     #[serde(rename = "_updated")]
     pub updated: Option<String>,
     #[serde(rename = "app_id")]
-    pub app_id: i64,
+    pub app_id: u32,
     pub name: String,
     pub version: String,
     #[serde(rename = "update_status")]
@@ -213,61 +268,4 @@ pub struct Version {
     pub tag: String,
 }
 
-#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)]
-#[serde(rename_all = "camelCase")]
-pub struct Service {
-    #[serde(rename = "_etag")]
-    pub etag: Option<String>,
-    #[serde(rename = "_id")]
-    pub id: i64,
-    #[serde(rename = "_created")]
-    pub created: Option<String>,
-    #[serde(rename = "_updated")]
-    pub updated: Option<String>,
-    pub name: String,
-    pub code: String,
-    pub role: Option<Vec<Value>>,
-    #[serde(rename = "type")]
-    pub type_field: String,
-    pub default: Option<Value>,
-    pub popularity: Option<u32>,
-    pub descr: Option<String>,
-    pub ports: Option<Ports>,
-    pub commercial: Option<bool>,
-    pub subscription: Option<Value>,
-    pub autodeploy: Option<bool>,
-    pub suggested: Option<bool>,
-    pub dependency: Option<Value>,
-    #[serde(rename = "avoid_render")]
-    pub avoid_render: Option<bool>,
-    pub price: Option<Price>,
-    pub icon: Option<Icon>,
-    #[serde(rename = "category_id")]
-    pub category_id: Option<u32>,
-    #[serde(rename = "parent_app_id")]
-    pub parent_app_id: Option<u32>,
-    #[serde(rename = "full_description")]
-    pub full_description: Option<String>,
-    pub description: Option<String>,
-    #[serde(rename = "plan_type")]
-    pub plan_type: Option<String>,
-    #[serde(rename = "ansible_var")]
-    pub ansible_var: Option<String>,
-    #[serde(rename = "repo_dir")]
-    pub repo_dir: Option<String>,
-    #[validate(min_length=1)]
-    pub cpu: String,
-    #[serde(rename = "ram_size")]
-    #[validate(min_length=1)]
-    pub ram_size: String,
-    #[serde(rename = "disk_size")]
-    #[validate(min_length=1)]
-    pub disk_size: String,
-    #[serde(rename = "dockerhub_image")]
-    pub dockerhub_image: Option<String>,
-    pub versions: Option<Vec<Version>>,
-    pub domain: String,
-    pub shared_ports: Option<Vec<String>>,
-    pub main: bool,
-}
 
diff --git a/src/forms/user.rs b/src/forms/user.rs
index 2da0dcf8..8ff0b4ce 100644
--- a/src/forms/user.rs
+++ b/src/forms/user.rs
@@ -16,45 +16,45 @@ pub struct User {
     #[serde(rename = "_id")]
     pub id: String,
     #[serde(rename = "first_name")]
-    pub first_name: String,
+    pub first_name: Option<String>,
     #[serde(rename = "last_name")]
-    pub last_name: String,
-    pub created: String,
-    pub updated: String,
+    pub last_name: Option<String>,
+    pub created: Option<String>,
+    pub updated: Option<String>,
     pub email: String,
     #[serde(rename = "email_confirmed")]
     pub email_confirmed: bool,
     pub social: bool,
-    pub website: String,
+    pub website: Option<String>,
     pub currency: Value,
-    pub phone: String,
+    pub phone: Option<String>,
     #[serde(rename = "password_change_required")]
     pub password_change_required: Value,
-    pub photo: String,
-    pub country: String,
+    pub photo: Option<String>,
+    pub country: Option<String>,
     #[serde(rename = "billing_first_name")]
     pub billing_first_name: Value,
     #[serde(rename = "billing_last_name")]
     pub billing_last_name: Value,
     #[serde(rename = "billing_postcode")]
-    pub billing_postcode: String,
+    pub billing_postcode: Option<String>,
     #[serde(rename = "billing_address_1")]
-    pub billing_address_1: String,
+    pub billing_address_1: Option<String>,
     #[serde(rename = "billing_address_2")]
-    pub billing_address_2: String,
+    pub billing_address_2: Option<String>,
     #[serde(rename = "billing_city")]
-    pub billing_city: String,
+    pub billing_city: Option<String>,
     #[serde(rename = "billing_country_code")]
-    pub billing_country_code: String,
+    pub billing_country_code: Option<String>,
     #[serde(rename = "billing_country_area")]
-    pub billing_country_area: String,
-    pub tokens: Vec<Token>,
-    pub subscriptions: Vec<Subscription>,
-    pub plan: Plan,
+    pub billing_country_area: Option<String>,
+    pub tokens: Option<Vec<Token>>,
+    pub subscriptions: Option<Vec<Subscription>>,
+    pub plan: Option<Plan>,
     #[serde(rename = "deployments_left")]
     pub deployments_left: Value,
     #[serde(rename = "suspension_hints")]
-    pub suspension_hints: SuspensionHints,
+    pub suspension_hints: Option<SuspensionHints>,
 }
 
 #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
@@ -72,9 +72,9 @@ pub struct Subscription {
     #[serde(rename = "user_id")]
     pub user_id: i64,
     #[serde(rename = "date_created")]
-    pub date_created: String,
+    pub date_created: Option<String>,
     #[serde(rename = "date_updated")]
-    pub date_updated: String,
+    pub date_updated: Option<String>,
 }
 
 #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
@@ -92,21 +92,21 @@ pub struct Plan {
     pub billing_email: String,
     #[serde(rename = "date_of_purchase")]
     pub date_of_purchase: String,
-    pub currency: String,
-    pub price: String,
-    pub period: String,
+    pub currency: Option<String>,
+    pub price: Option<String>,
+    pub period: Option<String>,
     #[serde(rename = "date_start")]
     pub date_start: String,
     pub active: bool,
     #[serde(rename = "billing_id")]
-    pub billing_id: String,
+    pub billing_id: Option<String>,
 }
 
 #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
 #[serde(rename_all = "camelCase")]
 pub struct SupportedStacks {
-    pub monthly: i64,
-    pub annually: i64,
+    pub monthly: Option<i64>,
+    pub annually: Option<i64>,
 }
 
 #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
@@ -132,8 +132,8 @@ impl TryInto<UserModel> for UserForm {
         // )?;
         Ok(UserModel {
             id: self.user.id,
-            first_name: self.user.first_name,
-            last_name: self.user.last_name,
+            first_name: self.user.first_name.unwrap_or("Noname".to_string()),
+            last_name: self.user.last_name.unwrap_or("Noname".to_string()),
             email: self.user.email,
             email_confirmed: self.user.email_confirmed,
         })
diff --git a/src/helpers/json.rs b/src/helpers/json.rs
index 7c193d69..d38a49ae 100644
--- a/src/helpers/json.rs
+++ b/src/helpers/json.rs
@@ -1,15 +1,12 @@
 use actix_web::{Responder, Result};
-use actix_web::error::ErrorBadRequest;
+use actix_web::error::{ErrorBadRequest, ErrorConflict, ErrorNotFound, ErrorInternalServerError};
 use serde_derive::Serialize;
-use actix_web::web;
 use actix_web::web::Json;
 use actix_web::Error;
 
 #[derive(Serialize)]
 pub(crate) struct JsonResponse<T> {
-    // pub(crate) status: String,
     pub(crate) message: String,
-    // pub(crate) code: u32,
     pub(crate) id: Option<i32>,
     pub(crate) item: Option<T>,
     pub(crate) list: Option<Vec<T>>
@@ -72,6 +69,45 @@ impl<T> JsonResponseBuilder<T>
         Err(ErrorBadRequest(
             serde_json::to_string(&json_response).unwrap()))
     }
+
+    pub(crate) fn not_found(self, msg: String) -> Result<Json<JsonResponse<T>>, Error>  {
+
+        let json_response = JsonResponse {
+            message: msg,
+            id: self.id,
+            item: self.item,
+            list: self.list
+        };
+
+        Err(ErrorNotFound(
+            serde_json::to_string(&json_response).unwrap()))
+    }
+
+    pub(crate) fn internal_error(self, msg: String) -> Result<Json<JsonResponse<T>>, Error>  {
+
+        let json_response = JsonResponse {
+            message: msg,
+            id: self.id,
+            item: self.item,
+            list: self.list
+        };
+
+        Err(ErrorInternalServerError(
+            serde_json::to_string(&json_response).unwrap()))
+    }
+
+    pub(crate) fn conflict(self, msg: String) -> Result<Json<JsonResponse<T>>, Error>  {
+
+        let json_response = JsonResponse {
+            message: msg,
+            id: self.id,
+            item: self.item,
+            list: self.list
+        };
+
+        Err(ErrorConflict(
+            serde_json::to_string(&json_response).unwrap()))
+    }
 }
 
 impl<T> From<T> for JsonResponseBuilder<T>
@@ -95,9 +131,7 @@ where T: serde::Serialize + Default
     {
         JsonResponseBuilder::default()
     }
-    pub(crate) fn new(status: String,
-                      message: String,
-                      code: u32,
+    pub(crate) fn new(message: String,
                       id: Option<i32>,
                       item:Option<T>,
                       list: Option<Vec<T>>) -> Self {
@@ -110,61 +144,61 @@ where T: serde::Serialize + Default
         }
     }
 
-    pub(crate) fn ok(id: i32, message: &str) -> JsonResponse<T> {
-
-        let msg = if !message.trim().is_empty() {
-            message.to_string()
-        }
-        else{
-            String::from("Success")
-        };
-
-        JsonResponse {
-            message: msg,
-            id: Some(id),
-            item: None,
-            list: None,
-        }
-    }
-
-    pub(crate) fn not_found() -> Self {
-        JsonResponse {
-            id: None,
-            item: None,
-            message: format!("Object not found"),
-            list: None,
-        }
-    }
-
-    pub(crate) fn internal_error(message: &str) -> Self {
-
-        let msg = if !message.trim().is_empty() {
-            message.to_string()
-        }
-        else{
-            String::from("Internal error")
-        };
-        JsonResponse {
-            id: None,
-            item: None,
-            message: msg,
-            list: None,
-        }
-    }
-
-    pub(crate) fn not_valid(message: &str) -> Self {
-
-        let msg = if !message.trim().is_empty() {
-            message.to_string()
-        }
-        else{
-            String::from("Validation error")
-        };
-        JsonResponse {
-            id: None,
-            item: None,
-            message: msg,
-            list: None,
-        }
-    }
+    // pub(crate) fn ok(id: i32, message: &str) -> JsonResponse<T> {
+    //
+    //     let msg = if !message.trim().is_empty() {
+    //         message.to_string()
+    //     }
+    //     else{
+    //         String::from("Success")
+    //     };
+    //
+    //     JsonResponse {
+    //         message: msg,
+    //         id: Some(id),
+    //         item: None,
+    //         list: None,
+    //     }
+    // }
+
+    // pub(crate) fn not_found() -> Self {
+    //     JsonResponse {
+    //         id: None,
+    //         item: None,
+    //         message: format!("Object not found"),
+    //         list: None,
+    //     }
+    // }
+    //
+    // pub(crate) fn internal_error(message: &str) -> Self {
+    //
+    //     let msg = if !message.trim().is_empty() {
+    //         message.to_string()
+    //     }
+    //     else{
+    //         String::from("Internal error")
+    //     };
+    //     JsonResponse {
+    //         id: None,
+    //         item: None,
+    //         message: msg,
+    //         list: None,
+    //     }
+    // }
+    //
+    // pub(crate) fn not_valid(message: &str) -> Self {
+    //
+    //     let msg = if !message.trim().is_empty() {
+    //         message.to_string()
+    //     }
+    //     else{
+    //         String::from("Validation error")
+    //     };
+    //     JsonResponse {
+    //         id: None,
+    //         item: None,
+    //         message: msg,
+    //         list: None,
+    //     }
+    // }
 }
\ No newline at end of file
diff --git a/src/helpers/mod.rs b/src/helpers/mod.rs
index 203eb0da..e5439f74 100644
--- a/src/helpers/mod.rs
+++ b/src/helpers/mod.rs
@@ -1,4 +1,5 @@
 pub mod client;
 pub(crate) mod json;
+pub(crate) mod stack;
 
 pub use json::*;
diff --git a/src/helpers/stack/builder.rs b/src/helpers/stack/builder.rs
new file mode 100644
index 00000000..c319d8a9
--- /dev/null
+++ b/src/helpers/stack/builder.rs
@@ -0,0 +1,167 @@
+use crate::helpers::stack::dctypes::{
+    Compose,
+    Port,
+    Ports,
+    PublishedPort,
+    Service,
+    Services
+};
+use serde_yaml;
+use crate::forms::{StackForm, stack};
+use crate::models::stack::Stack;
+#[derive(Clone, Debug)]
+struct Config {}
+
+impl Default for Config {
+    fn default() -> Self {
+        Config {}
+    }
+}
+
+/// A builder for constructing docker compose.
+#[derive(Clone, Debug)]
+pub struct DcBuilder {
+    config: Config,
+    pub(crate) stack: Stack
+}
+
+impl TryInto<Vec<Port>> for stack::Ports {
+    type Error = String;
+    fn try_into(self) -> Result<Vec<Port>, Self::Error> {
+        convert_shared_ports(self.shared_ports.clone().unwrap())
+    }
+}
+
+
+fn convert_shared_ports(ports: Vec<String>) -> Result<Vec<Port>, String> {
+    let mut _ports: Vec<Port> = vec![];
+    for p in ports {
+        let port = p.parse::<u16>().map_err(|e| e.to_string())?;
+        _ports.push(Port {
+            target: port,
+            host_ip: None,
+            published: Some(PublishedPort::Single(port)),
+            protocol: None,
+            mode: None,
+        });
+    }
+    Ok(_ports)
+}
+
+impl DcBuilder {
+
+    pub fn new(stack: Stack) -> Self {
+        DcBuilder {
+            config: Config::default(),
+            stack: stack,
+        }
+    }
+
+
+    pub fn build(&self) -> Option<String> {
+
+        tracing::debug!("Start build docker compose from {:?}", &self.stack.body);
+        let _stack = serde_json::from_value::<StackForm>(self.stack.body.clone());
+        let mut services = indexmap::IndexMap::new();
+        match _stack  {
+            Ok(apps) => {
+                println!("stack item {:?}", apps.custom.web);
+
+                for app_type in apps.custom.web {
+                    let code = app_type.app.code.clone().to_owned();
+                    let mut service = Service {
+                        image: Some(app_type.app.docker_image.to_string()),
+                        ..Default::default()
+                    };
+
+                    if let Some(ports) = &app_type.app.ports {
+                        if !ports.shared_ports.clone()?.is_empty() {
+                            service.ports = Ports::Long(app_type.app.ports?.try_into().unwrap())
+                        }
+                    }
+
+                    service.restart = Some("always".to_owned());
+                    services.insert(
+                        code,
+                        Some(service),
+                    );
+                }
+
+                if let Some(srvs) = apps.custom.service {
+
+                    if !srvs.is_empty() {
+
+                        for app_type in srvs {
+                            let code = app_type.app.code.to_owned();
+                            let mut service = Service {
+                                image: Some(app_type.app.docker_image.to_string()),
+                                ..Default::default()
+                            };
+
+                            if let Some(ports) = &app_type.app.ports {
+                                if !ports.shared_ports.clone()?.is_empty() {
+                                    service.ports = Ports::Long(app_type.app.ports?.try_into().unwrap())
+                                }
+                            }
+                            service.restart = Some("always".to_owned());
+                            services.insert(
+                                code,
+                                Some(service),
+                            );
+                        }
+                    }
+                }
+                if let Some(features) = apps.custom.feature {
+
+                    if !features.is_empty() {
+
+                        for app_type in features {
+                            let code = app_type.app.code.to_owned();
+                            let mut service = Service {
+                                // image: Some(app.dockerhub_image.as_ref().unwrap().to_owned()),
+                                image: Some(app_type.app.docker_image.to_string()),
+                                ..Default::default()
+                            };
+
+                            if let Some(ports) = &app_type.app.ports {
+                                if !ports.shared_ports.clone()?.is_empty() {
+                                    service.ports = Ports::Long(app_type.app.ports?.try_into().unwrap())
+                                }
+                            }
+                            service.restart = Some("always".to_owned());
+                            services.insert(
+                                code,
+                                Some(service),
+                            );
+                        }
+                    }
+                }
+            }
+            Err(e) => {
+                tracing::debug!("Unpack stack form {:?}", e);
+                ()
+            }
+        }
+
+        let compose_content = Compose {
+            version: Some("3.8".to_string()),
+            services: {
+                Services(services)
+            },
+            ..Default::default()
+        };
+
+        let fname= format!("./files/{}.yml", self.stack.stack_id);
+        tracing::debug!("Save docker compose to file {:?}", fname);
+        let target_file = std::path::Path::new(fname.as_str());
+        // serialize to string
+        let serialized = match serde_yaml::to_string(&compose_content) {
+            Ok(s) => s,
+            Err(e) => panic!("Failed to serialize docker-compose file: {}", e),
+        };
+        // serialize to file
+        std::fs::write(target_file, serialized.clone()).unwrap();
+
+        Some(serialized)
+    }
+}
diff --git a/src/helpers/stack/dctypes.rs b/src/helpers/stack/dctypes.rs
new file mode 100644
index 00000000..931deab1
--- /dev/null
+++ b/src/helpers/stack/dctypes.rs
@@ -0,0 +1,842 @@
+use derive_builder::*;
+#[cfg(feature = "indexmap")]
+use indexmap::IndexMap;
+use serde::{Deserialize, Serialize};
+use serde_yaml::Value;
+#[cfg(not(feature = "indexmap"))]
+use std::collections::HashMap;
+use std::convert::TryFrom;
+use std::fmt;
+use std::str::FromStr;
+
+#[allow(clippy::large_enum_variant)]
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
+#[serde(untagged)]
+pub enum ComposeFile {
+    V2Plus(Compose),
+    #[cfg(feature = "indexmap")]
+    V1(IndexMap<String, Service>),
+    #[cfg(not(feature = "indexmap"))]
+    V1(HashMap<String, Service>),
+    Single(SingleService),
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)]
+pub struct SingleService {
+    pub service: Service,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)]
+pub struct Compose {
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub version: Option<String>,
+    #[serde(default, skip_serializing_if = "Services::is_empty")]
+    pub services: Services,
+    #[serde(default, skip_serializing_if = "TopLevelVolumes::is_empty")]
+    pub volumes: TopLevelVolumes,
+    #[serde(default, skip_serializing_if = "ComposeNetworks::is_empty")]
+    pub networks: ComposeNetworks,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub service: Option<Service>,
+    #[cfg(feature = "indexmap")]
+    #[serde(flatten, skip_serializing_if = "IndexMap::is_empty")]
+    pub extensions: IndexMap<Extension, Value>,
+    #[cfg(not(feature = "indexmap"))]
+    #[serde(flatten, skip_serializing_if = "HashMap::is_empty")]
+    pub extensions: HashMap<Extension, Value>,
+}
+
+impl Compose {
+    pub fn new() -> Self {
+        Default::default()
+    }
+}
+
+#[derive(Builder, Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
+#[builder(setter(into), default)]
+pub struct Service {
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub hostname: Option<String>,
+    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
+    pub privileged: bool,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub healthcheck: Option<Healthcheck>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub deploy: Option<Deploy>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub image: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub container_name: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none", rename = "build")]
+    pub build_: Option<BuildStep>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub pid: Option<String>,
+    #[serde(default, skip_serializing_if = "Ports::is_empty")]
+    pub ports: Ports,
+    #[serde(default, skip_serializing_if = "Environment::is_empty")]
+    pub environment: Environment,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub network_mode: Option<String>,
+    #[serde(default, skip_serializing_if = "Vec::is_empty")]
+    pub devices: Vec<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub restart: Option<String>,
+    #[serde(default, skip_serializing_if = "Labels::is_empty")]
+    pub labels: Labels,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub tmpfs: Option<Tmpfs>,
+    #[serde(default, skip_serializing_if = "Ulimits::is_empty")]
+    pub ulimits: Ulimits,
+    #[serde(default, skip_serializing_if = "Volumes::is_empty")]
+    pub volumes: Volumes,
+    #[serde(default, skip_serializing_if = "Networks::is_empty")]
+    pub networks: Networks,
+    #[serde(default, skip_serializing_if = "Vec::is_empty")]
+    pub cap_add: Vec<String>,
+    #[serde(default, skip_serializing_if = "DependsOnOptions::is_empty")]
+    pub depends_on: DependsOnOptions,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub command: Option<Command>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub entrypoint: Option<Entrypoint>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub env_file: Option<EnvFile>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub stop_grace_period: Option<String>,
+    #[serde(default, skip_serializing_if = "Vec::is_empty")]
+    pub profiles: Vec<String>,
+    #[serde(default, skip_serializing_if = "Vec::is_empty")]
+    pub links: Vec<String>,
+    #[serde(default, skip_serializing_if = "Vec::is_empty")]
+    pub dns: Vec<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub ipc: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub net: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub stop_signal: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub user: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub working_dir: Option<String>,
+    #[serde(default, skip_serializing_if = "Vec::is_empty")]
+    pub expose: Vec<String>,
+    #[serde(default, skip_serializing_if = "Vec::is_empty")]
+    pub volumes_from: Vec<String>,
+    #[cfg(feature = "indexmap")]
+    #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
+    pub extends: IndexMap<String, String>,
+    #[cfg(not(feature = "indexmap"))]
+    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
+    pub extends: HashMap<String, String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub logging: Option<LoggingParameters>,
+    #[serde(default, skip_serializing_if = "is_zero")]
+    pub scale: i64,
+    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
+    pub init: bool,
+    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
+    pub stdin_open: bool,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub shm_size: Option<String>,
+    #[cfg(feature = "indexmap")]
+    #[serde(flatten, skip_serializing_if = "IndexMap::is_empty")]
+    pub extensions: IndexMap<Extension, Value>,
+    #[cfg(not(feature = "indexmap"))]
+    #[serde(flatten, skip_serializing_if = "HashMap::is_empty")]
+    pub extensions: HashMap<Extension, Value>,
+    #[serde(default, skip_serializing_if = "Vec::is_empty")]
+    pub extra_hosts: Vec<String>,
+    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
+    pub tty: bool,
+    #[serde(default, skip_serializing_if = "SysCtls::is_empty")]
+    pub sysctls: SysCtls,
+    #[serde(default, skip_serializing_if = "Vec::is_empty")]
+    pub security_opt: Vec<String>,
+}
+
+impl Service {
+    pub fn image(&self) -> &str {
+        self.image.as_deref().unwrap_or_default()
+    }
+
+    pub fn network_mode(&self) -> &str {
+        self.network_mode.as_deref().unwrap_or_default()
+    }
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
+#[serde(untagged)]
+pub enum EnvFile {
+    Simple(String),
+    List(Vec<String>),
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
+#[serde(untagged)]
+pub enum DependsOnOptions {
+    Simple(Vec<String>),
+    #[cfg(feature = "indexmap")]
+    Conditional(IndexMap<String, DependsCondition>),
+    #[cfg(not(feature = "indexmap"))]
+    Conditional(HashMap<String, DependsCondition>),
+}
+
+impl Default for DependsOnOptions {
+    fn default() -> Self {
+        Self::Simple(Vec::new())
+    }
+}
+
+impl DependsOnOptions {
+    pub fn is_empty(&self) -> bool {
+        match self {
+            Self::Simple(v) => v.is_empty(),
+            Self::Conditional(m) => m.is_empty(),
+        }
+    }
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
+pub struct DependsCondition {
+    pub condition: String,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
+pub struct LoggingParameters {
+    pub driver: String,
+    #[cfg(feature = "indexmap")]
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub options: Option<IndexMap<String, SingleValue>>,
+    #[cfg(not(feature = "indexmap"))]
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub options: Option<HashMap<String, SingleValue>>,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
+#[serde(untagged)]
+pub enum Ports {
+    Short(Vec<String>),
+    Long(Vec<Port>),
+}
+
+impl Default for Ports {
+    fn default() -> Self {
+        Self::Short(Vec::default())
+    }
+}
+
+impl Ports {
+    pub fn is_empty(&self) -> bool {
+        match self {
+            Self::Short(v) => v.is_empty(),
+            Self::Long(v) => v.is_empty(),
+        }
+    }
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
+pub struct Port {
+    pub target: u16,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub host_ip: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub published: Option<PublishedPort>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub protocol: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub mode: Option<String>,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
+#[serde(untagged)]
+pub enum PublishedPort {
+    Single(u16),
+    Range(String),
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
+#[serde(untagged)]
+pub enum Environment {
+    List(Vec<String>),
+    #[cfg(feature = "indexmap")]
+    KvPair(IndexMap<String, Option<SingleValue>>),
+    #[cfg(not(feature = "indexmap"))]
+    KvPair(HashMap<String, Option<SingleValue>>),
+}
+
+impl Default for Environment {
+    fn default() -> Self {
+        Self::List(Vec::new())
+    }
+}
+
+impl Environment {
+    pub fn is_empty(&self) -> bool {
+        match self {
+            Self::List(v) => v.is_empty(),
+            Self::KvPair(m) => m.is_empty(),
+        }
+    }
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash, Default, Ord, PartialOrd)]
+#[serde(try_from = "String")]
+pub struct Extension(String);
+
+impl FromStr for Extension {
+    type Err = ExtensionParseError;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        let owned = s.to_owned();
+        Extension::try_from(owned)
+    }
+}
+
+impl TryFrom<String> for Extension {
+    type Error = ExtensionParseError;
+
+    fn try_from(s: String) -> Result<Self, Self::Error> {
+        if s.starts_with("x-") {
+            Ok(Self(s))
+        } else {
+            Err(ExtensionParseError(s))
+        }
+    }
+}
+
+/// The result of a failed TryFrom<String> conversion for [`Extension`]
+///
+/// Contains the string that was being converted
+#[derive(Clone, Debug, Eq, PartialEq, Hash)]
+pub struct ExtensionParseError(pub String);
+
+impl fmt::Display for ExtensionParseError {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        write!(f, "unknown attribute {:?}, extensions must start with 'x-' (see https://docs.docker.com/compose/compose-file/#extension)", self.0)
+    }
+}
+
+impl std::error::Error for ExtensionParseError {}
+
+#[cfg(feature = "indexmap")]
+#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)]
+pub struct Services(pub IndexMap<String, Option<Service>>);
+#[cfg(not(feature = "indexmap"))]
+#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)]
+pub struct Services(pub HashMap<String, Option<Service>>);
+
+impl Services {
+    pub fn is_empty(&self) -> bool {
+        self.0.is_empty()
+    }
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
+#[serde(untagged)]
+pub enum Labels {
+    List(Vec<String>),
+    #[cfg(feature = "indexmap")]
+    Map(IndexMap<String, String>),
+    #[cfg(not(feature = "indexmap"))]
+    Map(HashMap<String, String>),
+}
+
+impl Default for Labels {
+    fn default() -> Self {
+        Self::List(Vec::new())
+    }
+}
+
+impl Labels {
+    pub fn is_empty(&self) -> bool {
+        match self {
+            Self::List(v) => v.is_empty(),
+            Self::Map(m) => m.is_empty(),
+        }
+    }
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
+#[serde(untagged)]
+pub enum Tmpfs {
+    Simple(String),
+    List(Vec<String>),
+}
+
+#[cfg(feature = "indexmap")]
+#[derive(Clone, Default, Debug, Serialize, Deserialize, Eq, PartialEq)]
+pub struct Ulimits(pub IndexMap<String, Ulimit>);
+#[cfg(not(feature = "indexmap"))]
+#[derive(Clone, Default, Debug, Serialize, Deserialize, Eq, PartialEq)]
+pub struct Ulimits(pub HashMap<String, Ulimit>);
+
+impl Ulimits {
+    pub fn is_empty(&self) -> bool {
+        self.0.is_empty()
+    }
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
+#[serde(untagged)]
+pub enum Ulimit {
+    Single(i64),
+    SoftHard { soft: i64, hard: i64 },
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
+#[serde(untagged)]
+pub enum Networks {
+    Simple(Vec<String>),
+    Advanced(AdvancedNetworks),
+}
+
+impl Default for Networks {
+    fn default() -> Self {
+        Self::Simple(Vec::new())
+    }
+}
+
+impl Networks {
+    pub fn is_empty(&self) -> bool {
+        match self {
+            Self::Simple(n) => n.is_empty(),
+            Self::Advanced(n) => n.0.is_empty(),
+        }
+    }
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
+#[serde(untagged)]
+pub enum BuildStep {
+    Simple(String),
+    Advanced(AdvancedBuildStep),
+}
+
+#[derive(Builder, Clone, Debug, Deserialize, Serialize, Eq, PartialEq, Default)]
+#[serde(deny_unknown_fields)]
+#[builder(setter(into), default)]
+pub struct AdvancedBuildStep {
+    pub context: String,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub dockerfile: Option<String>,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub args: Option<BuildArgs>,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub shm_size: Option<u64>,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub target: Option<String>,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub network: Option<String>,
+    #[serde(default, skip_serializing_if = "Vec::is_empty")]
+    pub cache_from: Vec<String>,
+    #[serde(default, skip_serializing_if = "Labels::is_empty")]
+    pub labels: Labels,
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
+#[serde(untagged)]
+pub enum BuildArgs {
+    Simple(String),
+    List(Vec<String>),
+    #[cfg(feature = "indexmap")]
+    KvPair(IndexMap<String, String>),
+    #[cfg(not(feature = "indexmap"))]
+    KvPair(HashMap<String, String>),
+}
+
+#[cfg(feature = "indexmap")]
+#[derive(Clone, Default, Debug, Serialize, Deserialize, Eq, PartialEq)]
+pub struct AdvancedNetworks(pub IndexMap<String, MapOrEmpty<AdvancedNetworkSettings>>);
+#[cfg(not(feature = "indexmap"))]
+#[derive(Clone, Default, Debug, Serialize, Deserialize, Eq, PartialEq)]
+pub struct AdvancedNetworks(pub HashMap<String, MapOrEmpty<AdvancedNetworkSettings>>);
+
+#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq, Hash)]
+#[serde(deny_unknown_fields)]
+pub struct AdvancedNetworkSettings {
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub ipv4_address: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub ipv6_address: Option<String>,
+    #[serde(default, skip_serializing_if = "Vec::is_empty")]
+    pub aliases: Vec<String>,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
+#[serde(untagged)]
+pub enum SysCtls {
+    List(Vec<String>),
+    #[cfg(feature = "indexmap")]
+    Map(IndexMap<String, Option<SingleValue>>),
+    #[cfg(not(feature = "indexmap"))]
+    Map(HashMap<String, Option<SingleValue>>),
+}
+
+impl Default for SysCtls {
+    fn default() -> Self {
+        Self::List(Vec::new())
+    }
+}
+
+impl SysCtls {
+    pub fn is_empty(&self) -> bool {
+        match self {
+            Self::List(v) => v.is_empty(),
+            Self::Map(m) => m.is_empty(),
+        }
+    }
+}
+
+#[cfg(feature = "indexmap")]
+#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)]
+pub struct TopLevelVolumes(pub IndexMap<String, MapOrEmpty<ComposeVolume>>);
+#[cfg(not(feature = "indexmap"))]
+#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)]
+pub struct TopLevelVolumes(pub HashMap<String, MapOrEmpty<ComposeVolume>>);
+
+impl TopLevelVolumes {
+    pub fn is_empty(&self) -> bool {
+        self.0.is_empty()
+    }
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
+pub struct ComposeVolume {
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub driver: Option<String>,
+    #[cfg(feature = "indexmap")]
+    #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
+    pub driver_opts: IndexMap<String, Option<SingleValue>>,
+    #[cfg(not(feature = "indexmap"))]
+    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
+    pub driver_opts: HashMap<String, Option<SingleValue>>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub external: Option<ExternalVolume>,
+    #[serde(default, skip_serializing_if = "Labels::is_empty")]
+    pub labels: Labels,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub name: Option<String>,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
+#[serde(untagged)]
+pub enum ExternalVolume {
+    Bool(bool),
+    Name { name: String },
+}
+
+#[cfg(feature = "indexmap")]
+#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)]
+pub struct ComposeNetworks(pub IndexMap<String, MapOrEmpty<NetworkSettings>>);
+
+#[cfg(not(feature = "indexmap"))]
+#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)]
+pub struct ComposeNetworks(pub HashMap<String, MapOrEmpty<NetworkSettings>>);
+
+impl ComposeNetworks {
+    pub fn is_empty(&self) -> bool {
+        self.0.is_empty()
+    }
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
+#[serde(untagged)]
+pub enum ComposeNetwork {
+    Detailed(ComposeNetworkSettingDetails),
+    Bool(bool),
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
+#[serde(deny_unknown_fields)]
+pub struct ComposeNetworkSettingDetails {
+    pub name: String,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
+#[serde(deny_unknown_fields)]
+pub struct ExternalNetworkSettingBool(bool);
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)]
+#[serde(deny_unknown_fields)]
+pub struct NetworkSettings {
+    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
+    pub attachable: bool,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub driver: Option<String>,
+    #[cfg(feature = "indexmap")]
+    #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
+    pub driver_opts: IndexMap<String, Option<SingleValue>>,
+    #[cfg(not(feature = "indexmap"))]
+    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
+    pub driver_opts: HashMap<String, Option<SingleValue>>,
+    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
+    pub enable_ipv6: bool,
+    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
+    pub internal: bool,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub external: Option<ComposeNetwork>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub ipam: Option<Ipam>,
+    #[serde(default, skip_serializing_if = "Labels::is_empty")]
+    pub labels: Labels,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub name: Option<String>,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
+#[serde(deny_unknown_fields)]
+pub struct Ipam {
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub driver: Option<String>,
+    #[serde(default, skip_serializing_if = "Vec::is_empty")]
+    pub config: Vec<IpamConfig>,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
+#[serde(deny_unknown_fields)]
+pub struct IpamConfig {
+    pub subnet: String,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub gateway: Option<String>,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)]
+#[serde(deny_unknown_fields)]
+pub struct Deploy {
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub mode: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub replicas: Option<i64>,
+    #[serde(default, skip_serializing_if = "Vec::is_empty")]
+    pub labels: Vec<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub update_config: Option<UpdateConfig>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub resources: Option<Resources>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub restart_policy: Option<RestartPolicy>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub placement: Option<Placement>,
+}
+
+fn is_zero(val: &i64) -> bool {
+    *val == 0
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
+#[serde(deny_unknown_fields)]
+pub struct Healthcheck {
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub test: Option<HealthcheckTest>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub interval: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub timeout: Option<String>,
+    #[serde(default, skip_serializing_if = "is_zero")]
+    pub retries: i64,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub start_period: Option<String>,
+    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
+    pub disable: bool,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
+#[serde(untagged)]
+pub enum HealthcheckTest {
+    Single(String),
+    Multiple(Vec<String>),
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash, Default)]
+#[serde(deny_unknown_fields)]
+pub struct Limits {
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub cpus: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub memory: Option<String>,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash, Default)]
+#[serde(deny_unknown_fields)]
+pub struct Placement {
+    #[serde(skip_serializing_if = "Vec::is_empty", default)]
+    pub constraints: Vec<String>,
+    #[serde(skip_serializing_if = "Vec::is_empty", default)]
+    pub preferences: Vec<Preferences>,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
+#[serde(deny_unknown_fields)]
+pub struct Preferences {
+    pub spread: String,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash, Default)]
+#[serde(deny_unknown_fields)]
+pub struct Resources {
+    pub limits: Option<Limits>,
+    pub reservations: Option<Limits>,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash, Default)]
+#[serde(deny_unknown_fields)]
+pub struct RestartPolicy {
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub condition: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub delay: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub max_attempts: Option<i64>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub window: Option<String>,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)]
+#[serde(deny_unknown_fields)]
+pub struct UpdateConfig {
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub parallelism: Option<i64>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub delay: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub failure_action: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub monitor: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub max_failure_ratio: Option<f64>,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
+#[serde(untagged)]
+pub enum Volumes {
+    Simple(Vec<String>),
+    Advanced(Vec<AdvancedVolumes>),
+}
+
+impl Default for Volumes {
+    fn default() -> Self {
+        Self::Simple(Vec::new())
+    }
+}
+
+impl Volumes {
+    pub fn is_empty(&self) -> bool {
+        match self {
+            Self::Simple(v) => v.is_empty(),
+            Self::Advanced(v) => v.is_empty(),
+        }
+    }
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
+#[serde(deny_unknown_fields)]
+pub struct AdvancedVolumes {
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub source: Option<String>,
+    pub target: String,
+    #[serde(rename = "type")]
+    pub _type: String,
+    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
+    pub read_only: bool,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub bind: Option<Bind>,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub volume: Option<Volume>,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub tmpfs: Option<TmpfsSettings>,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash, Default)]
+#[serde(deny_unknown_fields)]
+pub struct Bind {
+    pub propagation: String,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash, Default)]
+#[serde(deny_unknown_fields)]
+pub struct Volume {
+    pub nocopy: bool,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash, Default)]
+#[serde(deny_unknown_fields)]
+pub struct TmpfsSettings {
+    pub size: u64,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
+#[serde(untagged)]
+pub enum Command {
+    Simple(String),
+    Args(Vec<String>),
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
+#[serde(untagged)]
+pub enum Entrypoint {
+    Simple(String),
+    List(Vec<String>),
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, PartialOrd)]
+#[serde(untagged)]
+pub enum SingleValue {
+    String(String),
+    Bool(bool),
+    Unsigned(u64),
+    Signed(i64),
+    Float(f64),
+}
+
+impl fmt::Display for SingleValue {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            Self::String(s) => f.write_str(s),
+            Self::Bool(b) => write!(f, "{b}"),
+            Self::Unsigned(u) => write!(f, "{u}"),
+            Self::Signed(i) => write!(f, "{i}"),
+            Self::Float(fl) => write!(f, "{fl}"),
+        }
+    }
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Hash)]
+#[serde(untagged)]
+pub enum MapOrEmpty<T> {
+    Map(T),
+    Empty,
+}
+
+impl<T> Default for MapOrEmpty<T> {
+    fn default() -> Self {
+        Self::Empty
+    }
+}
+
+impl<T> From<MapOrEmpty<T>> for Option<T> {
+    fn from(value: MapOrEmpty<T>) -> Self {
+        match value {
+            MapOrEmpty::Map(t) => Some(t),
+            MapOrEmpty::Empty => None,
+        }
+    }
+}
+
+impl<T> Serialize for MapOrEmpty<T>
+    where
+        T: Serialize,
+{
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+        where
+            S: serde::Serializer,
+    {
+        match self {
+            Self::Map(t) => t.serialize(serializer),
+            Self::Empty => {
+                use serde::ser::SerializeMap;
+                serializer.serialize_map(None)?.end()
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/helpers/stack/mod.rs b/src/helpers/stack/mod.rs
new file mode 100644
index 00000000..7da2d2ec
--- /dev/null
+++ b/src/helpers/stack/mod.rs
@@ -0,0 +1,2 @@
+pub(crate) mod builder;
+pub(crate) mod dctypes;
\ No newline at end of file
diff --git a/src/models/stack.rs b/src/models/stack.rs
index 21432d35..f9ce272e 100644
--- a/src/models/stack.rs
+++ b/src/models/stack.rs
@@ -3,7 +3,7 @@ use serde_json::Value;
 use uuid::Uuid;
 use serde::{Serialize,Deserialize};
 
-#[derive(Debug, Serialize, Deserialize)]
+#[derive(Debug, Serialize, Deserialize, Clone)]
 pub struct Stack {
     pub id: i32,       // id - is a unique identifier for the app stack
     pub stack_id: Uuid, // external stack ID
@@ -14,3 +14,13 @@ pub struct Stack {
     pub created_at: DateTime<Utc>,
     pub updated_at: DateTime<Utc>,
 }
+
+impl Default for Stack {
+    fn default() -> Self {
+        Stack {
+            user_id: "".to_string(),
+            name: "".to_string(),
+                ..Default::default()
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/routes/client/add.rs b/src/routes/client/add.rs
index 6803cbba..f5acec2a 100644
--- a/src/routes/client/add.rs
+++ b/src/routes/client/add.rs
@@ -1,20 +1,12 @@
 use crate::configuration::Settings;
-use crate::helpers::client;
+use crate::helpers::{client, JsonResponse};
 use crate::models::user::User;
 use crate::models::Client;
 use actix_web::{error::ErrorInternalServerError, post, web, Responder, Result};
-use serde::Serialize;
 use sqlx::PgPool;
 use std::sync::Arc;
 use tracing::Instrument;
 
-#[derive(Serialize)]
-struct ClientAddResponse {
-    status: String,
-    message: String,
-    code: u32,
-    client: Option<Client>,
-}
 
 #[tracing::instrument(name = "Add client.")]
 #[post("")]
@@ -24,6 +16,7 @@ pub async fn add_handler(
     pool: web::Data<PgPool>,
 ) -> Result<impl Responder> {
     let query_span = tracing::info_span!("Counting the user's clients");
+
     match sqlx::query!(
         r#"
         SELECT
@@ -46,17 +39,12 @@ pub async fn add_handler(
                     client_count
                 );
 
-                return Ok(web::Json(ClientAddResponse {
-                    status: "error".to_string(),
-                    code: 400,
-                    message: "Too many clients already created".to_string(),
-                    client: None,
-                }));
+                return JsonResponse::build().err("Too many clients created".to_owned());
             }
         }
         Err(e) => {
             tracing::error!("Failed to execute query: {:?}", e);
-            return Err(ErrorInternalServerError(""));
+            return JsonResponse::build().internal_error("Internal Server Error".to_owned());
         }
     };
 
@@ -85,16 +73,16 @@ pub async fn add_handler(
         Ok(result) => {
             tracing::info!("New client {} have been saved to database", result.id);
             client.id = result.id;
-            Ok(web::Json(ClientAddResponse {
-                status: "success".to_string(),
-                message: "".to_string(),
-                code: 200,
-                client: Some(client),
-            }))
+
+            return JsonResponse::build()
+                .set_id(client.id)
+                .set_item(Some(client))
+                .ok("OK".to_owned());
         }
         Err(e) => {
             tracing::error!("Failed to execute query: {:?}", e);
-            return Err(ErrorInternalServerError(""));
+            let err = format!("Failed to insert. {}", e);
+            return JsonResponse::build().err(err);
         }
     }
 }
diff --git a/src/routes/rating/add.rs b/src/routes/rating/add.rs
index fda82486..2b344f98 100644
--- a/src/routes/rating/add.rs
+++ b/src/routes/rating/add.rs
@@ -3,16 +3,20 @@ use crate::helpers::{JsonResponse, JsonResponseBuilder};
 use crate::models;
 use crate::models::user::User;
 use crate::models::RateCategory;
-use actix_web::post;
-use actix_web::{web, Responder, Result};
 use sqlx::PgPool;
 use tracing::Instrument;
+use actix_web::{
+    web,
+    post,
+    Responder, Result,
+};
 
 // workflow
 // add, update, list, get(user_id), ACL,
 // ACL - access to func for a user
 // ACL - access to objects for a user
 
+
 #[tracing::instrument(name = "Add rating.")]
 #[post("")]
 pub async fn add_handler(
@@ -59,10 +63,7 @@ pub async fn add_handler(
                 form.obj_id,
                 form.category
             );
-
-            return JsonResponse::<models::Rating>::build()
-                .set_id(record.id)
-                .ok(format!("Already Rated"));
+            return JsonResponse::build().conflict("Already rated".to_owned());
         }
         Err(sqlx::Error::RowNotFound) => {}
         Err(e) => {
@@ -97,11 +98,11 @@ pub async fn add_handler(
 
             return JsonResponse::build()
                 .set_id(result.id)
-                .ok("Saved".to_string());
+                .ok("Saved".to_owned());
         }
         Err(e) => {
             tracing::error!("Failed to execute query: {:?}", e);
-            return JsonResponse::build().err("Failed to insert".to_string());
+            return JsonResponse::build().internal_error("Failed to insert".to_owned());
         }
     }
 }
diff --git a/src/routes/rating/get.rs b/src/routes/rating/get.rs
index 01e847fe..a956b633 100644
--- a/src/routes/rating/get.rs
+++ b/src/routes/rating/get.rs
@@ -17,6 +17,7 @@ pub async fn get_handler(
     path: web::Path<(i32,)>,
     pool: web::Data<PgPool>,
 ) -> Result<impl Responder> {
+    /// Get rating of any user
     let rate_id = path.0;
     let query_span = tracing::info_span!("Search for rate id={}.", rate_id);
     match sqlx::query_as!(
@@ -44,7 +45,12 @@ pub async fn get_handler(
 
 #[tracing::instrument(name = "Get all ratings.")]
 #[get("")]
-pub async fn default(path: web::Path<()>, pool: web::Data<PgPool>) -> Result<impl Responder> {
+pub async fn list_handler(
+    path: web::Path<()>,
+    pool: web::Data<PgPool>,
+) -> Result<impl Responder> {
+    /// Get ratings of all users
+
     let query_span = tracing::info_span!("Get all rates.");
     // let category = path.0;
     match sqlx::query_as!(models::Rating, r"SELECT * FROM rating")
@@ -54,14 +60,14 @@ pub async fn default(path: web::Path<()>, pool: web::Data<PgPool>) -> Result<imp
     {
         Ok(rating) => {
             tracing::info!("Ratings found: {:?}", rating.len());
-            return JsonResponse::build().set_list(rating).ok("OK".to_string());
+            return JsonResponse::build().set_list(rating).ok("OK".to_owned());
         }
         Err(sqlx::Error::RowNotFound) => {
-            return JsonResponse::build().err("Not Found".to_string());
+            return JsonResponse::build().not_found("Not Found".to_owned());
         }
         Err(e) => {
             tracing::error!("Failed to fetch rating, error: {:?}", e);
-            return JsonResponse::build().err("Internal Server Error".to_string());
+            return JsonResponse::build().internal_error("Internal Server Error".to_owned());
         }
     }
 }
diff --git a/src/routes/stack/add.rs b/src/routes/stack/add.rs
index bbe2e8a7..27260e04 100644
--- a/src/routes/stack/add.rs
+++ b/src/routes/stack/add.rs
@@ -1,12 +1,11 @@
 use actix_web::{
     web,
-    web::{Bytes, Data, Json},
+    web::{Bytes, Data},
     Responder, Result,
 };
 use crate::forms::stack::StackForm;
 use crate::helpers::JsonResponse;
 use crate::models::user::User;
-use crate::models::Stack;
 use actix_web::post;
 use chrono::Utc;
 use serde_json::Value;
@@ -16,6 +15,7 @@ use tracing::Instrument;
 use uuid::Uuid;
 use crate::models;
 
+
 #[tracing::instrument(name = "Add stack.")]
 #[post("")]
 pub async fn add(
@@ -28,21 +28,19 @@ pub async fn add(
     let body_str = str::from_utf8(&body_bytes).unwrap();
     let form = match serde_json::from_str::<StackForm>(body_str) {
         Ok(f) => {
-            println!("fine");
             f
         }
-        Err(err) => {
-            let err = format!("Error: {}", err);
-            return JsonResponse::<StackForm>::build().err(err);
+        Err(_err) => {
+            let msg = format!("Invalid data. {:?}", _err);
+            return JsonResponse::<StackForm>::build().err("Invalid data".to_owned());
         }
     };
-    // println!("app: {:?}", form);
 
     let user_id = user.id.clone();
     let request_id = Uuid::new_v4();
     let request_span = tracing::info_span!(
         "Validating a new stack", %request_id,
-        commonDomain=?&form.common_domain,
+        commonDomain=?&form.custom.project_name,
         region=?&form.region,
         domainList=?&form.domain_list
     );
@@ -52,7 +50,7 @@ pub async fn add(
     tracing::info!(
         "request_id {} Adding '{}' '{}' as a new stack",
         request_id,
-        form.common_domain,
+        form.custom.project_name,
         form.region
     );
 
@@ -67,33 +65,18 @@ pub async fn add(
         }
     };
 
-    let stack = Stack {
-        id: 0_i32,                // internal stack id
-        stack_id: Uuid::new_v4(), // public uuid of the stack
-        // user_id: Uuid::from_u128(user_id as u128),
-        user_id: user_id, //
-        name: stack_name,
-        body: body,
-        // body: body_str.to_string(),
-        created_at: Utc::now(),
-        updated_at: Utc::now(),
-    };
-
-    println!("stack object {:?}", stack);
     return match sqlx::query!(
         r#"
-        INSERT INTO user_stack (id, stack_id, user_id, name, body, created_at, updated_at)
-        VALUES ($1, $2, $3, $4, $5, $6, $7)
+        INSERT INTO user_stack (stack_id, user_id, name, body, created_at, updated_at)
+        VALUES ($1, $2, $3, $4, $5, $6)
         RETURNING id;
         "#,
-        0_i32,
-        stack.stack_id,
-        stack.user_id,
-        stack.name,
-        // sqlx::types::Json(stack.body),
-        stack.body,
-        stack.created_at,
-        stack.updated_at
+        Uuid::new_v4(),
+        user_id,
+        stack_name,
+        body,
+        Utc::now(),
+        Utc::now(),
     )
     .fetch_one(pool.get_ref())
     .instrument(query_span)
@@ -104,11 +87,12 @@ pub async fn add(
                 "req_id: {} New stack details have been saved to database",
                 request_id
             );
-            return JsonResponse::build().set_id(record.id).ok("OK".to_string());
+            return JsonResponse::build().set_id(record.id).ok("OK".to_owned());
         }
         Err(e) => {
             tracing::error!("req_id: {} Failed to execute query: {:?}", request_id, e);
-            return JsonResponse::build().err("Internal Server Error".to_string());
+            return JsonResponse::build().err("Internal Server Error".to_owned());
         }
     };
 }
+
diff --git a/src/routes/stack/compose.rs b/src/routes/stack/compose.rs
new file mode 100644
index 00000000..4d19a3f5
--- /dev/null
+++ b/src/routes/stack/compose.rs
@@ -0,0 +1,119 @@
+use actix_web::{
+    web,
+    web::{Data, Json},
+    Responder, Result,
+};
+
+use crate::helpers::JsonResponse;
+use crate::models::user::User;
+use crate::models::Stack;
+use actix_web::{get, post};
+use sqlx::PgPool;
+use std::str;
+use tracing::Instrument;
+use uuid::Uuid;
+use crate::helpers::stack::builder::DcBuilder;
+
+#[tracing::instrument(name = "User's generate docker-compose.")]
+#[post("/{id}")]
+pub async fn add(
+    user: web::ReqData<User>,
+    path: web::Path<(i32,)>,
+    pool: Data<PgPool>,
+) -> Result<impl Responder> {
+    let id = path.0;
+    tracing::debug!("Received id: {}", id);
+
+    let stack = match sqlx::query_as!(
+        Stack,
+        r#"
+        SELECT * FROM user_stack WHERE id=$1 AND user_id=$2 LIMIT 1
+        "#,
+        id, user.id
+    )
+        .fetch_one(pool.get_ref())
+        .await
+    {
+        Ok(stack) => {
+            tracing::info!("stack found: {:?}", stack.id,);
+            Some(stack)
+        }
+        Err(sqlx::Error::RowNotFound) => {
+            tracing::error!("Row not found 404");
+            None
+        }
+        Err(e) => {
+            tracing::error!("Failed to fetch stack, error: {:?}", e);
+            None
+        }
+    };
+
+    match stack {
+        Some(stack) => {
+            let id = stack.id.clone();
+            let mut dc = DcBuilder::new(stack);
+            let fc = dc.build();
+            tracing::debug!("Docker compose file content {:?}", fc);
+
+            return JsonResponse::build()
+                .set_id(id)
+                .set_item(fc.unwrap())
+                .ok("Success".to_owned());
+        }
+        None => {
+            return JsonResponse::build().err("Could not generate compose file".to_owned());
+        }
+    }
+}
+
+#[tracing::instrument(name = "Generate docker-compose. Admin")]
+#[get("/{id}/compose")]
+pub async fn admin(
+    user: web::ReqData<User>,
+    path: web::Path<(i32,)>,
+    pool: Data<PgPool>,
+) -> Result<impl Responder> {
+    ///  Admin function for generating compose file for specified user
+    let id = path.0;
+    tracing::debug!("Received id: {}", id);
+
+    let stack = match sqlx::query_as!(
+        Stack,
+        r#"
+        SELECT * FROM user_stack WHERE id=$1 LIMIT 1
+        "#,
+        id,
+    )
+        .fetch_one(pool.get_ref())
+        .await
+    {
+        Ok(stack) => {
+            tracing::info!("stack found: {:?}", stack.id,);
+            Some(stack)
+        }
+        Err(sqlx::Error::RowNotFound) => {
+            tracing::error!("Row not found 404");
+            None
+        }
+        Err(e) => {
+            tracing::error!("Failed to fetch stack, error: {:?}", e);
+            None
+        }
+    };
+
+    match stack {
+        Some(stack) => {
+            let id = stack.id.clone();
+            let mut dc = DcBuilder::new(stack);
+            let fc = dc.build();
+            // tracing::debug!("Docker compose file content {:?}", fc);
+            return JsonResponse::build()
+                .set_id(id)
+                .set_item(fc.unwrap()).ok("Success".to_owned());
+
+        }
+        None => {
+            return JsonResponse::build().err("Could not generate compose file".to_owned());
+        }
+    }
+}
diff --git a/src/routes/stack/deploy.rs b/src/routes/stack/deploy.rs
index 67edaa81..45c1b386 100644
--- a/src/routes/stack/deploy.rs
+++ b/src/routes/stack/deploy.rs
@@ -1,5 +1,105 @@
-use actix_web::HttpResponse;
+use std::sync::Arc;
+use actix_web::{
+    web,
+    post,
+    web::Data,
+    Responder, Result,
+};
+use crate::models::user::User;
+use crate::models::stack::Stack;
+use sqlx::PgPool;
+use lapin::{
+    options::*, publisher_confirm::Confirmation, BasicProperties, Connection,
+    ConnectionProperties
+};
+use crate::configuration::Settings;
+use crate::helpers::JsonResponse;
+use crate::helpers::stack::builder::DcBuilder;
+use futures_lite::stream::StreamExt;
+use crate::forms::StackPayload;
 
-pub async fn deploy() -> HttpResponse {
-    unimplemented!()
-}
\ No newline at end of file
+
+#[tracing::instrument(name = "Deploy for every user. Admin endpoint")]
+#[post("/{id}/deploy")]
+pub async fn add(
+    user: web::ReqData<User>,
+    path: web::Path<(i32,)>,
+    pool: Data<PgPool>,
+    sets: Data<Arc<Settings>>,
+) -> Result<impl Responder> {
+    let id = path.0;
+    tracing::debug!("Received id: {}", id);
+
+    let stack = match sqlx::query_as!(
+        Stack,
+        r#"
+        SELECT * FROM user_stack WHERE id=$1 LIMIT 1
+        "#,
+        id
+    )
+        .fetch_one(pool.get_ref())
+        .await
+    {
+        Ok(stack) => {
+            tracing::info!("Stack found: {:?}", stack.id,);
+            Some(stack)
+        }
+        Err(sqlx::Error::RowNotFound) => {
+            tracing::error!("Row not found 404");
+            None
+        }
+        Err(e) => {
+            tracing::error!("Failed to fetch stack, error: {:?}", e);
+            None
+        }
+    };
+
+    return match stack {
+        Some(stack) => {
+            let id = stack.id.clone();
+            let mut dc = DcBuilder::new(stack);
+            dc.build();
+
+            let addr = sets.amqp.connection_string();
+            let routing_key = "install.start.tfa.all.all".to_string();
+            tracing::debug!("Sending message to {:?}", routing_key);
+
+            let conn = Connection::connect(&addr, ConnectionProperties::default())
+                .await
+                .expect("Could not connect RabbitMQ");
+
+            tracing::info!("RABBITMQ CONNECTED");
+
+            let channel = conn.create_channel().await.unwrap();
+            let mut stack_data = serde_json::from_value::<StackPayload>(
+                dc.stack.body.clone()
+            ).unwrap();
+
+            stack_data.id = Some(id);
+            stack_data.user_token = Some(user.id.clone());
+            stack_data.user_email = Some(user.email.clone());
+            stack_data.stack_code = stack_data.custom.custom_stack_code.clone();
+
+            let payload = serde_json::to_string::<StackPayload>(&stack_data).unwrap();
+            let _payload = payload.as_bytes();
+
+            let confirm = channel
+                .basic_publish(
+                    "install",
+                    routing_key.as_str(),
+                    BasicPublishOptions::default(),
+                    _payload,
+                    BasicProperties::default(),
+                )
+                .await.unwrap()
+                .await.unwrap();
+
+            assert_eq!(confirm, Confirmation::NotRequested);
+            tracing::debug!("Message sent to rabbitmq");
+            return JsonResponse::<Stack>::build().set_id(id).ok("Success".to_owned());
+        }
+        None => {
+            JsonResponse::build().internal_error("Deployment failed".to_owned())
+        }
+    }
+}
diff --git a/src/routes/stack/get.rs b/src/routes/stack/get.rs
index 17f91816..271d2dcb 100644
--- a/src/routes/stack/get.rs
+++ b/src/routes/stack/get.rs
@@ -1,30 +1,24 @@
 use actix_web::{web, get, Responder, Result};
-use serde_derive::Serialize;
 use sqlx::PgPool;
+use crate::helpers::{JsonResponse, JsonResponseBuilder};
 use crate::models;
 use crate::models::user::User;
+use std::convert::From;
+use tracing::Instrument;
 
-#[derive(Serialize)]
-struct JsonResponse {
-    status: String,
-    message: String,
-    code: u32,
-    id: Option<i32>,
-    object: Option<models::Stack>,
-    objects: Option<Vec<models::Stack>>,
-}
 
-#[tracing::instrument(name = "Get stack.")]
+#[tracing::instrument(name = "Get logged user stack.")]
 #[get("/{id}")]
-pub async fn get(
+pub async fn item(
     user: web::ReqData<User>,
     path: web::Path<(i32,)>,
     pool: web::Data<PgPool>,
 ) -> Result<impl Responder> {
+    /// Get stack apps of logged user only
 
     let (id,) = path.into_inner();
 
-    tracing::info!("User {:?} is getting stack by id {:?}", user, id);
+    tracing::info!("User {:?} gets stack by id {:?}", user.id, id);
     match sqlx::query_as!(
         models::Stack,
         r#"
@@ -36,37 +30,59 @@ pub async fn get(
     .await
     {
         Ok(stack) => {
-            tracing::info!("stack found: {:?}", stack.id,);
-            let response = JsonResponse {
-                status: "Success".to_string(),
-                code: 200,
-                message: "".to_string(),
-                id: Some(stack.id),
-                object: Some(stack),
-                objects: None
-            };
-            return Ok(web::Json(response));
+            tracing::info!("Stack found: {:?}", stack.id,);
+            return JsonResponse::build().set_item(Some(stack)).ok("OK".to_owned());
         }
         Err(sqlx::Error::RowNotFound) => {
-            return Ok(web::Json(JsonResponse {
-                status: "Error".to_string(),
-                code: 404,
-                message: format!("Not Found"),
-                id: None,
-                object: None,
-                objects: None
-            }));
+            JsonResponse::build().not_found("Record not found".to_owned())
         }
         Err(e) => {
             tracing::error!("Failed to fetch stack, error: {:?}", e);
-            return Ok(web::Json(JsonResponse {
-                status: "Error".to_string(),
-                code: 500,
-                message: format!("Internal Server Error"),
-                id: None,
-                object: None,
-                objects: None
-            }));
+            return JsonResponse::build().internal_error("Could not fetch data".to_owned());
         }
     }
 }
+
+#[tracing::instrument(name = "Get user's stack list.")]
+#[get("/user/{id}")]
+pub async fn list(
+    user: web::ReqData<User>,
+    path: web::Path<(String,)>,
+    pool: web::Data<PgPool>,
+) -> Result<impl Responder> {
+
+    /// This is admin endpoint, used by a m2m app, client app is confidential
+    /// it should return stacks by user id
+    /// in order to pass validation at external deployment service
+
+    let (id,) = path.into_inner();
+    tracing::info!("Logged user: {:?}", user.id);
+    tracing::info!("Get stack list for user {:?}", id);
+
+    let query_span = tracing::info_span!("Get stacks by user id.");
+
+    match sqlx::query_as!(
+        models::Stack,
+        r#"
+        SELECT * FROM user_stack WHERE user_id=$1
+        "#,
+        id
+    )
+        .fetch_all(pool.get_ref())
+        .instrument(query_span)
+        .await
+    {
+        Ok(list) => {
+            return JsonResponse::build().set_list(list).ok("OK".to_string());
+        }
+        Err(sqlx::Error::RowNotFound) => {
+            tracing::error!("No stacks found for user: {:?}", &user.id);
+            return JsonResponse::build().not_found("No stacks found for user".to_string())
+        }
+        Err(e) => {
+            tracing::error!("Failed to fetch stack, error: {:?}", e);
+            return JsonResponse::build().internal_error("Could not fetch".to_string());
+        }
+    }
+}
+
diff --git a/src/routes/stack/mod.rs b/src/routes/stack/mod.rs
index f3e5bc95..27c80617 100644
--- a/src/routes/stack/mod.rs
+++ b/src/routes/stack/mod.rs
@@ -2,6 +2,8 @@ pub mod add;
 pub mod deploy;
 pub mod get;
 pub mod update;
+pub(crate) mod compose;
+
 pub use add::*;
 pub use update::*;
 pub use deploy::*;
diff --git a/src/startup.rs b/src/startup.rs
index 3f5c390f..3306fcd2 100644
--- a/src/startup.rs
+++ b/src/startup.rs
@@ -53,25 +53,20 @@ pub async fn run(
                     .wrap(Cors::permissive())
                     .service(crate::routes::rating::add_handler)
                     .service(crate::routes::rating::get_handler)
-                    .service(crate::routes::rating::default),
+                    .service(crate::routes::rating::list_handler),
             )
-            // .service(
-            //     web::resource("/stack/{id}")
-            //         .route(web::get()
-            //             .to(crate::routes::stack::get))
-            //         .route(web::post()
-            //             .to(crate::routes::stack::update))
-            //         .route(web::post()
-            //             .to(crate::routes::stack::add)),
-            // )
             .service(
                 web::scope("/stack")
                     .wrap(HttpAuthentication::bearer(
                         crate::middleware::trydirect::bearer_guard,
                     ))
                     .wrap(Cors::permissive())
-                    .service(crate::routes::stack::add::add)
-                    .service(crate::routes::stack::get::get),
+                    .service(crate::routes::stack::deploy::add)
+                    .service(crate::routes::stack::compose::add)
+                    .service(crate::routes::stack::compose::admin)
+                    .service(crate::routes::stack::get::item)
+                    .service(crate::routes::stack::get::list)
+                    .service(crate::routes::stack::add::add),
             )
             .app_data(db_pool.clone())
             .app_data(settings.clone())
diff --git a/custom-stack-payload-2.json b/tests/custom-stack-payload-2.json
similarity index 100%
rename from custom-stack-payload-2.json
rename to tests/custom-stack-payload-2.json
diff --git a/custom-stack-payload-3.json b/tests/custom-stack-payload-3.json
similarity index 100%
rename from custom-stack-payload-3.json
rename to tests/custom-stack-payload-3.json
diff --git a/tests/custom-stack-payload-4.json b/tests/custom-stack-payload-4.json
new file mode 100644
index 00000000..6f4bc7c4
--- /dev/null
+++ b/tests/custom-stack-payload-4.json
@@ -0,0 +1 @@
+{"commonDomain":"", "domainList":{}, "region":"fsn1", "zone":null, "server":"cx21", "os":"ubuntu-20.04", "ssl":"letsencrypt", "vars":[], "integrated_features":[],"extended_features":[],"subscriptions":["stack_migration"],"save_token":false,"cloud_token":"r6LAjqrynVt7pUwctVkzBlJmKjLOCxJIWjZFMLTkPYCCB4rsgphhEVhiL4DuO757","provider":"htz","stack_code":"custom-stack","selected_plan":"plan-individual-monthly","custom":{"web":[{"name":"Smarty Bot","code":"smarty-bot","domain":"smartybot.xyz","sharedPorts":["8000"],"versions":[],"custom":true,"type":"web","main":true,"_id":"lltkpq6p347kystct","dockerhub_user":"trydirect","dockerhub_name":"smarty-bot","url_app":"smartybot.xyz","url_git":"https://github.com/vsilent/smarty.git","disk_size":"1Gb","ram_size":"1Gb","cpu":1}],"feature":[{"_etag":null,"_id":198,"_created":"2022-04-27T14:10:27.280327","_updated":"2023-08-03T08:24:18.958721","name":"Portainer CE Feature","code":"portainer_ce_feature","role":["portainer-ce-feature"],"type":"feature","default":null,"popularity":null,"descr":null,"ports":{"public":["9000","8000"]},"commercial":null,"subscription":null,"autodeploy":null,"suggested":null,"dependency":null,"avoid_render":null,"price":null,"icon":{"light":{"width":1138,"height":1138,"image":"08589075-44e6-430e-98a5-f9dcf711e054.svg"},"dark":{}},"category_id":2,"parent_app_id":null,"full_description":null,"description":"<p class=\"content-editor__unstyled\"><strong style=\"font-weight:bold\">Portainer is a lightweight management UI which allows you to easily manage your different Docker environments (Docker hosts or Swarm clusters)</strong></p>","plan_type":null,"ansible_var":null,"repo_dir":null,"cpu":"0.6","ram_size":"1Gb","disk_size":"1Gb","dockerhub_image":"portainer-ce-feature","versions":[{"_etag":null,"_id":456,"_created":"2022-04-25T12:44:30.964547","_updated":"2023-03-17T13:46:51.433539","app_id":198,"name":"latest","version":"latest","update_status":"published","tag":"latest"}],"domain":"","sharedPorts":["9000"],"main":true,"version":{"_etag":null,"_id":456,"_created":"2022-04-25T12:44:30.964547","_updated":"2023-03-17T13:46:51.433539","app_id":198,"name":"latest","version":"latest","update_status":"published","tag":"latest"}}],"service":[{"_etag":null,"_id":230,"_created":"2023-05-24T12:51:52.108972","_updated":"2023-08-04T12:18:34.670194","name":"pgrst","code":"pgrst","role":null,"type":"service","default":null,"popularity":null,"descr":null,"ports":null,"commercial":null,"subscription":null,"autodeploy":null,"suggested":null,"dependency":null,"avoid_render":null,"price":null,"icon":null,"category_id":null,"parent_app_id":null,"full_description":null,"description":"<p class=\"content-editor__unstyled\">PostgREST description</p>","plan_type":null,"ansible_var":null,"repo_dir":null,"cpu":"1","ram_size":"1Gb","disk_size":"1Gb","dockerhub_image":"pgrst","versions":[{"_etag":"566","_id":566,"_created":"2023-08-15T12:10:44","_updated":"2023-08-15T12:10:44.905249","app_id":230,"name":"PostgreSQL","version":"15_4","update_status":"ready_for_testing","tag":"unstable"},{"_etag":null,"_id":563,"_created":null,"_updated":"2023-05-24T12:52:15.351522","app_id":230,"name":"0.0.5","version":"0.0.5","update_status":"ready_for_testing","tag":"0.0.5"}],"domain":"","sharedPorts":["9999"],"main":true,"version":{"_etag":"566","_id":566,"_created":"2023-08-15T12:10:44","_updated":"2023-08-15T12:10:44.905249","app_id":230,"name":"PostgreSQL","version":"15_4","update_status":"ready_for_testing","tag":"unstable"}}],"servers_count":3,"custom_stack_name":"mysampleproject4","custom_stack_code":"another-bot4","custom_stack_category":["New"],"custom_stack_short_description":"sample short description","custom_stack_description":"stack description","custom_stack_publish":false,"project_name":"Smarty Bot","project_git_url":"https://github.com/vsilent/smarty.git","project_overview":"my product 1","project_description":"my product 1"}}
diff --git a/tests/custom-stack-payload-5.json b/tests/custom-stack-payload-5.json
new file mode 100644
index 00000000..70bda710
--- /dev/null
+++ b/tests/custom-stack-payload-5.json
@@ -0,0 +1 @@
+{"commonDomain":"test.app", "domainList":{}, "region":"fsn1", "zone":null, "server":"cx21", "os":"ubuntu-20.04", "ssl":"letsencrypt", "vars":[], "integrated_features":[],"extended_features":[],"subscriptions":["stack_migration"],"save_token":false,"cloud_token":"r6LAjqrynVt7pUwctVkzBlJmKjLOCxJIWjZFMLTkPYCCB4rsgphhEVhiL4DuO757","provider":"htz","stack_code":"custom-stack","selected_plan":"plan-individual-monthly","custom":{"web":[{"name":"Smarty Bot","code":"smarty-bot","domain":"smartybot.xyz","sharedPorts":["8000"],"versions":[],"custom":true,"type":"web","main":true,"_id":"lltkpq6p347kystct","dockerhub_user":"trydirect","dockerhub_name":"smarty-bot","url_app":"smartybot.xyz","url_git":"https://github.com/vsilent/smarty.git","disk_size":"1Gb","ram_size":"1Gb","cpu":1}],"feature":[{"_etag":null,"_id":198,"_created":"2022-04-27T14:10:27.280327","_updated":"2023-08-03T08:24:18.958721","name":"Portainer CE Feature","code":"portainer_ce_feature","role":["portainer-ce-feature"],"type":"feature","default":null,"popularity":null,"descr":null,"ports":{"public":["9000","8000"]},"commercial":null,"subscription":null,"autodeploy":null,"suggested":null,"dependency":null,"avoid_render":null,"price":null,"icon":{"light":{"width":1138,"height":1138,"image":"08589075-44e6-430e-98a5-f9dcf711e054.svg"},"dark":{}},"category_id":2,"parent_app_id":null,"full_description":null,"description":"<p class=\"content-editor__unstyled\"><strong style=\"font-weight:bold\">Portainer is a lightweight management UI which allows you to easily manage your different Docker environments (Docker hosts or Swarm clusters)</strong></p>","plan_type":null,"ansible_var":null,"repo_dir":null,"cpu":0.6,"ram_size":"1Gb","disk_size":"1Gb","dockerhub_image":"portainer-ce-feature","versions":[{"_etag":null,"_id":456,"_created":"2022-04-25T12:44:30.964547","_updated":"2023-03-17T13:46:51.433539","app_id":198,"name":"latest","version":"latest","update_status":"published","tag":"latest"}],"domain":"","sharedPorts":["9000"],"main":true,"version":{"_etag":null,"_id":456,"_created":"2022-04-25T12:44:30.964547","_updated":"2023-03-17T13:46:51.433539","app_id":198,"name":"latest","version":"latest","update_status":"published","tag":"latest"}}],"service":[{"_etag":null,"_id":230,"_created":"2023-05-24T12:51:52.108972","_updated":"2023-08-04T12:18:34.670194","name":"pgrst","code":"pgrst","role":null,"type":"service","default":null,"popularity":null,"descr":null,"ports":null,"commercial":null,"subscription":null,"autodeploy":null,"suggested":null,"dependency":null,"avoid_render":null,"price":null,"icon":null,"category_id":null,"parent_app_id":null,"full_description":null,"description":"<p class=\"content-editor__unstyled\">PostgREST description</p>","plan_type":null,"ansible_var":null,"repo_dir":null,"cpu":1,"ram_size":"1Gb","disk_size":"1Gb","dockerhub_image":"pgrst","versions":[{"_etag":"566","_id":566,"_created":"2023-08-15T12:10:44","_updated":"2023-08-15T12:10:44.905249","app_id":230,"name":"PostgreSQL","version":"15_4","update_status":"ready_for_testing","tag":"unstable"},{"_etag":null,"_id":563,"_created":null,"_updated":"2023-05-24T12:52:15.351522","app_id":230,"name":"0.0.5","version":"0.0.5","update_status":"ready_for_testing","tag":"0.0.5"}],"domain":"","sharedPorts":["9999"],"main":true,"version":{"_etag":"566","_id":566,"_created":"2023-08-15T12:10:44","_updated":"2023-08-15T12:10:44.905249","app_id":230,"name":"PostgreSQL","version":"15_4","update_status":"ready_for_testing","tag":"unstable"}}],"servers_count":3,"custom_stack_name":"mysampleproject5","custom_stack_code":"another-bot5","custom_stack_category":["New"],"custom_stack_short_description":"sample short description","custom_stack_description":"stack description","custom_stack_publish":false,"project_name":"Smarty Bot","project_git_url":"https://github.com/vsilent/smarty.git","project_overview":"my product 1","project_description":"my product 1"}}
diff --git a/tests/custom-stack-payload-6.json b/tests/custom-stack-payload-6.json
new file mode 100644
index 00000000..61dedb91
--- /dev/null
+++ b/tests/custom-stack-payload-6.json
@@ -0,0 +1 @@
+{"commonDomain":"test.app", "domainList":{}, "region":"fsn1", "zone":null, "server":"cx21", "os":"ubuntu-20.04", "ssl":"letsencrypt", "vars":[], "integrated_features":[],"extended_features":[],"subscriptions":["stack_migration"],"save_token":false,"cloud_token":"r6LAjqrynVt7pUwctVkzBlJmKjLOCxJIWjZFMLTkPYCCB4rsgphhEVhiL4DuO757","provider":"htz","stack_code":"another-bot8","selected_plan":"plan-individual-monthly","custom":{"web":[{"name":"Smarty Bot","code":"smarty-bot","domain":"smartybot.xyz","sharedPorts":["8000"],"versions":[],"custom":true,"type":"web","main":true,"_id":0,"dockerhub_user":"trydirect","dockerhub_name":"smarty-bot","url_app":"smartybot.xyz","url_git":"https://github.com/vsilent/smarty.git","disk_size":"1Gb","ram_size":"1Gb","cpu":1}],"feature":[{"_etag":null,"_id":198,"_created":"2022-04-27T14:10:27.280327","_updated":"2023-08-03T08:24:18.958721","name":"Portainer CE Feature","code":"portainer_ce_feature","role":["portainer-ce-feature"],"type":"feature","default":null,"popularity":null,"descr":null,"ports":{"public":["9000","8000"]},"commercial":null,"subscription":null,"autodeploy":null,"suggested":null,"dependency":null,"avoid_render":null,"price":null,"icon":{"light":{"width":1138,"height":1138,"image":"08589075-44e6-430e-98a5-f9dcf711e054.svg"},"dark":{}},"category_id":2,"parent_app_id":null,"full_description":null,"description":"<p class=\"content-editor__unstyled\"><strong style=\"font-weight:bold\">Portainer is a lightweight management UI which allows you to easily manage your different Docker environments (Docker hosts or Swarm clusters)</strong></p>","plan_type":null,"ansible_var":null,"repo_dir":null,"cpu":0.6,"ram_size":"1Gb","disk_size":"1Gb","dockerhub_image":"portainer-ce-feature","versions":[{"_etag":null,"_id":456,"_created":"2022-04-25T12:44:30.964547","_updated":"2023-03-17T13:46:51.433539","app_id":198,"name":"latest","version":"latest","update_status":"published","tag":"latest"}],"domain":"","sharedPorts":["9000"],"main":true,"version":{"_etag":null,"_id":456,"_created":"2022-04-25T12:44:30.964547","_updated":"2023-03-17T13:46:51.433539","app_id":198,"name":"latest","version":"latest","update_status":"published","tag":"latest"}}],"service":[{"_etag":null,"_id":230,"_created":"2023-05-24T12:51:52.108972","_updated":"2023-08-04T12:18:34.670194","name":"pgrst","code":"pgrst","role":null,"type":"service","default":null,"popularity":null,"descr":null,"ports":null,"commercial":null,"subscription":null,"autodeploy":null,"suggested":null,"dependency":null,"avoid_render":null,"price":null,"icon":null,"category_id":null,"parent_app_id":null,"full_description":null,"description":"<p class=\"content-editor__unstyled\">PostgREST description</p>","plan_type":null,"ansible_var":null,"repo_dir":null,"cpu":1,"ram_size":"1Gb","disk_size":"1Gb","dockerhub_image":"pgrst","versions":[{"_etag":"566","_id":566,"_created":"2023-08-15T12:10:44","_updated":"2023-08-15T12:10:44.905249","app_id":230,"name":"PostgreSQL","version":"15_4","update_status":"ready_for_testing","tag":"unstable"},{"_etag":null,"_id":563,"_created":null,"_updated":"2023-05-24T12:52:15.351522","app_id":230,"name":"0.0.5","version":"0.0.5","update_status":"ready_for_testing","tag":"0.0.5"}],"domain":"","sharedPorts":["9999"],"main":true,"version":{"_etag":"566","_id":566,"_created":"2023-08-15T12:10:44","_updated":"2023-08-15T12:10:44.905249","app_id":230,"name":"PostgreSQL","version":"15_4","update_status":"ready_for_testing","tag":"unstable"}}],"servers_count":3,"custom_stack_name":"mysampleproject6","custom_stack_code":"another-bot8","custom_stack_category":["New"],"custom_stack_short_description":"sample short description","custom_stack_description":"stack description","custom_stack_publish":false,"project_name":"Smarty Bot","project_git_url":"https://github.com/vsilent/smarty.git","project_overview":"my product 1","project_description":"my product 1"}}
diff --git a/tests/custom-stack-payload-7.json b/tests/custom-stack-payload-7.json
new file mode 100644
index 00000000..1fbfbbdf
--- /dev/null
+++ b/tests/custom-stack-payload-7.json
@@ -0,0 +1 @@
+{"commonDomain":"test.app", "domainList":{}, "region":"fsn1", "zone":null, "server":"cx21", "os":"ubuntu-20.04", "ssl":"letsencrypt", "vars":[], "integrated_features":[],"extended_features":[],"subscriptions":["stack_migration"],"save_token":false,"cloud_token":"r6LAjqrynVt7pUwctVkzBlJmKjLOCxJIWjZFMLTkPYCCB4rsgphhEVhiL4DuO757","provider":"htz","stack_code":"another-bot8","selected_plan":"plan-individual-monthly","custom":{"web":[{"name":"Smarty Bot","code":"smarty-bot","domain":"smartybot.xyz","sharedPorts":["8000"],"versions":[],"custom":true,"type":"web","main":true,"_id":0,"dockerhub_user":"trydirect","dockerhub_name":"smarty-bot","url_app":"smartybot.xyz","url_git":"https://github.com/vsilent/smarty.git","disk_size":"1Gb","ram_size":"1Gb","cpu":1}],"feature":[{"_etag":null,"_id":198,"_created":"2022-04-27T14:10:27.280327","_updated":"2023-08-03T08:24:18.958721","name":"Portainer CE Feature","code":"portainer_ce_feature","role":["portainer-ce-feature"],"type":"feature","default":null,"popularity":null,"descr":null,"ports":{"public":["9000","8000"]},"commercial":null,"subscription":null,"autodeploy":null,"suggested":null,"dependency":null,"avoid_render":null,"price":null,"icon":{"light":{"width":1138,"height":1138,"image":"08589075-44e6-430e-98a5-f9dcf711e054.svg"},"dark":{}},"category_id":2,"parent_app_id":null,"full_description":null,"description":"<p class=\"content-editor__unstyled\"><strong style=\"font-weight:bold\">Portainer is a lightweight management UI which allows you to easily manage your different Docker environments (Docker hosts or Swarm clusters)</strong></p>","plan_type":null,"ansible_var":null,"repo_dir":null,"cpu":0.6,"ram_size":"1Gb","disk_size":"1Gb","dockerhub_image":"portainer-ce-feature","versions":[{"_etag":null,"_id":456,"_created":"2022-04-25T12:44:30.964547","_updated":"2023-03-17T13:46:51.433539","app_id":198,"name":"latest","version":"latest","update_status":"published","tag":"latest"}],"domain":"","sharedPorts":["9000"],"main":true,"version":{"_etag":null,"_id":456,"_created":"2022-04-25T12:44:30.964547","_updated":"2023-03-17T13:46:51.433539","app_id":198,"name":"latest","version":"latest","update_status":"published","tag":"latest"}}],"service":[{"_etag":null,"_id":230,"_created":"2023-05-24T12:51:52.108972","_updated":"2023-08-04T12:18:34.670194","name":"pgrst","code":"pgrst","role":null,"type":"service","default":null,"popularity":null,"descr":null,"ports":null,"commercial":null,"subscription":null,"autodeploy":null,"suggested":null,"dependency":null,"avoid_render":null,"price":null,"icon":null,"category_id":null,"parent_app_id":null,"full_description":null,"description":"<p class=\"content-editor__unstyled\">PostgREST description</p>","plan_type":null,"ansible_var":null,"repo_dir":null,"cpu":1,"ram_size":"1Gb","disk_size":"1Gb","dockerhub_image":"pgrst","versions":[{"_etag":"566","_id":566,"_created":"2023-08-15T12:10:44","_updated":"2023-08-15T12:10:44.905249","app_id":230,"name":"PostgreSQL","version":"15_4","update_status":"ready_for_testing","tag":"unstable"},{"_etag":null,"_id":563,"_created":null,"_updated":"2023-05-24T12:52:15.351522","app_id":230,"name":"0.0.5","version":"0.0.5","update_status":"ready_for_testing","tag":"0.0.5"}],"domain":"","sharedPorts":["9999"],"main":true,"version":{"_etag":"566","_id":566,"_created":"2023-08-15T12:10:44","_updated":"2023-08-15T12:10:44.905249","app_id":230,"name":"PostgreSQL","version":"15_4","update_status":"ready_for_testing","tag":"unstable"}}],"servers_count":3,"custom_stack_name":"mysampleproject7","custom_stack_code":"another-bot9","custom_stack_category":["New"],"custom_stack_short_description":"sample short description","custom_stack_description":"stack description","custom_stack_publish":false,"project_name":"Smarty Bot","project_git_url":"https://github.com/vsilent/smarty.git","project_overview":"my product 1","project_description":"my product 1"}}
diff --git a/tests/custom-stack-payload-8.json b/tests/custom-stack-payload-8.json
new file mode 100644
index 00000000..7a55fc94
--- /dev/null
+++ b/tests/custom-stack-payload-8.json
@@ -0,0 +1 @@
+{"commonDomain":"","domainList":{},"region":"fsn1","zone":null,"server":"cpx11","os":"ubuntu-20.04","ssl":"letsencrypt","vars":[],"integrated_features":[],"extended_features":[],"subscriptions":[],"form_app":[],"save_token":false,"disk_type":"pd-standart","cloud_token":"****","provider":"htz","stack_code":"custom-stack","selected_plan":"plan-individual-monthly","custom":{"web":[{"_etag":null,"_id":233,"_created":"2023-07-19T06:38:57.608807","_updated":"2023-08-15T11:12:14.921797","name":"FastAPI","code":"fastapi","role":null,"type":"web","default":true,"popularity":null,"descr":null,"ports":{"public":["5050","8000","8080"]},"commercial":null,"subscription":null,"autodeploy":null,"suggested":null,"dependency":null,"avoid_render":null,"price":null,"icon":{"light":{"width":2500,"height":2500,"image":"8d1ba06d-04e2-4523-879e-2846c86a10d8.svg"},"dark":{}},"category_id":null,"parent_app_id":null,"full_description":null,"description":null,"plan_type":null,"ansible_var":null,"repo_dir":null,"cpu":0.1,"ram_size":"0.2Gb","disk_size":"0.2Gb","dockerhub_image":"fastapi","form":null,"versions":[{"_etag":null,"_id":587,"_created":null,"_updated":"2023-07-20T12:51:20.321999","app_id":233,"name":"0.0.1","version":"0.0.1","update_status":"ready_for_testing","tag":"0.0.1"},{"_etag":null,"_id":590,"_created":null,"_updated":"2023-07-20T08:36:49.651219","app_id":233,"name":"0.0.1","version":"0.0.1","update_status":"ready_for_testing","tag":"latest"},{"_etag":null,"_id":589,"_created":null,"_updated":"2023-07-20T08:36:55.200575","app_id":233,"name":"0.0.1","version":"0.0.1","update_status":"ready_for_testing","tag":"unstable"},{"_etag":"591","_id":591,"_created":null,"_updated":"2023-08-15T08:17:40.226186","app_id":233,"name":"Fastapi","version":"0.100.0","update_status":"published","tag":"stable"}],"domain":"fastapi.test","sharedPorts":["8000"],"main":true,"version":{"_etag":"591","_id":591,"_created":null,"_updated":"2023-08-15T08:17:40.226186","app_id":233,"name":"Fastapi","version":"0.100.0","update_status":"published","tag":"stable"}}],"feature":[{"_etag":null,"_id":198,"_created":"2022-04-27T14:10:27.280327","_updated":"2023-08-03T08:24:18.958721","name":"Portainer CE Feature","code":"portainer_ce_feature","role":["portainer-ce-feature"],"type":"feature","default":null,"popularity":null,"descr":null,"ports":{"public":["9000","8000"]},"commercial":null,"subscription":null,"autodeploy":null,"suggested":null,"dependency":null,"avoid_render":null,"price":null,"icon":{"light":{"width":1138,"height":1138,"image":"08589075-44e6-430e-98a5-f9dcf711e054.svg"},"dark":{}},"category_id":2,"parent_app_id":null,"full_description":null,"description":"<p class=\"content-editor__unstyled\"><strong style=\"font-weight:bold\">Portainer is a lightweight management UI which allows you to easily manage your different Docker environments (Docker hosts or Swarm clusters)</strong></p>","plan_type":null,"ansible_var":null,"repo_dir":null,"cpu":0.6,"ram_size":"1Gb","disk_size":"1Gb","dockerhub_image":"portainer-ce-feature","form":null,"versions":[{"_etag":null,"_id":456,"_created":"2022-04-25T12:44:30.964547","_updated":"2023-09-07T07:28:51.18965","app_id":198,"name":"2.18.4","version":"2.18.4","update_status":"published","tag":"2.18.4"}],"domain":"","sharedPorts":["9000"],"main":false,"version":{"_etag":null,"_id":456,"_created":"2022-04-25T12:44:30.964547","_updated":"2023-09-07T07:28:51.18965","app_id":198,"name":"2.18.4","version":"2.18.4","update_status":"published","tag":"2.18.4"}}],"service":[{"_etag":null,"_id":24,"_created":"2020-06-19T13:07:24.228389","_updated":"2023-08-08T10:34:13.4985","name":"PostgreSQL","code":"postgres","role":[],"type":"service","default":null,"popularity":null,"descr":null,"ports":null,"commercial":null,"subscription":null,"autodeploy":null,"suggested":null,"dependency":null,"avoid_render":null,"price":null,"icon":{"light":{"width":576,"height":594,"image":"fd23f54c-e250-4228-8d56-7e5d93ffb925.svg"},"dark":{}},"category_id":null,"parent_app_id":null,"full_description":null,"description":null,"plan_type":null,"ansible_var":null,"repo_dir":null,"cpu":0,"ram_size":null,"disk_size":null,"dockerhub_image":"postgres","form":null,"versions":[{"_etag":null,"_id":458,"_created":"2022-10-20T07:57:05.88997","_updated":"2023-04-05T07:24:39.637749","app_id":24,"name":"15","version":"15","update_status":"published","tag":"15"},{"_etag":null,"_id":288,"_created":"2022-10-20T07:56:16.160116","_updated":"2023-03-17T13:46:51.433539","app_id":24,"name":"10.22","version":"10.22","update_status":"published","tag":"10.22"},{"_etag":null,"_id":303,"_created":"2022-10-20T07:57:24.710286","_updated":"2023-03-17T13:46:51.433539","app_id":24,"name":"13.8","version":"13.8","update_status":"published","tag":"13.8"},{"_etag":null,"_id":266,"_created":"2022-10-20T07:56:32.360852","_updated":"2023-04-05T06:49:31.782132","app_id":24,"name":"11","version":"11","update_status":"published","tag":"11"},{"_etag":null,"_id":267,"_created":"2022-10-20T07:57:35.552085","_updated":"2023-03-17T13:46:51.433539","app_id":24,"name":"12.12","version":"12.12","update_status":"published","tag":"12.12"},{"_etag":null,"_id":38,"_created":"2020-06-19T13:07:24.258724","_updated":"2022-10-20T07:58:06.882602","app_id":24,"name":"14.5","version":"14.5","update_status":"published","tag":"14.5"},{"_etag":null,"_id":564,"_created":null,"_updated":"2023-05-24T12:55:57.894215","app_id":24,"name":"0.0.5","version":"0.0.5","update_status":"ready_for_testing","tag":"0.0.5"},{"_etag":null,"_id":596,"_created":null,"_updated":"2023-08-09T11:00:33.004267","app_id":24,"name":"Postgres","version":"15.1","update_status":"published","tag":"15.1"}],"domain":"","sharedPorts":["6372"],"main":false,"version":{"_etag":null,"_id":596,"_created":null,"_updated":"2023-08-09T11:00:33.004267","app_id":24,"name":"Postgres","version":"15.1","update_status":"published","tag":"15.1"}}],"servers_count":3,"project_name":"FastAPI example","custom_stack_code":"fastapi-example","project_git_url":"https://github.com/trydirect/fastapi.git"}}
diff --git a/tests/custom-stack-payload-9.json b/tests/custom-stack-payload-9.json
new file mode 100644
index 00000000..3bf473d6
--- /dev/null
+++ b/tests/custom-stack-payload-9.json
@@ -0,0 +1 @@
+{"commonDomain":"","domainList":{},"region":"fsn1","zone":null,"server":"cx21","os":"ubuntu-20.04","ssl":"letsencrypt","vars":[],"integrated_features":[],"extended_features":[],"subscriptions":["stack_migration"],"form_app":[],"save_token":true,"disk_type":"pd-standart","cloud_token":"D3dkDL4Qy1NpFezTl60V5RYfB5p0BSwLLoKVHhEqmJQ3jObSG77irP86e9YtCYVi","provider":"htz","stack_code":"custom-stack","selected_plan":"plan-individual-monthly","custom":{"web":[{"_etag":null,"_id":233,"_created":"2023-07-19T06:38:57.608807","_updated":"2023-08-15T11:12:14.921797","name":"FastAPI","code":"fastapi","role":null,"type":"web","default":true,"popularity":null,"descr":null,"ports":{"public":["5050","8000","8080"]},"commercial":null,"subscription":null,"autodeploy":null,"suggested":null,"dependency":null,"avoid_render":null,"price":null,"icon":{"light":{"width":2500,"height":2500,"image":"8d1ba06d-04e2-4523-879e-2846c86a10d8.svg"},"dark":{}},"category_id":null,"parent_app_id":null,"full_description":null,"description":null,"plan_type":null,"ansible_var":null,"repo_dir":null,"cpu":0.1,"ram_size":"0.2Gb","disk_size":"0.2Gb","dockerhub_image":"fastapi","form":null,"versions":[{"_etag":null,"_id":587,"_created":null,"_updated":"2023-07-20T12:51:20.321999","app_id":233,"name":"0.0.1","version":"0.0.1","update_status":"ready_for_testing","tag":"0.0.1"},{"_etag":null,"_id":590,"_created":null,"_updated":"2023-07-20T08:36:49.651219","app_id":233,"name":"0.0.1","version":"0.0.1","update_status":"ready_for_testing","tag":"latest"},{"_etag":null,"_id":589,"_created":null,"_updated":"2023-07-20T08:36:55.200575","app_id":233,"name":"0.0.1","version":"0.0.1","update_status":"ready_for_testing","tag":"unstable"},{"_etag":"591","_id":591,"_created":null,"_updated":"2023-08-15T08:17:40.226186","app_id":233,"name":"Fastapi","version":"0.100.0","update_status":"published","tag":"stable"}],"domain":"fastapi.test","sharedPorts":["8000"],"main":true,"version":{"_etag":"591","_id":591,"_created":null,"_updated":"2023-08-15T08:17:40.226186","app_id":233,"name":"Fastapi","version":"0.100.0","update_status":"published","tag":"stable"}},{"name":"smarty bot","code":"smarty-bot","domain":"smarty-bot.fastapi.test","sharedPorts":["8080"],"versions":[],"custom":true,"type":"web","main":false,"_id":0,"dockerhub_user":"vsilent","dockerhub_name":"smarty","disk_size":"1Gb","ram_size":"1Gb","cpu":1}],"feature":[{"_etag":null,"_id":198,"_created":"2022-04-27T14:10:27.280327","_updated":"2023-08-03T08:24:18.958721","name":"Portainer CE Feature","code":"portainer_ce_feature","role":["portainer-ce-feature"],"type":"feature","default":null,"popularity":null,"descr":null,"ports":{"public":["9000","8000"]},"commercial":null,"subscription":null,"autodeploy":null,"suggested":null,"dependency":null,"avoid_render":null,"price":null,"icon":{"light":{"width":1138,"height":1138,"image":"08589075-44e6-430e-98a5-f9dcf711e054.svg"},"dark":{}},"category_id":2,"parent_app_id":null,"full_description":null,"description":"<p class=\"content-editor__unstyled\"><strong style=\"font-weight:bold\">Portainer is a lightweight management UI which allows you to easily manage your different Docker environments (Docker hosts or Swarm clusters)</strong></p>","plan_type":null,"ansible_var":null,"repo_dir":null,"cpu":0.6,"ram_size":"1Gb","disk_size":"1Gb","dockerhub_image":"portainer-ce-feature","form":null,"versions":[{"_etag":null,"_id":456,"_created":"2022-04-25T12:44:30.964547","_updated":"2023-09-07T07:28:51.18965","app_id":198,"name":"2.18.4","version":"2.18.4","update_status":"published","tag":"2.18.4"}],"domain":"","sharedPorts":["9000"],"main":false,"version":{"_etag":null,"_id":456,"_created":"2022-04-25T12:44:30.964547","_updated":"2023-09-07T07:28:51.18965","app_id":198,"name":"2.18.4","version":"2.18.4","update_status":"published","tag":"2.18.4"}}],"service":[{"_etag":null,"_id":246,"_created":"2023-09-15T11:41:21.353013","_updated":"2023-09-15T11:45:40.307663","name":"Mailhog","code":"mailhog_service","role":["mailhog"],"type":"service","default":null,"popularity":null,"descr":null,"ports":{"public":["8025"],"private":["1025","8025"]},"commercial":null,"subscription":null,"autodeploy":null,"suggested":null,"dependency":null,"avoid_render":null,"price":null,"icon":{"light":{"width":48,"height":48,"image":"9ed27fd5-4745-4fa1-86a6-e53557d700ff.png"},"dark":{"width":48,"height":48,"image":"0d7b38f8-d2ad-4068-a5bd-aa32c7e1958f.png"}},"category_id":null,"parent_app_id":null,"full_description":null,"description":"<p class=\"content-editor__unstyled\">MailHog is an email testing tool for developers.</p>","plan_type":null,"ansible_var":null,"repo_dir":null,"cpu":0,"ram_size":null,"disk_size":null,"dockerhub_image":null,"form":null,"versions":[{"_etag":null,"_id":625,"_created":null,"_updated":"2023-09-15T11:42:10.48131","app_id":246,"name":"latest","version":"latest","update_status":"ready_for_production","tag":"latest"}],"domain":"","sharedPorts":["8025"],"main":false,"version":{"_etag":null,"_id":625,"_created":null,"_updated":"2023-09-15T11:42:10.48131","app_id":246,"name":"latest","version":"latest","update_status":"ready_for_production","tag":"latest"}},{"_etag":null,"_id":24,"_created":"2020-06-19T13:07:24.228389","_updated":"2023-08-08T10:34:13.4985","name":"PostgreSQL","code":"postgres","role":[],"type":"service","default":null,"popularity":null,"descr":null,"ports":null,"commercial":null,"subscription":null,"autodeploy":null,"suggested":null,"dependency":null,"avoid_render":null,"price":null,"icon":{"light":{"width":576,"height":594,"image":"fd23f54c-e250-4228-8d56-7e5d93ffb925.svg"},"dark":{}},"category_id":null,"parent_app_id":null,"full_description":null,"description":null,"plan_type":null,"ansible_var":null,"repo_dir":null,"cpu":0,"ram_size":null,"disk_size":null,"dockerhub_image":"postgres","form":null,"versions":[{"_etag":null,"_id":458,"_created":"2022-10-20T07:57:05.88997","_updated":"2023-04-05T07:24:39.637749","app_id":24,"name":"15","version":"15","update_status":"published","tag":"15"},{"_etag":null,"_id":288,"_created":"2022-10-20T07:56:16.160116","_updated":"2023-03-17T13:46:51.433539","app_id":24,"name":"10.22","version":"10.22","update_status":"published","tag":"10.22"},{"_etag":null,"_id":303,"_created":"2022-10-20T07:57:24.710286","_updated":"2023-03-17T13:46:51.433539","app_id":24,"name":"13.8","version":"13.8","update_status":"published","tag":"13.8"},{"_etag":null,"_id":266,"_created":"2022-10-20T07:56:32.360852","_updated":"2023-04-05T06:49:31.782132","app_id":24,"name":"11","version":"11","update_status":"published","tag":"11"},{"_etag":null,"_id":267,"_created":"2022-10-20T07:57:35.552085","_updated":"2023-03-17T13:46:51.433539","app_id":24,"name":"12.12","version":"12.12","update_status":"published","tag":"12.12"},{"_etag":null,"_id":38,"_created":"2020-06-19T13:07:24.258724","_updated":"2022-10-20T07:58:06.882602","app_id":24,"name":"14.5","version":"14.5","update_status":"published","tag":"14.5"},{"_etag":null,"_id":564,"_created":null,"_updated":"2023-05-24T12:55:57.894215","app_id":24,"name":"0.0.5","version":"0.0.5","update_status":"ready_for_testing","tag":"0.0.5"},{"_etag":null,"_id":596,"_created":null,"_updated":"2023-08-09T11:00:33.004267","app_id":24,"name":"Postgres","version":"15.1","update_status":"published","tag":"15.1"}],"domain":"","sharedPorts":["6372"],"main":false,"version":{"_etag":null,"_id":596,"_created":null,"_updated":"2023-08-09T11:00:33.004267","app_id":24,"name":"Postgres","version":"15.1","update_status":"published","tag":"15.1"}}],"servers_count":3,"project_name":"FastAPI example 3","custom_stack_code":"fastapi-example-3","project_git_url":"https://github.com/trydirect/fastapi.git"}}
diff --git a/custom-stack-payload-singleapp.json b/tests/custom-stack-payload-singleapp.json
similarity index 100%
rename from custom-stack-payload-singleapp.json
rename to tests/custom-stack-payload-singleapp.json
diff --git a/custom-stack-payload.json b/tests/custom-stack-payload.json
similarity index 100%
rename from custom-stack-payload.json
rename to tests/custom-stack-payload.json
